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 Resolutionne possède que 2 propriétés, toutes deux de type implicite Int pour stocker la largeur et la hauteur.

La classe VideoModepossède 4 priorités : une Resolutionqui 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 Stringoptionnel 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 cinemaResolutionpar 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 cinemaResolutiona été modifiée ; celle de hdResolutionest 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 frameRatede 25.0.

Nous crérons ensuite une seconde instance anotherTenEightyVideoMode par affectation de la première instance tenEightyVideoMode.

Lorsque nous modifions le frameRatede 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é :

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.

#swift

Zéro Tech, le blog Tech de Zéro Janvier@zerojanvier@mamot.fr