Les propriétés stockées stockent des valeurs constantes et variables dans le cadre d'une instance, tandis que les propriétés calculées calculent (plutôt que stockent) une valeur. Les propriétés calculées sont fournies par des classes, des structures et des énumérations. Les propriétés stockées sont fournies uniquement par les classes et les structures.

Les propriétés stockées

Dans sa forme la plus simple, une propriété stockée est une constante ou une variable stockée dans le cadre d'une instance d'une classe ou d'une structure particulière.

Les propriétés stockées peuvent être des propriétés stockées variables (introduites par le mot-clé var) ou des propriétés stockées constantes (introduites par le mot-clé let).

Vous pouvez fournir une valeur par défaut pour une propriété stockée dans le cadre de sa définition. Vous pouvez également définir et modifier la valeur initiale d'une propriété stockée lors de l'initialisation. Cela est vrai même pour les propriétés stockées constantes.

L'exemple ci-dessous définit une structure appelée FixedLengthRange, qui décrit une plage d'entiers dont la longueur (length) de plage ne peut pas être modifiée après sa création :


struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}

var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)
// La plage représente les valeurs entières 0, 1, et 2

rangeOfThreeItems.firstValue = 6
// La plage représente les valeurs entières 6, 7, et 8

Les instances de FixedLengthRange ont une propriété stockée variable appelée firstValue et une propriété stockée constante appelée length. Dans l'exemple ci-dessus, length est initialisé lors de la création de la nouvelle plage et ne peut plus être modifié par la suite, car il s'agit d'une propriété constante.

Propriétés stockées d'une instance de structure constante

Si vous créez une instance d'une structure et affectez cette instance à une constante, vous ne pouvez pas modifier les propriétés de l'instance, même si elles ont été déclarées comme propriétés variables :


let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
// La plage représente les valeurs entières 0, 1, 2, et 3

rangeOfFourItems.firstValue = 6
// Erreur !!!

Comme rangeOfFourItems est déclaré comme une constante (avec le mot-clé let), il n'est pas possible de modifier sa propriété firstValue, même s'il s'agit d'une propriété de variable.

Ce comportement est dû au fait que les structures sont des types de valeur. Lorsqu'une instance d'un type valeur est marquée comme une constante, toutes ses propriétés le sont également.
Il n'en va pas de même pour les classes, qui sont des types de référence. Si vous affectez une instance d'un type de référence à une constante, vous pouvez toujours modifier les propriétés de variable de cette instance.

Propriétés stockées paresseuses ou lazy

Une propriété stockée différée est une propriété dont la valeur initiale n'est calculée qu'à la première utilisation. Vous indiquez une propriété stockée différée en écrivant le modificateur lazy avant sa déclaration.

Vous devez toujours déclarer une propriété différée en tant que variable, car sa valeur initiale peut ne pas être récupérée avant la fin de l'initialisation de l'instance. Les propriétés constantes doivent toujours avoir une valeur avant la fin de l'initialisation et ne peuvent donc pas être déclarées comme paresseuses.

Les propriétés différées sont utiles lorsque la valeur initiale d'une propriété dépend de facteurs externes dont les valeurs ne sont connues qu'après l'initialisation d'une instance.

Les propriétés différées sont également utiles lorsque la valeur initiale d'une propriété nécessite une configuration complexe ou coûteuse en calcul et qui ne doit être effectuée que si ou jusqu'à ce que cela soit nécessaire.


class DataImporter {
    // DataImporter est une classe pour importer des données à partir d'un fichier externe.
    var filename = "data.txt"
    // la classe DataImporter fournirait ici une fonctionnalité d'importation de données
}

class DataManager {
    lazy var importer = DataImporter()
    var data = [String]()
    // la classe DataManager fournirait ici une fonctionnalité de gestion des données
}

let manager = DataManager()
manager.data.append("Some data")
manager.data.append("Some more data")
// l'instance DataImporter pour la propriété importer n'a pas encore été créée

Une partie des fonctionnalités de la classe DataManager réside dans la possibilité d'importer des données à partir d'un fichier. Cette fonctionnalité est fournie par la classe DataImporter, qui est supposée prendre un temps non négligeable pour s'initialiser. Cela est dû au fait qu'une instance DataImporter doit ouvrir un fichier et lire son contenu en mémoire lorsque l'instance DataImporter est initialisée.

