Découverte de Swift (5)
Au programme de ce cinquième épisode de ma découverte de Swift, une introduction aux structures et aux classes, avant d'aborder des sujets plus précis à leur sujet dans les prochains épisodes.
Les structures et les classes permettent de créer des types de données plus ou moins complexes, accompagnées de fonctionnalités permettant de les manipuler.
Structures vs. classes
Structures et classes : points communs
Structures et classes partagent plusieurs points communs. Elles peuvent toutes deux : – définir des propriétés pour stocker des valeurs – définir des méthodes pour fournir des fonctionnalités – définir des subscripts pour donner accès à leurs valeurs – définir des initialisateurs pour mettre en place leur état initial – faire l'objet d'extensions pour étendre leurs fonctionnalités – se conformer à des protocoles pour fournir certaines fonctionnalités standards
Particularités des classes
Les classes présentes des particularités que n'ont pas les structures : – l'héritage permet à une classe d'hériter des caractéristiques d'une autre classe – il est possible de vérifier et d'interpréter le type d'une instance à l'exécution – des désinitialisateurs permet de “nettoyer” les ressources affectées à une instance – plusieurs références peuvent être liées à la même instance
Définition
Nous pouvons commencer par déclarer une structure simple et une classe à peine moins simple :
struct Resolution {
var width = 0
var height = 0
}
class VideoMode {
var resolution = Resolution()
var interlaced = false
var frameRate = 0.0
var name: String?
}
La structure Resolution
ne possède que 2 propriétés, toutes deux de type implicite Int
pour stocker la largeur et la hauteur.
La classe VideoMode
possède 4 priorités : une Resolution
qui est une instance de la structure précédente, ainsi qu'un Bool
pour l'entrelaçage, un Double
pour le frame rate, et un String
optionnel pour le nom.
Création d'une instance
Rien de plus simple que créer une instance d'une structure ou d'une classe :
let someResolution = Resolution()
let someVideoMode = VideoMode()
Accès aux propriétés
Chaque instance est créée ainsi avec comme propriétés les valeurs par défaut prévues dans la définition de la structure ou de la classe.
On peut le vérifier facilement en accédant en lecture aux propriétés de nos instances :
print("The width of someResolution is \(someResolution.width)")
// Prints "The width of someResolution is 0"
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 0"
ll est également possible d'accéder de la même façon à une propriété pour la modifier :
someVideoMode.resolution.width = 1280
print("The width of someVideoMode is \(someVideoMode.resolution.width)")
// Prints "The width of someVideoMode is 1280"
Initialisation d'une structure par ses propriétés
Il est possible de créer une instance de structure à partir de ses propriétés, grâce à un initialisateur par défaut fourni par Swift :
let vgaResolution = Resolution(width: 640, height: 480)
print("The width of vgaResolution is \(vgaResolution.width)")
// Prints "The width of someResolution is 640"
Cet initialisateur attend autant de paramètres que la structure possède de propriétés, il suffit alors de les renseigner pour créer une instance avec les valeurs souhaitées.
Structures = type par valeur
Les structures sont des types par valeur, ce qui signifie que leur valeur est copiée lorsqu'ils sont affectés à une constante ou une variable, ou qu'ils sont passés à une fonction.
Ce mode de fonctionnement par valeur est également celui de la plupart des types standards proposés dans Swift, comme les Int
les Double
, les String
, etc.
Un exemple simple permet d'illustrer ce mode de fonctionnement :
let hdResolution = Resolution(width: 1920, height: 1080)
var cinemaResolution = hdResolution
cinemaResolution.width = 2048
print("The width of cinemaResolution is \(cinemaResolution.width)")
// Prints "The width of cinemaResolution is 2048"
print("The width of hdResolution is \(hdResolution.width)")
// Prints "The width of hdResolution is 1920"
Nous avons créé hdResolution
, une première instance de la structure Resolution
, avec une largeur de 1920 et une hauteur de 1080. Nous avons ensuite créé une seconde instance cinemaResolution
par copie de la première.
Ensuite, nous avons modifié la largeur de l'instance cinemaResolution
.
En affichant successivement la largeur des 2 instances, nous pouvons vérifier que seule celle de cinemaResolution
a été modifiée ; celle de hdResolution
est restée inchangée.
C'est le cas car l'initialisation de la seconde instance s'est faite par copie de la première : même si l'une a été créée à l'identique de l'autre, les deux instances ne partagent pas les mêmes données, elles vivent indépendamment l'une de l'autre après l'initialisation.
Classes = type par référence
A l'inverse, les classes sont des types par référence. Cela signifie que l'affectation d'une instance de classe à une autre instance ne crée par une copie de l'instance mais qu'au contraire les deux instances partagent la même référence. Modifier le contenu de l'une impacte donc directement l'autre.
L'exemple suivant le montre bien :
let tenEightyVideoMode = VideoMode()
tenEightyVideoMode.resolution = hdResolution
tenEightyVideoMode.interlaced = true
tenEightyVideoMode.name = "1080i"
tenEightyVideoMode.frameRate = 25.0
let alsoTenEightyVideoMode = tenEightyVideoMode
alsoTenEightyVideoMode.frameRate = 30.0
print("The framerate of alsoTenEightyVideoMode is \(alsoTenEightyVideoMode.frameRate)")
// Prints "The framerate of alsoTenEightyVideoMode is 30.0"
print("The framerate of tenEightyVideoMode is \(tenEightyVideoMode.frameRate)")
// Prints "The framerate of tenEightyVideoMode is 30.0"
Nous créons tout d'abord une instance tenEightyVideoMode
avec un certain nombres de propriétés, donc un frameRate
de 25.0
.
Nous crérons ensuite une seconde instance anotherTenEightyVideoMode
par affectation de la première instance tenEightyVideoMode
.
Lorsque nous modifions le frameRate
de l'instance anotherTenEightyVideoMode
, nous pouvons constater que la modifiation a également impacté l'instance tenEightyVideoMode
.
Les deux instances sont deux références au même objet : modifier l'une impacte directement l'autre.
Opérateurs d'identité
En lien avec cette notion de type par référence, il existe 2 opérateurs d'identité :
===
: opérateur “identique à”!==
: opérateur “non identique à”
Deux constantes ou variables sont considérées comme “identiques” (au sens de l'opérateur ===
) si elles contiennent une référence à la même instance d'une classe.
Dans le cas de notre exemple précédent, tenEightyVideoMode
et anotherTenEightyVideoMode
sont identiques puisqu'ils sont tous deux une référence à la même instance de la classe VideoMode
, comme le montre le code suivant :
swift
if tenEightyVideoMode === alsoTenEightyVideoMode {
print("tenEightyVideoMode and alsoTenEightyVideoMode refer to the same VideoMode instance")
}
// Prints "tenEightyVideoMode and alsoTenEightyVideoMode refer to the same VideoMode instance"
`
Il ne faut donc pas confondre l'opérateur d'identité ===
et l'opérateur d'égalité ==
:
– L'identité contrôle si deux constantes ou variables réfèrent à la même instance.
– L'égalité contrôle si deux instances sont équivalentes en valeur (cette égalité pouvant faire l'objet de règles spécifiques définies dans la classe)
Et la suite ?
Dans le prochain billet, nous entrerons plus dans le détail des structures et des classes, en s'intéressant de plus près aux propriétés.
Zéro Tech, le blog Tech de Zéro Janvier – @zerojanvier@diaspodon.fr