Pourquoi créer des «options implicitement non emballées», car cela implique que vous savez qu'il y a une valeur?

493

Pourquoi voudriez-vous créer une option "implicitement non emballée" plutôt que de créer simplement une variable ou une constante régulière? Si vous savez qu'il peut être déballé avec succès, alors pourquoi créer une option en premier lieu? Par exemple, pourquoi est-ce:

let someString: String! = "this is the string"

va être plus utile que:

let someString: String = "this is the string"

Si «les optionnels indiquent qu'une constante ou une variable ne peut avoir« aucune valeur »», mais «il est parfois clair d'après la structure d'un programme qu'une optionnelle aura toujours une valeur après que cette valeur ait été définie pour la première fois», quel est l'intérêt de ce qui en fait une option en premier lieu? Si vous savez qu'une option aura toujours une valeur, cela ne la rend-elle pas facultative?

Johnston
la source

Réponses:

127

Considérons le cas d'un objet qui peut avoir des propriétés nulles pendant sa construction et sa configuration, mais qui est immuable et non nul par la suite (NSImage est souvent traité de cette façon, bien que dans son cas, il soit toujours utile de muter parfois). Les options implicitement déballées nettoieraient beaucoup son code, avec une perte de sécurité relativement faible (tant que la seule garantie détenue serait sûre).

(Edit) Pour être clair cependant: les options régulières sont presque toujours préférables.

Catfish_Man
la source
459

Avant de pouvoir décrire les cas d'utilisation des options implicitement non enveloppées, vous devez déjà comprendre ce que sont les options et options optionnelles implicitement non enveloppées dans Swift. Si vous ne le faites pas, je vous recommande d'abord de lire mon article sur les options

Quand utiliser une option implicitement déballée

Il y a deux raisons principales pour lesquelles on créerait une option implicitement non emballée. Tout cela a à voir avec la définition d'une variable qui ne sera jamais accessible quand nilsinon, le compilateur Swift vous forcera toujours à déballer explicitement un Facultatif.

1. Une constante qui ne peut pas être définie lors de l'initialisation

Chaque constante de membre doit avoir une valeur au moment où l'initialisation est terminée. Parfois, une constante ne peut pas être initialisée avec sa valeur correcte lors de l'initialisation, mais il peut toujours être garanti d'avoir une valeur avant d'être accessible.

L'utilisation d'une variable facultative permet de contourner ce problème car un facultatif est automatiquement initialisé avec nilet la valeur qu'il contiendra éventuellement restera immuable. Cependant, il peut être difficile de déballer constamment une variable dont vous savez qu'elle n'est pas nulle. Les options implicitement non enveloppées offrent les mêmes avantages qu'une option, avec l'avantage supplémentaire de ne pas avoir à la déballer explicitement partout.

Un bon exemple de cela est lorsqu'une variable membre ne peut pas être initialisée dans une sous-classe UIView jusqu'à ce que la vue soit chargée:

class MyView: UIView {
    @IBOutlet var button: UIButton!
    var buttonOriginalWidth: CGFloat!

    override func awakeFromNib() {
        self.buttonOriginalWidth = self.button.frame.size.width
    }
}

Ici, vous ne pouvez pas calculer la largeur d'origine du bouton avant le chargement de la vue, mais vous savez que awakeFromNib sera appelée avant toute autre méthode sur la vue (autre que l'initialisation). Au lieu de forcer la valeur à être explicitement dépliée de façon inutile dans toute votre classe, vous pouvez la déclarer comme facultative implicitement non enveloppée.

2. Lorsque votre application ne peut pas récupérer d'un être variable nil

Cela devrait être extrêmement rare, mais si votre application ne peut pas continuer à s'exécuter si une variable est nilaccessible, ce serait une perte de temps de s'embêter à la tester nil. Normalement, si vous avez une condition qui doit absolument être vraie pour que votre application continue de fonctionner, vous utiliserez un assert. Une option implicitement non enveloppée contient une assertion pour nil. Même alors, il est souvent bon de déballer l'option et d'utiliser une assertion plus descriptive si elle est nulle.

Quand ne pas utiliser une option implicitement déballée

1. Variables des membres calculées avec paresse

Parfois, vous avez une variable membre qui ne doit jamais être nulle, mais elle ne peut pas être définie sur la valeur correcte lors de l'initialisation. Une solution consiste à utiliser une option implicitement non enveloppée, mais une meilleure façon consiste à utiliser une variable paresseuse:

class FileSystemItem {
}