ll est possible pour une instance DataManager de gérer ses données sans jamais importer de données à partir d'un fichier, il n'est donc pas nécessaire de créer une nouvelle instance DataImporter lors de la création de DataManager. Il est plus logique de créer l'instance DataImporter quand elle est utilisée pour la première fois.

Étant donné qu'elle est marquée avec le modificateur lazy, l'instance DataImporter de la propriété importer n'est créée que lors du premier accès à la propriété importée, par exemple lorsque sa propriété filename est interrogée :


print(manager.importer.filename)
// L'instance DataImporter pour la propriété importer a maintenant été créée
// Affiche "data.txt"

Les propriétés calculées

En plus des propriétés stockées, les classes, les structures et les énumérations peuvent définir des propriétés calculées, qui ne stockent pas réellement de valeurs. Au lieu de cela, ils fournissent un getter et un setter facultatif pour récupérer et définir indirectement d'autres propriétés et valeurs.


class Carre {
    var longueur = 1
    var perimetre: Int {
        get {
            return longueur * 4
        }
        set {
            longueur = newValue / 4
        }
    } 
}

let a = Carre()

a.longueur = 3
print(a.longueur) // Affiche 3
print(a.perimetre)  // 12 => Le setter modifie le périmètre

a.périmètre = 16
print(a.longueur) // Affiche 4
print(a.perimetre)  // 16 => Le getter récupère la valeur
  • get : Action permettant de récupérer une valeur, on appelle ça le getter.
  • set : Action permettant de modifier une valeur, on appelle ça le setter.

struct Point {
    var x = 0.0, y = 0.0
}

struct Size {
    var width = 0.0, height = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}

var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))
let initialSquareCenter = square.center
square.center = Point(x: 20.0, y: 20.0)

print("square.origin est maintenant de (\(square.origin.x), \(square.origin.y))")
// square.origin est maintenant de (15.0, 15.0)

L'exemple ci-dessus définit trois structures pour travailler avec des formes géométriques : Point qui encapsule les coordonnées x et y d'un point, Size qui encapsule width et height et enfin Rect qui définit un rectangle par un point d'origine et une taille.

La structure Rect fournit une propriété calculée appelée center. La position centrale actuelle de Rect peut toujours être déterminée à partir de son point d'origine (Point) et de sa taille (Size), vous n'avez donc pas besoin de stocker le point central comme une valeur explicite. Rect définit un getter et un setter personnalisés pour une variable calculée appelée center, pour vous permettre de travailler avec le rectangle comme si center était une propriété stockée réelle.

L'exemple ci-dessus crée une nouvelle variable Rect appelée square. Cette variable est initialisée avec un point d'origine de (0, 0), une largeur de 10 et une hauteur de 10. Ce carré est représenté par le carré vert clair dans le schéma ci-dessous.

La propriété center de la variable square est ensuite accessible via la syntaxe à points ( square.center), qui provoque l'appel du getter pour center, pour récupérer la valeur actuelle de la propriété. Plutôt que de renvoyer une valeur existante, le getter calcule et renvoie une nouvelle valeur Point pour représenter le centre du carré. Comme on peut le voir ci-dessus, le getter renvoie correctement un point central de (5, 5).

La propriété center est ensuite définie sur une nouvelle valeur de (15, 15), qui déplace le carré vers le haut et vers la droite, jusqu'à la nouvelle position indiquée par le carré vert foncé dans le diagramme ci-dessous. La définition de la propriété appelle le setter pour center, qui modifie les valeurs x et y de la propriété stockée origin, et déplace le carré vers sa nouvelle position.

Les propriétés swift
Les propriétés - Propriété calculée - Swift - Source Apple

Un getter est une méthode qui permet de lire la valeur d'une propriété. Par exemple, si vous avez une propriété nommée nom dans une classe, vous pouvez définir un getter pour cette propriété qui renvoie la valeur de nom lorsque vous appelez la propriété.

Un setter est une méthode qui permet de définir la valeur d'une propriété. Par exemple, vous pouvez définir un setter pour la propriété nom qui permet de définir la valeur de nom lorsque vous assignez une valeur à la propriété.

Voici un autre exemple pour définir un getter et un setter pour une propriété en Swift :


class Personne {
    var nom: String {
        get {
            return self._nom
        }
        set {
            self._nom = newValue
        }
    }
    private var _nom: String
    
    init(nom: String) {
        self._nom = nom
    }
}

