Pourquoi les collections BCL utilisent-elles des énumérateurs de structure, pas des classes?

Nous soaps tous que les structures mutables sont mauvaises en général. Je suis également sûr que, parce que IEnumerable.GetEnumerator() renvoie le type IEnumerator , les structures sont immédiatement encadrées dans un type de référence, ce qui coûte plus cher que si elles étaient simplement des types de référence.

Alors, pourquoi, dans les collections génériques BCL, tous les énumérateurs sont-ils mutables? Il devait sûrement y avoir une bonne raison. La seule chose qui me vient à l’esprit est que les structures peuvent être facilement copiées, préservant ainsi l’état de l’énumérateur à un point arbitraire. Mais append une méthode Copy() à l’interface IEnumerator aurait été moins gênant, donc je ne vois pas cela comme une justification logique en soi.

Même si je ne suis pas d’accord avec une décision de conception, j’aimerais pouvoir comprendre le raisonnement qui la sous-tend.

    En effet, c’est pour des raisons de performance. L’équipe de la BCL a fait beaucoup de recherches sur ce point avant de décider de suivre ce que vous appelez à juste titre une pratique suspecte et dangereuse: l’utilisation d’un type de valeur mutable.

    Vous demandez pourquoi cela ne cause pas la boxe. C’est parce que le compilateur C # ne génère pas de code pour encadrer IEnumerable ou IEnumerator dans une boucle foreach s’il peut l’éviter!

    Quand nous voyons

     foreach(X x in c) 

    La première chose à faire est de vérifier si c a une méthode appelée GetEnumerator. Si c’est le cas, alors nous vérifions si le type renvoyé a la méthode MoveNext et la propriété current. Si c’est le cas, la boucle foreach est générée entièrement à l’aide d’appels directs à ces méthodes et propriétés. Ce n’est que si “le modèle” ne peut pas être associé que nous nous tournons vers la recherche des interfaces.

    Cela a deux effets souhaitables.

    Premièrement, si la collection est, par exemple, une collection de ints, mais a été écrite avant l’invention des types génériques, alors la pénalité imposée par la mise en boîte de la valeur de Current n’est pas atteinte. Si Current est une propriété qui retourne un int, nous l’utilisons simplement.

    Deuxièmement, si l’énumérateur est un type de valeur, alors il ne contient pas l’énumérateur sur IEnumerator.

    Comme je l’ai dit, l’équipe de la BCL a fait beaucoup de recherches à ce sujet et a découvert que la grande majorité du temps, l’allocation et la désallocation de l’enquêteur étaient suffisantes pour en faire un type de valeur, même si cela pouvait causer des bugs fous.

    Par exemple, considérez ceci:

     struct MyHandle : IDisposable { ... } ... using (MyHandle h = whatever) { h = somethingElse; } 

    Vous vous attendriez à juste titre à ce que la tentative de mutation m ait échoué, et en effet elle le fait. Le compilateur détecte que vous essayez de modifier la valeur de quelque chose qui a une disposition en attente et que cela risque de ne pas éliminer l’object qui doit être éliminé.

    Maintenant, supposons que vous ayez:

     struct MyHandle : IDisposable { ... } ... using (MyHandle h = whatever) { h.Mutate(); } 

    Que se passe t-il ici? Vous pourriez raisonnablement vous attendre à ce que le compilateur fasse ce qu’il fait si h était un champ en lecture seule: faites une copie et modifiez la copie pour vous assurer que la méthode ne jette pas d’objects dans la valeur à éliminer.

    Cependant, cela entre en conflit avec notre intuition sur ce qui devrait se passer ici:

     using (Enumerator enumtor = whatever) { ... enumtor.MoveNext(); ... } 

    Nous nous attendons à ce que faire un MoveNext dans un bloc using déplace l’énumérateur vers le suivant, qu’il s’agisse d’un type struct ou ref.

    Malheureusement, le compilateur C # a aujourd’hui un bogue. Si vous êtes dans cette situation, nous choisissons la stratégie à suivre de manière incohérente. Le comportement actuel est:

    • si la variable de type valeur en cours de mutation via une méthode est un local normal, elle est mutée normalement

    • mais s’il s’agit d’un local hissé (car il s’agit d’une variable fermée d’une fonction anonyme ou d’un bloc iterator), le local est en réalité généré en tant que champ en lecture seule et l’engin qui garantit que les mutations se produisent sur une copie plus de.

    Malheureusement, la spécification fournit peu de conseils à ce sujet. Il est clair que quelque chose est cassé parce que nous le faisons de manière incohérente, mais que la bonne chose à faire n’est pas du tout claire.

    Les méthodes Struct sont intégrées lorsque le type de structure est connu au moment de la compilation et que la méthode d’appel via l’interface est lente, donc la réponse est: pour des raisons de performances.