Opérateur == personnalisé, faut-il le conserver ?

Lorsque vous faites cela dans Unity:
if (myGameObject == null) {}
Unity fait quelque chose de spécial avec l'opérateur ==. Au lieu de ce à quoi la plupart des gens s’attendraient, nous avons une implémentation spéciale de l’opérateur ==.
Cela sert à deux fins :
1) Lorsqu'un MonoBehaviour possède des champs, dans l'éditeur uniquement[1], nous ne définissons pas ces champs à "vrai null", mais à un objet "faux null". Notre opérateur == personnalisé est capable de vérifier si quelque chose est l'un de ces faux objets null et se comporte en conséquence. Bien qu'il s'agisse d'une configuration exotique, elle nous permet de stocker des informations dans le faux objet null qui vous donne plus d'informations contextuelles lorsque vous invoquez une méthode dessus, ou lorsque vous demandez à l'objet une propriété. Sans cette astuce, vous n'obtiendriez qu'une NullReferenceException, une trace de pile, mais vous n'auriez aucune idée de quel GameObject avait le MonoBehaviour qui avait le champ qui était null. Avec cette astuce, nous pouvons mettre en évidence le GameObject dans l'inspecteur, et pouvons également vous donner plus de direction : « il semble que vous accédiez à un champ non initialisé dans ce MonoBehaviour ici, utilisez l'inspecteur pour faire pointer le champ vers quelque chose ».
Le deuxième objectif est un peu plus compliqué.
2) Lorsque vous obtenez un objet ac# de type "GameObject"[2], il ne contient presque rien. c'est parce Unity est un moteur C/C++. Toutes les informations réelles sur ce GameObject (son nom, la liste des composants qu'il possède, ses HideFlags, etc.) se trouvent du côté c++. La seule chose que possède l’objet c# est un pointeur vers l’objet natif. Nous appelons ces objets C# des « objets wrapper ». La durée de vie de ces objets c++ comme GameObject et tout ce qui dérive de UnityEngine.Object est explicitement gérée. Ces objets sont détruits lorsque vous chargez une nouvelle scène. Ou lorsque vous appelez Object.Destroy(myObject); sur eux. La durée de vie des objets C# est gérée de manière C#, avec un garbage collector. Cela signifie qu'il est possible d'avoir un objet wrapper ac# qui existe toujours, qui enveloppe un objet c++ qui a déjà été détruit. Si vous comparez cet objet à null, notre opérateur == personnalisé renverra « true » dans ce cas, même si la variable c# réelle n'est en réalité pas vraiment null.
Bien que ces deux cas d’utilisation soient assez raisonnables, la vérification null personnalisée comporte également de nombreux inconvénients.
- C'est contre-intuitif.
- La comparaison de deux UnityEngine.Objects entre eux ou avec null est plus lente que prévu.
- L'opérateur personnalisé == n'est pas thread-safe, vous ne pouvez donc pas comparer les objets hors du thread principal. (celui-ci, nous pourrions le réparer).
- Il se comporte de manière incohérente avec l'opérateur ??, qui effectue également une vérification null , mais celui-ci effectue une vérification null pure en C# et ne peut pas être contourné pour appeler notre vérification null personnalisée.
En passant en revue tous ces avantages et inconvénients, si nous avions construit notre API à partir de zéro, nous aurions choisi de ne pas effectuer de vérification null personnalisée, mais plutôt d'avoir une propriété myObject.destroyed que vous pouvez utiliser pour vérifier si l'objet est mort ou non, et simplement vivre avec le fait que nous ne pouvons plus donner de meilleurs messages d'erreur au cas où vous invoqueriez une fonction sur un champ qui est null.
Ce à quoi nous réfléchissons, c'est si nous devrions ou non changer cela. C'est une étape dans notre quête sans fin pour trouver le bon équilibre entre « réparer et nettoyer les vieilles choses » et « ne pas casser les vieux projets ». Dans ce cas, nous nous demandons ce que vous en pensez. Pour Unity5, nous avons travaillé sur la capacité d' Unity à mettre à jour automatiquement vos scripts (plus d'informations à ce sujet dans un prochain article de blog). Malheureusement, nous ne serions pas en mesure de mettre à niveau automatiquement vos scripts dans ce cas (car nous ne pouvons pas faire la distinction entre « il s'agit d'un ancien script qui souhaite en fait l'ancien comportement » et « il s'agit d'un nouveau script qui souhaite en fait le nouveau comportement »).
Nous penchons vers « supprimer l'opérateur == personnalisé », mais nous hésitons, car cela changerait la signification de toutes les vérifications null que vos projets effectuent actuellement. Et pour les cas où l'objet n'est pas « vraiment null» mais un objet détruit, un nullcheck renvoyait true, et si nous changeons cela, il renverra false. Si vous souhaitez vérifier si votre variable pointe vers un objet détruit, vous devez modifier le code pour vérifier « if (myObject.destroyed) {} » à la place. Nous sommes un peu nerveux à ce sujet, comme si vous n'aviez pas lu cet article de blog, et très probablement si vous l'avez fait, il est très facile de ne pas se rendre compte de ce changement de comportement, d'autant plus que la plupart des gens ne se rendent pas compte que cette vérification null personnalisée existe du tout.[3]
Si nous le changeons, nous devrions le faire pour Unity5, car le seuil de difficulté de mise à niveau que nous sommes prêts à faire supporter aux utilisateurs est encore plus bas pour les versions non majeures.
Que préféreriez-vous que nous fassions ? Vous offrir une expérience plus propre, au prix de devoir modifier les contrôles null dans votre projet, ou garder les choses telles qu'elles sont ?
Au revoir, Lucas (@lucasmeijer)
[1] Nous le faisons uniquement dans l’éditeur. C'est pourquoi lorsque vous appelez GetComponent() pour interroger un composant qui n'existe pas, vous voyez une allocation de mémoire C# se produire, car nous générons cette chaîne d'avertissement personnalisée à l'intérieur du faux objet null nouvellement alloué. Cette allocation de mémoire ne se produit pas dans les jeux construits. C'est un très bon exemple de la raison pour laquelle, si vous profilez votre jeu, vous devez toujours profiler le joueur autonome ou le joueur mobile réel, et non profiler l'éditeur, car nous effectuons de nombreux contrôles de sécurité/sûreté/utilisation supplémentaires dans l'éditeur pour vous faciliter la vie, au détriment de certaines performances. Lors du profilage des performances et des allocations de mémoire, ne profilez jamais l'éditeur, profilez toujours le jeu construit.
[2] Ceci est vrai non seulement pour GameObject, mais pour tout ce qui dérive de UnityEngine.Object
[3] Histoire amusante : Je suis tombé sur ce problème lors de l'optimisation des performances de GetComponent<T>() et lors de l'implémentation d'une mise en cache pour le composant de transformation, je ne voyais aucun avantage en termes de performances. Ensuite @jonasechterhoffa examiné le problème et est arrivé à la même conclusion. Le code de mise en cache ressemble à ceci :
private Transform m_CachedTransform
public Transform transform
{
get
{
if (m_CachedTransform == null)
m_CachedTransform = InternalGetTransform();
return m_CachedTransform;
}
}
Il s'avère que deux de nos propres ingénieurs n'ont pas compris que la vérification null était plus coûteuse que prévu et était la cause du fait que nous ne voyions aucun avantage en termes de vitesse grâce à la mise en cache. Cela a conduit à la question « Et bien, si même nous l'avons manqué, combien de nos utilisateurs le manqueront-ils ? », ce qui donne lieu à cet article de blog :)