La propriété nom est définie avec un getter et un setter. Le getter renvoie la valeur de _nom et le setter définit la valeur de _nom. La propriété _nom est déclarée comme private (privée) pour éviter tout accès direct à la propriété.

La valeur : newValue

En Swift, newValue est une constante implicite qui est disponible lors de l'utilisation d'un setter pour une propriété. Il représente la nouvelle valeur que l'on souhaite assigner à la propriété.

Voici un exemple simple qui montre comment utiliser newValue dans un setter :


class Person {
    var name: String {
        get {
            return self._name
        }
        set {
            self._name = newValue
        }
    }
    private var _name: String
    
    init(name: String) {
        self._name = name
    }
}

Dans ce code, la propriété name définit un getter et un setter. Le setter définit self._name avec newValue. Chaque fois que vous assignez une valeur à la propriété name, le setter sera appelé et la valeur sera stockée dans _name.

Si le setter d'une propriété calculée ne définit pas de nom pour la nouvelle valeur à définir, un nom par défaut newValue est utilisé. Voici une version alternative de la structure Rect qui tire parti de cette notation abrégée :


struct AlternativeRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Si le corps entier d'un getter est une seule expression, le getter renvoie implicitement à cette expression. Voici une autre version de la structure Rect qui tire parti de cette notation abrégée et de la notation abrégée pour les setters :


struct CompactRect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            Point(x: origin.x + (size.width / 2),
                  y: origin.y + (size.height / 2))
        }

        set {
            origin.x = newValue.x - (size.width / 2)
            origin.y = newValue.y - (size.height / 2)
        }
    }
}

Propriétés calculées en lecture seule

Une propriété calculée avec un getter mais aucun setter est appelée propriété calculée en lecture seule. Une propriété calculée en lecture seule renvoie toujours une valeur et est accessible via la syntaxe dot (.), mais ne peut pas être définie sur une valeur différente.

Vous devez déclarer les propriétés calculées, y compris les propriétés calculées en lecture seule, en tant que propriétés variables avec le mot clé var, car leur valeur n'est pas fixe.

Vous pouvez simplifier la déclaration d'une propriété calculée en lecture seule en supprimant le mot-clé get et ses accolades :


struct Cuboid {
    var width = 0.0, height = 0.0, depth = 0.0
    var volume: Double {
        return width * height * depth
    }
}
let fourByFiveByTwo = Cuboid(width: 4.0, height: 5.0, depth: 2.0)
print("Le volume est de \(fourByFiveByTwo.volume)")
// "Le volume est de 40.0"

Cet exemple définit une nouvelle structure appelée Cuboid, qui représente une boîte rectangulaire 3D avec les propriétés width, height et depth. Cette structure possède également une propriété calculée en lecture seule appelée volume, qui calcule et renvoie le volume actuel de Cuboid. Cela n'aurait pas de sens si volume était paramétrable, car il serait ambigu de savoir quelles sont les valeurs de width, height et depth.

Observateurs de propriétés : willset et didset

Les observateurs de propriétés observent et réagissent aux changements de valeurs d'une propriété. Les observateurs de propriétés sont appelés chaque fois que la valeur d'une propriété est définie, même si la nouvelle valeur est la même que la valeur actuelle de la propriété.

On peut ajouter des observateurs de propriétés aux propriétés stockées que l'on définit, aux propriétés stockées que l'on hérite et aux propriétés calculées que l'on hérite.

Pour une propriété héritée, vous ajoutez un observateur de propriétés en remplaçant cette propriété dans une sous-classe. Pour une propriété calculée que vous définissez, utilisez le setter de la propriété pour observer et répondre aux changements de valeurs, au lieu d'essayer de créer un observateur.

  • willSet est appelé juste avant que la valeur ne soit stockée.
  • didSet est appelé immédiatement après le stockage de la nouvelle valeur.

Si vous implémentez un observateur willSet, il transmet la nouvelle valeur de propriété en tant que paramètre constant. Vous pouvez spécifier un nom pour ce paramètre. Si vous n'écrivez pas le nom du paramètre et les parenthèses dans votre implémentation, le paramètre est rendu disponible avec un nom de paramètre par défaut de newValue.

De même, si vous implémentez un observateur didSet, un paramètre constant contenant l'ancienne valeur de la propriété lui est transmis. Vous pouvez nommer le paramètre ou utiliser le nom de paramètre par défaut oldValue. Si vous affectez une valeur à une propriété dans son propre observateur didSet, la nouvelle valeur que vous affectez remplace celle qui vient d'être définie.

