39 votes

Est-il possible de forcer le déballage de variables auxquelles on a accédé de manière facultative dans la même ligne de code ?

someFunction(completion: { [weak self] in
    self?.variable = self!.otherVariable
})

Est-ce que c'est toujours sûr ? J'accède à l'option self au début de l'instruction, et personnellement je suppose que la deuxième partie de cette instruction ne sera jamais exécutée si self est nil . Est-ce que c'est vrai ? Si self en effet, c 'est nil la deuxième partie n'arrivera jamais ? Et il n'arrivera jamais que self pourrait être "tué" par cette seule ligne de code ?

0 votes

13 votes

C'est une question intéressante. Up-voté. C'est assez facile de tester le fonctionnement aujourd'hui, mais est-ce que cela garantit toujours Le travail est la question. Ça a l'air suspect et je ne l'utiliserais pas même si je savais que ça marche. C'est assez facile à utiliser if let ou guard pour que vos intentions soient claires.

0 votes

Une question encore plus pertinente est la suivante : pourquoi souhaitez-vous utiliser les optionnels de cette manière ?

26voto

Martin R Points 105727

Chaînage optionnel de "The Swift Programming Language" donne l'exemple suivant :

 let john = Person()
 // ...
 let someAddress = Address()
 // ...
 john.residence?.address = someAddress

suivi de (c'est nous qui soulignons) :

Dans cet exemple, la tentative de définir la propriété adresse de john.residence échouera, car john.residence est actuellement nul.

L'affectation fait partie du chaînage facultatif, ce qui signifie que aucun des codes situés à droite de l'opérateur = n'est évalué.

Appliqué à votre cas : Sur

self?.variable = self!.otherVariable

le côté droit est pas évalué si self est nil . Par conséquent, la réponse à votre question

Si le soi est effectivement nul, la deuxième partie ne se produira jamais ?

est "oui". En ce qui concerne la deuxième question

Et il n'arrivera jamais qu'un individu puisse être "tué" pendant cette seule ligne de code ?

Mon original hypothèse était qu'une fois self a été déterminé comme étant != nil , une référence forte à self! est maintenue pendant toute l'évaluation de l'instruction de sorte que cela ne puisse pas se produire. Cependant (comme l'a souligné @Hamish), cela n'est pas garanti. L'ingénieur d'Apple Joe Groff écrit à Confirmation de l'ordre des opérations dans le forum Swift :

Ce n'est pas garanti. Les libérations peuvent être optimisées pour se produire plus tôt que cela, à n'importe quel moment après la dernière utilisation formelle de la référence forte. Puisque la référence forte est chargée afin d'évaluer le côté gauche weakProperty?.variable n'est pas utilisé par la suite, rien ne le maintient en vie, il peut donc être immédiatement libéré.
S'il y a des effets secondaires dans le getter pour la variable qui font que l'objet référencé par weakProperty à désallouer, en éliminant la référence faible, alors cela ferait échouer le force-unwrap du côté droit. Vous devez utiliser if let pour tester la référence faible, et référencer la référence forte liée par la balise if let

2 votes

Je pense que c'est la bonne réponse. Si le côté gauche est nil de toute opération d'affectation, le côté droit ne sera pas évalué. Considérez quelque chose comme ceci : instance?.val = ([] as [Int])[0] (en supposant que val est un Int ). Le côté droit de cette expression provoquera un crash, mais ne sera pas évalué si instance est nil .

0 votes

@JAL vrai mais imaginez si le côté gauche ne l'était pas. nil et ensuite, parce que c'est une opération asynchrone, avant de lire le bon opérande, self est devenu nil . Cela pourrait provoquer un crash à partir de l'opérande de droite ?

0 votes

