2 minutes
Unity 3D : C# closures
Quand la programmation fonctionnelle te fait froncer les sourcils.
C’est quoi une closure ?
Une closure c’est tout simplement une fonction anonyme qui capture une variable d’un autre scope.
Prenons un exemple d’un script qui ajoute un callback sur un bouton :
// MyBehaviour.cs
Button _button;
int _number = 0;
void Start()
{
_button = GameObject.Find("Button");
_button.onClick.AddListener(() => {
_number++;
Debug.Log(_number);
});
}
// On click:
// 1
// 2
// 3...
Ici, l’expression lambda dans AddListener
reçoit une référence à la variable _number
, faisant d’elle une closure.
Le problème
Si on clique sur le bouton 3 fois, _number
sera égal à 3. Jusque la, tout va bien. Maintenant si on détruit MyBehaviour.cs
, que l’on crée une nouvelle instance et que l’on clique sur le bouton, _number
sera égal à 4, alors que la valeur attendue est 1 !
Mais pourquoi ?
Si l’on détruit l’instance de MyBehaviour
mais pas le bouton _button
, la variable _number
capturée n’est pas détruite et existe toujours dans le delegate
qui est passé en paramètre de AddListener()
.
Par la suite, chaque incrément se fera sur cette variable interne et non sur la variable membre de la nouvelle instance de MyBehaviour
. On dit alors que la variable est “hoisted”, sa durée de vie étant étendue tant que le delegate
est en vie.
Solution
La solution est plutôt simple, il faut détruire toutes les références restantes. Ici il suffit de détruire les delegates qui sont dans le bouton :
// MyBehaviour.cs
Button _button;
int _number = 0;
void Start()
{
_button = GameObject.Find("Button");
+ _button.onClick.RemoveAllListeners();
_button.onClick.AddListener(() => {
_number++;
Debug.Log(_number);
});
}