En effet, c'est pour des raisons de performances. La BCL équipe fait beaucoup de recherche sur ce point avant de décider d'aller avec ce que vous appelez à juste titre comme un suspect et dangereux de la pratique: l'utilisation d'une mutable type de valeur.
Vous vous demandez pourquoi ce n'est pas une cause de la boxe. C'est parce que le compilateur C# ne génère pas de code de zone de trucs à IEnumerable ou IEnumerator dans une boucle foreach, si on peut l'éviter!
Lorsque nous voir
foreach(X x in c)
la première chose à faire est de vérifier pour voir si c est une méthode GetEnumerator. Si c'est le cas, nous allons vérifier si le type il retourne a la méthode MoveNext et de la propriété actuelle. Si c'est le cas, la boucle foreach est produite entièrement à l'aide de diriger les appels à ces méthodes et propriétés. Seulement si "le modèle" ne peut être égalée ne nous retombons à la recherche pour les interfaces.
Cela a deux effets désirables.
Tout d'abord, si la collection est, disons, une collection d'entiers, mais a été écrit avant que les types génériques ont été inventés, alors il ne prend pas la peine de boxe de boxe de la valeur du Courant à l'objet, et puis unboxing à int. Si le Courant est une propriété qui retourne un int, nous viens de l'utiliser.
Deuxièmement, si l'agent recenseur est un type valeur, alors il n'a pas de boîte de l'agent recenseur à IEnumerator.
Comme je l'ai dit, la BCL équipe a fait beaucoup de recherches sur cette question et a découvert que la grande majorité du temps, la peine de l'allocation et la désallocation de l'agent recenseur était assez grande qu'il valait la peine de faire un type de valeur, même si cela peut causer quelques fous de bugs.
Par exemple, considérez ceci:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h = somethingElse;
}
Vous serait tout à fait en droit d'attendre de la tentative de muter h à l'échec, et en effet il ne. Le compilateur détecte que vous essayez de modifier la valeur de quelque chose qui a une cession, et que cela pourrait causer à l'objet qui doit être disposé à fait ne pas être éliminés.
Maintenant, supposons que vous avez eu:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h.Mutate();
}
Ce qui se passe ici? Vous pourriez raisonnablement s'attendre à ce que le compilateur pourrait faire ce qu'il fait si h était un champ en lecture seule: faire une copie, et la mutation de l'exemplaire afin de s'assurer que la méthode ne pas jeter des choses dans la valeur qui doit être éliminé.
Cependant, qui est en conflit avec notre intuition sur ce qui devrait se passer ici:
using (Enumerator enumtor = whatever)
{
...
enumtor.MoveNext();
...
}
Nous nous attendons à ce que fait un MoveNext à l'intérieur d'un bloc using va déplacer l'agent recenseur à la suivante, qu'il s'agisse d'une structure ou d'un type de référence.
Malheureusement, le compilateur C# dispose aujourd'hui d'un bug. Si vous êtes dans cette situation, nous choisissons la stratégie à suivre de façon incohérente. Le comportement d'aujourd'hui est:
si le type de valeur de la variable d'être muté par l'intermédiaire d'une méthode est un local normal puis il est muté normalement
mais si c'est un hissé local (parce que c'est un fermé-sur une variable d'une fonction anonyme ou dans un itérateur bloc), puis le local est en fait générée comme un champ en lecture seule, et l'équipement qui assure que les mutations se produisent sur une copie qui prend le dessus.
Malheureusement, la spécification fournit peu d'indications sur cette question. Clairement, quelque chose est brisé parce que nous le faisons de façon incohérente, mais ce que le droit chose à faire n'est pas clair du tout.