class Directory : FileSystemItem {
    lazy var contents : [FileSystemItem] = {
        var loadedContents = [FileSystemItem]()
        // load contents and append to loadedContents
        return loadedContents
    }()
}

Désormais, la variable membre contentsn'est initialisée que lors du premier accès. Cela donne à la classe une chance d'entrer dans le bon état avant de calculer la valeur initiale.

Remarque: Cela peut sembler contredire # 1 d'en haut. Cependant, il y a une distinction importante à faire. Ce qui buttonOriginalWidthprécède doit être défini pendant viewDidLoad pour empêcher quiconque de modifier la largeur des boutons avant d'accéder à la propriété.

2. Partout ailleurs

Pour la plupart, les options implicitement non enveloppées doivent être évitées car si elles sont utilisées par erreur, l'intégralité de votre application se bloque lors de son accès nil. Si vous n'êtes jamais sûr de savoir si une variable peut être nulle, utilisez toujours par défaut une optionnelle normale. Déballer une variable qui n'est jamais nilcertainement ne fait pas grand mal.

drewag
la source
4
Cette réponse devrait être mise à jour pour la version bêta 5. Vous ne pouvez plus l'utiliser if someOptional.
Père Noël
2
@SantaClaus hasValueest défini directement sur Facultatif. Je préfère la sémantique de hasValuecelle de != nil. Je pense que c'est beaucoup plus compréhensible pour les nouveaux programmeurs qui n'ont pas utilisé nildans d'autres langues. hasValueest beaucoup plus logique que nil.
drewag
2
On dirait qu'il a hasValueété retiré de la bêta 6. Ash l'a bien remis ... github.com/AshFurrow/hasValue
Chris Wagner
1
@newacct Concernant le type de retour des initialiseurs Objc, il s'agit plutôt d'une option implicite implicitement non enveloppée. Le comportement que vous avez décrit pour l'utilisation d'une option "non facultative" est exactement ce que ferait une option implicitement non enveloppée (n'échoue pas tant qu'elle n'est pas accessible). En ce qui concerne le fait de laisser le programme échouer plus tôt en dépliant de force, je conviens que l'utilisation d'un non-optionnel est préférable, mais ce n'est pas toujours possible.
drewag
1
@confile no. Quoi qu'il en soit, il apparaîtra dans Objective-C sous la forme d'un pointeur (s'il était facultatif, implicitement déplié ou non facultatif).
drewag
56

Les options optionnelles implicitement non encapsulées sont utiles pour présenter une propriété comme non optionnelle lorsqu'elle doit vraiment être optionnelle sous les couvertures. Cela est souvent nécessaire pour «faire le nœud» entre deux objets liés qui ont chacun besoin d'une référence à l'autre. Cela a du sens lorsqu'aucune référence n'est réellement facultative, mais l'un d'eux doit être nul pendant l'initialisation de la paire.

Par exemple:

// These classes are buddies that never go anywhere without each other
class B {
    var name : String
    weak var myBuddyA : A!
    init(name : String) {
        self.name = name
    }
}

class A {
    var name : String
    var myBuddyB : B
    init(name : String) {
        self.name = name
        myBuddyB = B(name:"\(name)'s buddy B")
        myBuddyB.myBuddyA = self
    }
}

var a = A(name:"Big A")
println(a.myBuddyB.name)   // prints "Big A's buddy B"

Toute Binstance doit toujours avoir une myBuddyAréférence valide , donc nous ne voulons pas que l'utilisateur la traite comme facultative, mais nous avons besoin qu'elle soit facultative pour que nous puissions construire un Bavant d'avoir unA se référer.

TOUTEFOIS! Ce type d'exigence de référence mutuelle est souvent une indication d'un couplage serré et d'une mauvaise conception. Si vous vous reposez sur des options implicitement déballées, vous devriez probablement envisager une refactorisation pour éliminer les dépendances croisées.

n8gray
la source
7
Je pense que l'une des raisons pour lesquelles ils ont créé cette fonctionnalité de langage est@IBOutlet
Jiaaro
11
+1 pour la mise en garde "CEPENDANT". Ce n'est peut-être pas toujours vrai, mais c'est certainement quelque chose à surveiller.
JMD
4
Vous avez toujours un cycle de référence fort entre A et B. Les options implicitement déballées NE créent PAS une référence faible. Vous devez toujours déclarer myByddyA ou myBuddyB comme faible (probablement myBuddyA)
drewag
6
Pour être encore plus clair pourquoi cette réponse est fausse et dangereusement trompeuse: les options implicitement non enveloppées n'ont absolument rien à voir avec la gestion de la mémoire et n'empêchent pas les cycles de conservation. Cependant, les options implicitement non enveloppées sont toujours utiles dans les circonstances décrites pour la configuration de la référence bidirectionnelle. Donc, il suffit d'ajouter la weakdéclaration et de supprimer "sans créer un cycle de rétention solide"
drewag
1
@drewag: Vous avez raison - j'ai modifié la réponse pour supprimer le cycle de rétention. Je comptais affaiblir la référence arrière, mais je suppose que cela m'a échappé.
n8gray
37

Les options implicitement déballées sont un compromis pragmatique pour rendre le travail dans un environnement hybride qui doit interagir avec les cadres Cocoa existants et leurs conventions plus agréables, tout en permettant également une migration progressive vers un paradigme de programmation plus sûr - sans pointeurs nuls - imposé par le compilateur Swift.

Swift book, dans le chapitre The Basics , section Implicitly Unwrapped Optionals dit:

Les options implicitement déballées sont utiles lorsque la valeur d'une option est confirmée pour exister immédiatement après que l'option est définie pour la première fois et peut certainement être supposée exister à tout moment par la suite. L'utilisation principale des options optionnelles implicitement non enveloppées dans Swift est lors de l'initialisation de la classe, comme décrit dans Références non possédées et Propriétés optionnelles implicitement non enveloppées .

Vous pouvez considérer une option implicitement déballée comme donnant la permission de la dérouler automatiquement chaque fois qu’elle est utilisée. Plutôt que de placer un point d'exclamation après le nom de l'option lorsque vous l'utilisez, vous placez un point d'exclamation après le type de l'option lorsque vous le déclarez.

Cela revient à utiliser des cas où la non- nilgravité des propriétés est établie via la convention d'utilisation et ne peut pas être appliquée par le compilateur lors de l'initialisation de la classe. Par exemple, les UIViewControllerpropriétés qui sont initialisées à partir de NIB ou de storyboards, où l'initialisation est divisée en phases distinctes, mais après, viewDidLoad()vous pouvez supposer que les propriétés existent généralement. Sinon, pour satisfaire le compilateur, vous deviez utiliser le déballage forcé , la liaison facultative ou le chaînage facultatif uniquement pour masquer l'objectif principal du code.

La partie ci-dessus du livre Swift se réfère également au chapitre Comptage de référence automatique :

Cependant, il existe un troisième scénario, dans lequel les deux propriétés doivent toujours avoir une valeur, et aucune propriété ne doit jamais l'être nilune fois l'initialisation terminée. Dans ce scénario, il est utile de combiner une propriété non possédée sur une classe avec une propriété facultative implicitement non enveloppée sur l'autre classe.

Cela permet d'accéder aux deux propriétés directement (sans déballage facultatif) une fois l'initialisation terminée, tout en évitant un cycle de référence.

Cela revient aux bizarreries de ne pas être un langage récupéré, donc la rupture des cycles de conservation est sur vous en tant que programmeur et les options implicitement déballées sont un outil pour masquer cette bizarrerie.

Cela couvre le "Quand utiliser les options implicitement déballées dans votre code?" question. En tant que développeur d'applications, vous les rencontrerez principalement dans les signatures de méthodes des bibliothèques écrites en Objective-C, qui n'ont pas la capacité d'exprimer des types facultatifs.

Dans Utilisation de Swift avec Cocoa et Objective-C, section Travailler avec nil :

Étant donné qu'Objective-C ne garantit pas qu'un objet est non nul, Swift rend toutes les classes dans les types d'arguments et les types de retour facultatifs dans les API Objective-C importées. Avant d'utiliser un objet Objective-C, vous devez vérifier qu'il ne manque pas.

Dans certains cas, vous pouvez être absolument certain qu'une méthode ou une propriété Objective-C ne renvoie jamais une nilréférence d'objet. Pour rendre les objets de ce scénario spécial plus pratiques à utiliser, Swift importe les types d'objet en tant qu'options implicitement dépliées . Les types facultatifs implicitement déballés incluent toutes les fonctions de sécurité des types facultatifs. De plus, vous pouvez accéder directement à la valeur sans vérifiernilou le déballer vous-même. Lorsque vous accédez à la valeur dans ce type de type facultatif sans la déballer en toute sécurité en premier, l'option facultative implicitement déballée vérifie si la valeur est manquante. Si la valeur est manquante, une erreur d'exécution se produit. Par conséquent, vous devez toujours vérifier et déballer vous-même une option implicitement déballée, sauf si vous êtes sûr que la valeur ne peut pas être manquante.

... et au-delà dragons

Palimondo
la source
Merci pour cette réponse complète. Pouvez-vous penser à une liste de contrôle rapide pour savoir quand utiliser un optionnel implicitement non enveloppé et quand une variable standard suffirait?
Hairgami_Master
@Hairgami_Master J'ai ajouté ma propre réponse avec une liste et des exemples concrets
drewag
18

Les exemples simples sur une ou plusieurs lignes ne couvrent pas très bien le comportement des options - oui, si vous déclarez une variable et lui fournissez une valeur tout de suite, il n'y a aucun intérêt à une option.

Le meilleur cas que j'ai vu jusqu'à présent est une configuration qui se produit après l'initialisation de l'objet, suivie d'une utilisation "garantie" pour suivre cette configuration, par exemple dans un contrôleur de vue:

class MyViewController: UIViewController {

    var screenSize: CGSize?

    override func viewDidLoad {
        super.viewDidLoad()
        screenSize = view.frame.size
    }

    @IBAction printSize(sender: UIButton) {
        println("Screen size: \(screenSize!)")
    }
}

Nous savons que printSizesera appelé après le chargement de la vue - c'est une méthode d'action connectée à un contrôle à l'intérieur de cette vue, et nous nous sommes assurés de ne pas l'appeler autrement. Nous pouvons donc nous épargner une vérification / liaison facultative avec le !. Swift ne peut pas reconnaître cette garantie (au moins jusqu'à ce qu'Apple résolve le problème d'arrêt), vous dites donc au compilateur qu'elle existe.

Cela rompt cependant la sécurité de type dans une certaine mesure. Partout où vous avez une option implicitement déballée est un endroit où votre application peut planter si votre «garantie» ne tient pas toujours, c'est donc une fonctionnalité à utiliser avec parcimonie. De plus, utiliser !tout le temps donne l'impression que vous criez, et personne n'aime ça.

rickster
la source
Pourquoi ne pas simplement initialiser screenSize à CGSize (hauteur: 0, largeur: 0) et éviter d'avoir à crier la variable à chaque fois que vous y accédez?
Martin Gordon
Une taille n'est peut-être pas le meilleur exemple, car elle CGSizeZeropeut être une bonne valeur sentinelle dans une utilisation réelle. Mais que se passe-t-il si vous avez une taille chargée à partir de nib qui pourrait en fait être nulle? Ensuite, l'utilisation en CGSizeZerotant que sentinelle ne vous aide pas à faire la distinction entre une valeur non définie et définie sur zéro. De plus, cela s'applique également aux autres types chargés à partir de nib (ou n'importe où ailleurs après init): chaînes, références aux sous-vues, etc.
rickster
2
Une partie de l'intérêt de l'option dans les langages fonctionnels est de ne pas avoir de valeurs sentinelles. Soit vous avez une valeur, soit vous n'en avez pas. Vous ne devriez pas avoir un cas où vous avez une valeur qui indique une valeur manquante.
Wayne Tanner
4
Je pense que vous avez mal compris la question du PO. L'OP ne pose pas de questions sur le cas général des options, mais plutôt sur le besoin / l'utilisation d'options optionnelles implicitement non emballées (c'est-à-dire pas let foo? = 42plutôt let foo! = 42). Cela ne règle pas cela. (Cela peut être une réponse pertinente à propos des options, faites attention, mais pas à propos des options implicitement déballées qui sont un animal différent / apparenté.)
JMD
15

Apple donne un excellent exemple dans The Swift Programming Language -> Automatic Reference Counting -> Resolving Strong Reference Cycles between Class Instances -> Unown References and Implicitly Unwrapped Optional Properties

class Country {
    let name: String
    var capitalCity: City! // Apple finally correct this line until 2.0 Prerelease (let -> var)
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

L'initialiseur de Cityest appelé depuis l'initialiseur de Country. Cependant, l'initialiseur de Countryne peut pas passer selfà l' Cityinitialiseur tant qu'une nouvelle Countryinstance n'est pas complètement initialisée, comme décrit dans Initialisation en deux phases .

Pour faire face à cette exigence, vous déclarez la capitalCitypropriété de Countrycomme une propriété facultative implicitement non encapsulée.

fujianjin6471
la source
Le tutoriel mentionné dans cette réponse est ici .
Franklin Yu
3

La justification des options implicites est plus facile à expliquer en examinant d'abord la justification du déballage forcé.

Déballage forcé d'une option (implicite ou non), en utilisant le! , signifie que vous êtes certain que votre code n'a pas de bogues et que l'option a déjà une valeur là où elle est déballée. Sans le ! , vous affirmeriez probablement avec une liaison facultative:

 if let value = optionalWhichTotallyHasAValue {
     println("\(value)")
 } else {
     assert(false)
 }

ce qui n'est pas aussi agréable que

println("\(value!)")

Maintenant, les options implicites vous permettent d'exprimer une option que vous attendez d'avoir toujours une valeur une fois dépliée, dans tous les flux possibles. Cela va donc encore plus loin en vous aidant - en relâchant l'exigence d'écrire le! pour déballer à chaque fois, et vous assurer que le runtime continuera à générer des erreurs au cas où vos hypothèses sur le flux seraient erronées.

Danra
la source
1
@newacct: non nul dans tous les flux possibles à travers votre code n'est pas identique à non nul depuis l'initialisation parent (classe / structure) via la désallocation. Interface Builder est l'exemple classique (mais il existe de nombreux autres modèles d'initialisation retardée): si une classe n'est utilisée que depuis une pointe, les variables de sortie ne seront pas définies init(que vous pourriez même ne pas implémenter), mais elles ' re garanti pour être réglé après awakeFromNib/ viewDidLoad.
rickster
3

Si vous savez avec certitude, une valeur renvoyée par un optionnel au lieu de nil, Optionally Unwrapped Optionals utilise pour attraper directement ces valeurs des optionnels et non optionnels ne peut pas.

//Optional string with a value
let optionalString: String? = "This is an optional String"

//Declaration of an Implicitly Unwrapped Optional String
let implicitlyUnwrappedOptionalString: String!

//Declaration of a non Optional String
let nonOptionalString: String

//Here you can catch the value of an optional
implicitlyUnwrappedOptionalString = optionalString

//Here you can't catch the value of an optional and this will cause an error
nonOptionalString = optionalString

Voilà donc la différence entre l'utilisation de

let someString : String! et let someString : String

enadun
la source
1
Cela ne répond pas à la question du PO. OP sait ce qu'est une option implicitement non emballée.
Franklin Yu
0

Je pense que Optionalc'est un mauvais nom pour cette construction qui déroute beaucoup de débutants.

D'autres langages (Kotlin et C # par exemple) utilisent le terme Nullable, et cela le rend beaucoup plus facile à comprendre.

Nullablesignifie que vous pouvez affecter une valeur nulle à une variable de ce type. Donc, si c'est le cas Nullable<SomeClassType>, vous pouvez lui attribuer des valeurs nulles, si c'est juste SomeClassType, vous ne pouvez pas. Voilà comment Swift fonctionne.

Pourquoi les utiliser? Eh bien, parfois, vous devez avoir des valeurs nulles, c'est pourquoi. Par exemple, lorsque vous savez que vous voulez avoir un champ dans une classe, mais que vous ne pouvez l'attribuer à rien lorsque vous créez une instance de cette classe, mais vous le ferez plus tard. Je ne donnerai pas d'exemples, car les gens les ont déjà fournis ici. J'écris juste ceci pour donner mes 2 cents.

Btw, je vous suggère de voir comment cela fonctionne dans d'autres langages, comme Kotlin et C #.

Voici un lien expliquant cette fonctionnalité dans Kotlin: https://kotlinlang.org/docs/reference/null-safety.html

D'autres langages, comme Java et Scala, ont des Optionals, mais ils fonctionnent différemment de Optionals dans Swift, car les types de Java et Scala sont tous annulables par défaut.

Dans l'ensemble, je pense que cette fonctionnalité aurait dû être nommée Nullabledans Swift, et non Optional...

Hé toi
la source
0

Implicitly Unwrapped Optionalest un sucre syntaxique car Optionalcela n'oblige pas un programmeur à déballer une variable. Il peut être utilisé pour une variable qui ne peut pas être initialisée pendant two-phase initialization processet implique non nul. Cette variable se comporte comme non nulle mais est en fait une variable facultative . Un bon exemple est - les points de vente d'Interface Builder

Optional sont généralement préférables

var nonNil: String = ""
var optional: String?
var implicitlyUnwrappedOptional: String!

func foo() {
    //get a value
    nonNil.count
    optional?.count

    //Danderour - makes a force unwrapping which can throw a runtime error
    implicitlyUnwrappedOptional.count

    //assign to nil
//        nonNil = nil //Compile error - 'nil' cannot be assigned to type 'String'
    optional = nil
    implicitlyUnwrappedOptional = nil
}
yoAlex5
la source