@Honey pas si une référence forte à self a été capturé comme l'indique le commentaire du code dans la réponse de dfri. Je pense que la durée de vie de l'expression signifie la ligne entière (les deux côtés de l'affectation).

16voto

dfri Points 11222

Non, ce n'est pas sûr.

Comme le souligne @Hamish dans un commentaire ci-dessous, l'ingénieur compilateur Swift Joe Groff décrit qu'il n'y a aucune garantie qu'une référence forte soit maintenue pendant la durée de l'évaluation de l'ERS [ c'est moi qui souligne ]

Confirmation de l'ordre des opérations

Rod_Brown :

Bonjour,

Je m'interroge sur la sécurité d'un type d'accès sur une variable faible :

class MyClass {

    weak var weakProperty: MyWeakObject?

    func perform() {
        // Case 1
        weakProperty?.variable = weakProperty!.otherVariable

        // Case 2
        weakProperty?.performMethod(weakProperty!)
    }
}

Avec les deux cas ci-dessus, est-il garanti par Swift que l'option weakProperty peut être déballée de force à ces endroits ?

Je suis curieux de connaître les garanties que Swift donne sur l'accès lors de chaînage optionnel. Par exemple, est-ce que les weakProperty! Les accesseurs sont garantis ne se déclenchent que si le chaînage optionnel détermine d'abord que la valeur est déjà non nil ?

De plus, la conservation de l'objet faible est-elle garantie pour la durée de cette évaluation, ou la variable faible peut-elle potentiellement être être capable de désallouer entre l'accès optionnel et la méthode appelée ? appelée ?

Joe_Groff :

Ce n'est pas garanti. Les libérations peuvent être optimisées pour se produire plus tôt plus tôt que cela, à tout moment après la dernière utilisation formelle de la référence forte. forte. Puisque la référence forte chargée afin d'évaluer le côté gauche weakProperty?.variable n'est pas utilisé par la suite, il n'y a rien qui le maintient en vie, donc il pourrait être immédiatement libéré. S'il y a des effets secondaires dans le getter de la variable qui font que l'objet référencé par weakProperty à désallouer, nil -en éliminant la référence faible, alors cela causerait le force-unwrap sur le côté droit à échouer . Vous devez utiliser if let pour tester la référence faible, et référencer la référence forte liée par le if let :

if let property = weakProperty {
  property.variable = property.otherVariable
  property.performMethod(property)
}

Cela devrait être plus sûr et aussi plus efficace, puisque la référence faible est chargée et testée une fois au lieu de quatre.


Étant donné la réponse citée par Joe Groff ci-dessus, ma réponse précédente est sans objet, mais je la laisserai ici comme un voyage peut-être intéressant (bien que raté) dans les profondeurs du compilateur Swift.


Réponse historique aboutissant à un argument final erroné, mais à travers un parcours intéressant, néanmoins.

Je vais baser cette réponse sur mon commentaire à la réponse supprimée de @appzYourLife :

C'est une pure spéculation, mais en considérant le lien assez étroit entre de nombreux développeurs expérimentés du noyau de Swift et la librairie Boost, je suppose que weak est verrouillée en une référence forte pour la durée de vie de l'expression, si celle-ci assigne/mute quelque chose dans self tout comme la méthode explicitement utilisée std::weak_ptr::lock() de son homologue C++.

Examinons votre exemple, dans lequel self a été capturé par un weak et n'est pas nil lors de l'accès au côté gauche de l'expression d'affectation

self?.variable = self!.otherVariable
/* ^             ^^^^^-- what about this then?
   |
    \-- we'll assume this is a success */

Nous pouvons examiner le traitement sous-jacent de weak (Swift) dans le moteur d'exécution Swift, swift/include/swift/Runtime/HeapObject.h spécifiquement :

/// Load a value from a weak reference.  If the current value is a
/// non-null object that has begun deallocation, returns null;
/// otherwise, retains the object before returning.
///
/// \param ref - never null
/// \return can be null
SWIFT_RUNTIME_EXPORT
HeapObject *swift_weakLoadStrong(WeakReference *ref);

La clé ici est le commentaire

Si la valeur actuelle est un objet non nul qui a commencé à être désalloué, renvoie null ; sinon, conserve l'objet avant de retourner .

Comme ceci est basé sur un commentaire de code d'exécution backend, c'est encore quelque peu spéculatif, mais je dirais que ce qui précède implique que lors d'une tentative d'accès à la valeur pointée par un weak cette référence sera en effet conservée comme une référence forte pendant toute la durée de l'appel ( "... jusqu'au retour" ).


Pour essayer de racheter le "quelque peu spéculatif" de la partie ci-dessus, nous pouvons continuer à creuser la manière dont Swift gère l'accès à une valeur via une balise weak référence. Sur le site Commentaire de @idmean:ci-dessous (en étudiant le code SIL généré pour un exemple comme celui de l'OP:s) nous savons que le swift_weakLoadStrong(...) est appelée.

Nous allons donc commencer par examiner l'implémentation de la fonction swift_weakLoadStrong(...) fonction dans swift/stdlib/public/runtime/HeapObject.cpp et voir où nous en serons à partir de là :

HeapObject *swift::swift_weakLoadStrong(WeakReference *ref) {
  return ref->nativeLoadStrong();
}

Nous trouvons que la mise en œuvre de la nativeLoadStrong() méthode de WeakReference de swift/include/swift/Runtime/HeapObject.h

HeapObject *nativeLoadStrong() {
  auto bits = nativeValue.load(std::memory_order_relaxed);
  return nativeLoadStrongFromBits(bits);
}

De le même fichier la mise en œuvre de nativeLoadStrongFromBits(...) :

HeapObject *nativeLoadStrongFromBits(WeakReferenceBits bits) {
  auto side = bits.getNativeOrNull();
  return side ? side->tryRetain() : nullptr;
}

En continuant la chaîne d'appel, tryRetain() est une méthode de HeapObjectSideTableEntry (qui est essentiel pour la machine à états du cycle de vie des objets ), et nous trouvons son implémentation dans swift/stdlib/public/SwiftShims/RefCount.h

HeapObject* tryRetain() {
  if (refCounts.tryIncrement())
    return object.load(std::memory_order_relaxed);
  else
    return nullptr;
}

La mise en œuvre de la tryIncrement() de la méthode RefCounts (ici invoqué via une instance d'un type de typedef sa spécialisation ) peut être trouvé dans le même fichier que ci-dessus :

// Increment the reference count, unless the object is deiniting.
bool tryIncrement() {
  ...
}

Je pense que le commentaire ici suffit pour que nous utilisions cette méthode comme point final : si l'objet ne se désinite pas (ce que nous avons supposé plus haut qu'il ne le fait pas, puisque la méthode lhs de l'affectation dans l'exemple de l'OP est supposée être réussie), le nombre de références (fortes) sur l'objet sera augmenté, et une HeapObject (soutenu par un fort incrément du compte de référence) sera transmis à l'opérateur d'affectation. Nous n'avons pas besoin d'étudier comment la décrémentation correspondante du nombre de références est finalement effectuée à la fin de l'affectation, mais nous savons maintenant, au-delà de toute spéculation, que l'objet associé à l'objet weak sera conservée comme référence forte pendant toute la durée de vie de l'affectation, à condition qu'elle n'ait pas été libérée/désallouée au moment de l'accès à sa partie gauche (auquel cas sa partie droite ne sera jamais traitée, comme cela a été expliqué dans la section Réponse de @MartinR:s ).

1 votes

Excellente réponse ! Je viens de vérifier et l'assemblage semble effectivement faire appel à cette fonction et fait également un appel à _swift_rt_swift_release qui semble être la contrepartie de cet appel. (Bien que je trouve vraiment l'assemblage Swift difficile à suivre).

0 votes

" pour la durée de vie de l'expression " vous voulez dire self?.variable = self!.otherVariable La durée de vie de celle-ci serait du début de la lecture de l'opérande gauche à la fin de la lecture de l'opérande droit ?

0 votes

Je n'ai pas suivi la chaîne d'appels ni toutes les conditions requises avant la libération et la désallocation finale de l'objet. Mais le terrier du lapin est un peu trop profond pour que je m'y attarde en ce moment... C'est bien d'avoir votre vérification SIL générée pour le swift_weakLoadStrong appelez, merci !

3voto

matt Points 60113

Est-ce que c'est toujours sûr

Non . Vous ne faites pas la "danse du faible et du fort". Faites-le ! Chaque fois que vous utilisez weak self vous devez déballer l'option en toute sécurité, puis vous référer uniquement au résultat de ce déballage, comme ceci :

someFunction(completion: { [weak self] in
    if let sself = self { // safe unwrap
        // now refer only to `sself` here
        sself.variable = sself.otherVariable
        // ... and so on ...
    }
})

0 votes

@Sti Cela ne répond pas directement à votre question, qui est plus théorique que pratique. Mais il vous donne des indications sur ce qu'il faut faire en pratique. Le déballage forcé, même dans des situations où vous savez que tout ira bien, n'est toujours pas une bonne façon de procéder. Plutôt que d'être une mauvaise réponse, elle vous fournit la bonne façon de faire les choses.

3voto

Gleb Lukianets Points 29

La documentation indique clairement États que, si le côté gauche de l'affectation est déterminé comme étant nul, le côté droit ne sera pas évalué. Cependant, dans l'exemple donné self est faible référence et peut être libérée (et annulée) juste après que la vérification optionnelle soit passée, mais juste avant que la force-unwrap ne se produise, rendant l'expression entière non sûre avec nil.

-4voto

Honey Points 9108

AVANT LA CORRECTION :

Je pense que d'autres ont répondu aux détails de votre question bien mieux que je ne pourrais le faire.

Mais à part l'apprentissage. Si vous voulez vraiment que votre code fonctionne de manière fiable, il est préférable de procéder ainsi :

someFunction(completion: { [weak self] in
    guard let _ = self else{
        print("self was nil. End of discussion")
        return
    }
    print("we now have safely 'captured' a self, no need to worry about this issue")
    self?.variable = self!.otherVariable
    self!.someOthervariable = self!.otherVariable
}

APRÈS CORRECTION.

Grâce à l'explication de MartinR ci-dessous, j'ai beaucoup appris.

La lecture de ce grand post sur la capture de la fermeture . Je pensais servilement que chaque fois que vous voyez quelque chose entre parenthèses [] ça veut dire qu'il est capturé et que sa valeur ne change pas. Mais la seule chose que nous faisons dans les parenthèses est que nous sommes weak -en l'unifiant et en nous faisant comprendre qu'elle a de la valeur. pourrait devenir nil . Si nous avions fait quelque chose comme [x = self] nous aurions réussi à le capturer, mais nous aurions encore le problème de détenir un pointeur fort sur self et créer un cycle de mémoire. (C'est intéressant dans le sens où la ligne est très mince entre la création d'un cycle de mémoire et la création d'un crash dû à la désallocation de la valeur parce que vous l'avez affaiblie).

Donc, pour conclure :

  1. [capturedSelf = self]

    crée un cycle de mémoire. Ce n'est pas bon !

  2. [weak self] in guard let _ = self else{ return }

    (peut conduire à un crash si vous forcez self déballer après) le site guard let est tout à fait inutile. Parce que la ligne suivante, toujours self peut devenir nil . Pas bon !

  3. [weak self] self?.method1()

    (peut conduire à un crash si vous forcez self déballer après. J'irais jusqu'au bout si self est non nil . Échouerait en toute sécurité si self est nil .) C'est très probablement ce que vous voulez. C'est Bon !

  4. [weak self] in guard let strongSelf = self else{ return }

    Échouera en toute sécurité si self a été désalloué ou procéder si non nil . Mais cela va à l'encontre du but recherché, car vous ne devriez pas avoir besoin de communiquer avec self quand il a supprimé sa propre référence. Je n'arrive pas à trouver un bon cas d'utilisation pour cela. C'est probablement inutile !

0 votes

Je ne vois pas en quoi cela est différent (ou meilleur) que Réponse de Matt .

0 votes

@JAL hah. Je n'avais pas vu. Je suppose que la mienne est plus descriptive et utilise la garde, ce qui est probablement mieux, mais vous avez raison, nos réponses sont à 90% les mêmes.

3 votes

C'est même pire. La référence au soi n'est pas prise ici de manière explicite.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X