L'exemple qui suit définit une nouvelle classe appelée StepCounter, qui suit le nombre total de pas qu'une personne fait en marchant. Cette classe peut être utilisée avec des données d'entrée provenant d'un podomètre ou d'un autre compteur de pas pour suivre l'exercice d'une personne au cours de sa routine quotidienne :


class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("Sur le point de définir totalSteps sur \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("On ajoute \(totalSteps - oldValue) pas")
            }
        }
    }
}

let stepCounter = StepCounter()

stepCounter.totalSteps = 200
// Sur le point de définir totalSteps sur 200
// On ajoute 200 pas

stepCounter.totalSteps = 360
// Sur le point de définir totalSteps sur 360
// On ajoute 160 pas

stepCounter.totalSteps = 896
// Sur le point de définir totalSteps sur 896
// On ajoute 536 pas

La classe StepCounter déclare une propriété totalSteps de type Int. Il s'agit d'une propriété stockée avec observateurs willSet et didSet. Ces observateurs sont appelés chaque fois que la propriété reçoit une nouvelle valeur. Cela est vrai même si la nouvelle valeur est identique à la valeur actuelle.

L' observateur willSet utilise un nom de paramètre personnalisé pour la nouvelle valeur à venir : newTotalSteps. Dans cet exemple, il imprime simplement la valeur qui est sur le point d'être définie.

L'observateur didSet est appelé après la mise à jour de la valeur de totalSteps. Il compare la nouvelle valeur de totalSteps à l'ancienne valeur. Si le nombre total de pas a augmenté, un message est imprimé pour indiquer le nombre de nouveaux pas effectués. L'observateur didSet ne fournit pas de nom de paramètre personnalisé pour l'ancienne valeur alors le nom par défaut oldValue est utilisé à la place.

Wrapper ou emballage de propriétés

Un wrapper (ou emballage) de propriété ajoute une couche de séparation entre le code qui gère la façon dont une propriété est stockée et le code qui définit une propriété.

Par exemple, si vous avez des propriétés qui fournissent des contrôles de sécurité des threads ou stockent leurs données sous-jacentes dans une base de données, vous devez écrire ce code sur chaque propriété. Lorsque vous utilisez un wrapper de propriété, vous écrivez le code de gestion une fois lorsque vous définissez le wrapper, puis réutilisez ce code de gestion en l'appliquant à plusieurs propriétés.

Pour définir un wrapper de propriété, on commencer par créer une structure, une énumération ou une classe qui définit une propriété wrappedValue. Dans le code ci-dessous, la structure TwelveOrLess garantit que la valeur qu'elle encapsule contient toujours un nombre inférieur ou égal à 12. Si vous lui demandez de stocker un plus grand nombre, il stocke 12 à la place.


@propertyWrapper
struct TwelveOrLess {
    private var number: Int
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

Le setter s'assure que les nouvelles valeurs sont inférieures à 12 et le getter renvoie la valeur stockée.

La déclaration de la variable number est marquée comme private, cela garantit qu'elle n'est utilisée que dans l'implémentation de TwelveOrLess. Le code qui est écrit n'importe où ailleurs accède à la valeur de number à l'aide du getter et du setter pour wrappedValue, et ne peut pas utiliser number directement.

On applique un wrapper à une propriété en écrivant le nom du wrapper avant la propriété en tant qu'attribut. Ci-dessous, la structure qui stocke un petit rectangle, utilise la même définition que celle implémentée par le wrapper de TwelveOrLess (soit un chiffre inférieur à 12) :


struct SmallRectangle {
    @TwelveOrLess var height: Int  
    // Valeur qui sera > 12
    @TwelveOrLess var width: Int  
    // Valeur qui sera > 12
}

var rectangle = SmallRectangle()
print(rectangle.height)  
// "0"

rectangle.height = 10
print(rectangle.height)  
// "10"

rectangle.height = 24
print(rectangle.height)  
// "12"

Les propriétés height et width obtiennent leurs valeurs initiales à partir de la définition de TwelveOrLess, qui prend TwelveOrLess.number soit la valeur zéro. Le setter dans TwelveOrLess traite 10 comme une valeur valide, de sorte que le stockage du nombre 10 dans rectangle.height se déroule tel qu'il est écrit. Cependant, 24 est plus grand que TwelveOrLess ne le permet, cette valeur est remplacé par 12.

Les valeurs initiales des propriétés enveloppées

L'exemple précédent définit la valeur initiale de la propriété encapsulée en donnant à number une valeur initiale dans la définition de TwelveOrLess. Le code qui utilise ce wrapper de propriétés ne peut pas spécifier une valeur initiale différente pour une propriété encapsulée par TwelveOrLess. Pour prendre en charge la définition d'une valeur initiale ou une autre personnalisation, le wrapper de propriétés doit ajouter un initialiseur. Voici une version étendue qui définit les initialiseurs définissant la valeur encapsulée et maximale :


@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

La définition de SmallNumber inclut trois initialiseurs : init(), init(wrappedValue:) et init(wrappedValue:maximum:) pour définir la valeur encapsulée et la valeur maximale. Nous aborderons le sujet de l'initialisation dans quelques chapitres.

Lorsque vous appliquez un wrapper à une propriété et que vous ne spécifiez pas de valeur initiale, Swift utilise l'initialiseur init() pour configurer le wrapper.


struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}
var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)  
// "0 0"

