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)

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("the volume is \(fourByFiveByTwo.volume)")
// Prints "the volume is 40.0"

Observateurs de propriétés

Les observateurs de propriétés observent et répondent 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é.

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.

class StepCounter {
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("About to set totalSteps to \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue  {
                print("Added \(totalSteps - oldValue) steps")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// About to set totalSteps to 200
// Added 200 steps
stepCounter.totalSteps = 360
// About to set totalSteps to 360
// Added 160 steps
stepCounter.totalSteps = 896
// About to set totalSteps to 896
// Added 536 steps

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é.

Pour définir un wrapper de propriété, vous créez 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
    init() { self.number = 0 }  // initialise la variable à 0
    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 la marque 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)  // Prints "0"

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

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

Valeurs initiales des propriétés enveloppées

L'exemple ci-dessus 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)
    }
}

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)  // Prints "0 0"

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)
// Prints "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "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)  // Prints "1"

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

On peut définir une valeur projetée. Par exemple, un wrapper de propriétés 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 $.

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


@propertyWrapper
struct SmallNumber {
    private var number: Int
    var projectedValue: Bool
    init() {
        self.number = 0
        self.projectedValue = false
    }
    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

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

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

Variables globales et locales

Les variables globales sont des variables définies en dehors de tout contexte de fonction, méthode, fermeture ou type. Les variables locales sont des variables définies dans une fonction, une méthode ou un contexte de fermeture.

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.

Syntaxe de propriétés de types de classes

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


struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}

enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}

class SomeClass {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 27
    }
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}