Les instances de SmallNumber qui "wrap" height et width sont créées en appelant SmallNumber(). Le code à l'intérieur de cet initialiseur définit la valeur encapsulée initiale et la valeur maximale initiale, en utilisant les valeurs par défaut de zéro et 12.

Lorsque vous spécifiez une valeur initiale pour une propriété, Swift utilise l'initialiseur init(wrappedValue:) pour configurer le wrapper. Par exemple :


struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// "1 1"

Lorsque vous écrivez = 1 sur une propriété avec un wrapper, cela se traduit par un appel à l'initialiseur init(wrappedValue:. Les instances de SmallNumber qui "wrap" height et width sont créées en appelant SmallNumber(wrappedValue: 1). L'initialiseur utilise la valeur encapsulée qui est spécifiée ici, et il utilise la valeur maximale par défaut de 12.

Lorsque vous écrivez des arguments entre parenthèses après l'attribut personnalisé, Swift utilise l'initialiseur qui accepte ces arguments pour configurer le wrapper. Par exemple, si vous fournissez une valeur initiale et une valeur maximale, Swift utilise l'initialiseur init(wrappedValue:maximum:) :


struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// "5 4"

Lorsque vous incluez des arguments de wrapper de propriétés, vous pouvez également spécifier une valeur initiale à l'aide de l'affectation. Swift traite l'affectation comme un argument wrappedValue et utilise l'initialiseur qui accepte les arguments que vous incluez. Par exemple :


struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)  
// "1"

mixedRectangle.height = 20
print(mixedRectangle.height)
// "12"

L'instance de SmallNumber qui enveloppe height est créée en appelant SmallNumber(wrappedValue: 1), qui utilise la valeur maximale par défaut de 12. L'instance qui enveloppe with est créée en appelant SmallNumber(wrappedValue: 2, maximum: 9)

Projection d'une valeur à partir d'un wrapper de propriétés

En plus de la valeur enveloppée, un wrapper de propriété peut présenter des fonctionnalités supplémentaires en définissant une valeur projetée. Par exemple, un wrapper de propriété qui gère l'accès à une base de données peut exposer une méthode flushDatabaseConnection() sur sa valeur projetée. Le nom de la valeur projetée est le même que celui de la valeur encapsulée, sauf qu'il commence par un signe dollar ($). Étant donné que votre code ne peut pas définir de propriétés commençant par $, la valeur projetée n'interfère jamais avec les propriétés que vous définissez.

Dans l'exemple ci-dessus, si vous essayez de définir la propriété sur un nombre trop grand, le wrapper de propriété ajuste le nombre avant de le stocker. Le code ci-dessous ajoute une propriété projectedValue à la structure SmallNumber pour savoir si le wrapper de propriété a ajusté la nouvelle valeur de la propriété avant de stocker cette nouvelle valeur.


@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// "true"

L'écriture someStructure.$someNumber accède à la valeur projetée du wrapper. Après avoir stocké un petit nombre comme quatre, la valeur de someStructure.$someNumber est false. En revanche, la valeur projetée est true après avoir essayé de stocker un nombre trop grand, comme 55.

Un wrapper de propriété peut renvoyer une valeur de n'importe quel type comme valeur projetée. Dans cet exemple, le wrapper de propriété n'expose qu'une seule information, à savoir si le nombre a été ajusté, il expose donc cette valeur booléenne comme sa valeur projetée. Un wrapper qui a besoin d'exposer plus d'informations peut renvoyer une instance d'un autre type de données.

Lorsque vous accédez à une valeur projetée à partir du code qui fait partie du type, comme un getter de propriété ou une méthode d'instance, vous pouvez omettre self. avant le nom de la propriété, tout comme pour accéder à d'autres propriétés. Le code dans l'exemple suivant fait référence à la valeur projetée du wrapper autour de height et width pour $height et $width :


enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

L'accès à height et width se comporte de la même manière que l'accès à toute autre propriété. À la fin de la méthode resize(to:), l'instruction return vérifie $height et $width pour déterminer si le wrapper de propriété a ajusté la hauteur ou la largeur.

Variables globales et locales

  • Variable globale : Une variable globale est une variable déclarée en dehors de toute fonction, méthode, fermeture, type ou boucle dans un programme. Elle est disponible dans tout le programme et peut être modifiée depuis n'importe quelle partie du code.
  • Variable locale : Une variable locale est déclarée à l'intérieur d'une fonction, méthode, fermeture, type ou boucle et est disponible uniquement à l'intérieur de celle-ci. Elles ne peuvent pas être modifiées depuis d'autres parties du code.
Les constantes et les variables globales sont toujours calculées paresseusement, de la même manière que les propriétés stockées différées. Contrairement à ces propriétés stockées différées, les constantes globales et les variables n'ont pas besoin d'être marquées avec le modificateur lazy.
Les constantes et variables locales ne sont jamais calculées paresseusement.

Vous pouvez appliquer un wrapper de propriété à une variable stockée locale, mais pas à une variable globale ou à une variable calculée. Par exemple, dans le code ci-dessous, myNumber utilise SmallNumber comme wrapper de propriété :


func someFunction() {
    @SmallNumber var myNumber: Int = 0

    myNumber = 10
    // myNumber vaut 10

    myNumber = 24
    // myNumber vaut 12
}

La définition de la valeur de myNumber à 10 est valide. Ensuite, étant donné que le wrapper de propriété n'autorise pas les valeurs supérieures à 12, il définit la valeur de myNumber sur 12 au lieu de 24.

Les propriétés de type

Les propriétés d'instance sont des propriétés qui appartiennent à une instance d'un type particulier. Chaque fois que vous créez une nouvelle instance de ce type, elle possède son propre ensemble de valeurs de propriété, distinct de toute autre instance.

Vous pouvez également définir des propriétés qui appartiennent au type lui-même, et non à une seule instance de ce type. Il n'y aura jamais qu'une seule copie de ces propriétés, quel que soit le nombre d'instances de ce type que vous créez. Ces types de propriétés sont appelés propriétés de type.

Les propriétés de type sont utiles pour définir des valeurs universelles pour toutes les instances d'un type particulier, telles qu'une propriété constante que toutes les instances peuvent utiliser ou une propriété variable qui stocke une valeur globale pour toutes les instances de ce type.

Les propriétés de type stockées peuvent être des variables ou des constantes. Les propriétés de type calculées sont toujours déclarées en tant que propriétés de variable, de la même manière que les propriétés d'instance calculées.

Contrairement aux propriétés d'instance stockées, vous devez toujours donner aux propriétés de type stockées une valeur par défaut. C'est parce que le type lui-même n'a pas d'initialiseur qui peut affecter une valeur à une propriété de type stockée au moment de l'initialisation.

Syntaxe de la propriété de type

En langage Swift, les propriétés de type sont écrites dans le cadre de la définition du type, dans les accolades extérieures du type, et chaque propriété de type est explicitement limitée au type qu'elle prend en charge.

Vous définissez les propriétés du type avec le mot-clé static. Pour les propriétés de type calculées pour les types de classe, vous pouvez utiliser le mot-clé class à la place pour permettre aux sous-classes de remplacer l'implémentation de la superclasse (nous aborderons ce sujet dans les prochains chapitres). L'exemple ci-dessous montre la syntaxe des propriétés de type stockées et calculées :


struct SomeStructure {
    static var storedTypeProperty = "Une valeur."
    static var computedTypeProperty: Int {
        return 1
    }
}
enum SomeEnumeration {
    static var storedTypeProperty = "Une valeur"
    static var computedTypeProperty: Int {
        return 6
    }
}
class SomeClass {
    static var storedTypeProperty = "Une valeur"
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}
Les exemples de propriétés de type calculé ci-dessus concernent les propriétés de type calculé en lecture seule, mais vous pouvez également définir des propriétés de type calculé en lecture-écriture avec la même syntaxe que pour les propriétés d'instance calculées.

Interrogation et définition des propriétés de type

Les propriétés de type sont interrogées et définies avec une syntaxe à points, tout comme les propriétés d'instance. Cependant, les propriétés de type sont interrogées et définies sur le type, et non sur une instance de ce type. Par exemple :


print(SomeStructure.storedTypeProperty)
// "Une valeur"

SomeStructure.storedTypeProperty = "Une autre valeur"
print(SomeStructure.storedTypeProperty)
// "Une autre valeur"

print(SomeEnumeration.computedTypeProperty)
// "6"

print(SomeClass.computedTypeProperty)
// "27"

Les exemples suivant utilisent deux propriétés de type stockées dans le cadre d'une structure qui modélise un indicateur de niveau audio pour un certain nombre de canaux audio. Chaque canal a un niveau audio entier compris entre 0 et 10 inclus. Les canaux audio décrits ci-dessus sont représentés par des instances de la structure AudioChannel :


struct AudioChannel {
    static let thresholdLevel = 10
    static var maxInputLevelForAllChannels = 0
    var currentLevel: Int = 0 {
        didSet {
            if currentLevel > AudioChannel.thresholdLevel {
                // Limiter le nouveau niveau audio au niveau maximum
                currentLevel = AudioChannel.thresholdLevel
            }
            if currentLevel > AudioChannel.maxInputLevelForAllChannels {
                // Stocker le nouveau niveau d'entrée comme maximal global
                AudioChannel.maxInputLevelForAllChannels = currentLevel
            }
        }
    }
}

La structure AudioChannel définit deux propriétés de type stockées pour prendre en charge sa fonctionnalité. Le premier, thresholdLevel, définit la valeur de seuil maximale qu'un niveau audio peut prendre. Il s'agit d'une valeur constante de 10 pour toutes les instances AudioChannel. Si un signal audio arrive avec une valeur supérieure à 10, il sera plafonné à cette valeur de seuil.

La deuxième propriété de type est une propriété stockée variable appelée maxInputLevelForAllChannels. Cela permet de suivre la valeur d'entrée maximale qui a été reçue par n'importe quelle instance AudioChannel. Il commence par une valeur initiale de 0.

La structure AudioChannel définit également une propriété d'occurrence stockée appelée currentLevel, qui représente le niveau audio actuel du canal sur une échelle de 0 à 10.

La propriété currentLevela un observateur didSet de propriété pour vérifier la valeur de currentLevel chaque fois qu'elle est définie. Cet observateur effectue deux contrôles :

  • Si la nouvelle valeur de currentLevel est supérieure à celle autorisée par thresholdLevel, la propriété observée plafonne currentLevel par rapport à thresholdLevel.
  • Si la nouvelle valeur de currentLevel (après tout plafonnement) est supérieure à toute valeur précédemment reçue par une instance d' AudioChannel , l'observateur de propriété stocke la nouvelle valeur currentLevel dans la propriété de type maxInputLevelForAllChannels.
Dans la première de ces deux vérifications, l'observateur didSet définit une valeur currentLevel différente. Cependant, cela ne provoque pas le rappel de l'observateur.

Vous pouvez utiliser la structure AudioChannel pour créer deux nouveaux canaux audio appelés leftChannel et rightChannel, pour représenter les niveaux audio d'un système audio stéréo :


var leftChannel = AudioChannel()
var rightChannel = AudioChannel()

Si vous définissez le currentLevel du canal gauche sur 7, vous pouvez voir que la propriété type maxInputLevelForAllChannels est mise à jour pour égaler 7 :


leftChannel.currentLevel = 7
print(leftChannel.currentLevel)
// "7"

print(AudioChannel.maxInputLevelForAllChannels)
// "7"

Si vous essayez de définir le currentLevel du canal droit sur 11, vous pouvez voir que la propriété du canal droit currentLevel est plafonnée à la valeur maximale de 10, et la propriété type maxInputLevelForAllChannels est mise à jour pour égaler 10 :


rightChannel.currentLevel = 11
print(rightChannel.currentLevel)
// "10"
print(AudioChannel.maxInputLevelForAllChannels)
// "10"