実際、これはパフォーマンス上の理由によるものです。BCL チームは、この点について多くの調査を行った後、不審で危険な慣行として正しく指摘されていること、つまり可変値型の使用を採用することを決定しました。
あなたは、なぜこれがボクシングを引き起こさないのかと尋ねます。これは、C# コンパイラが foreach ループで IEnumerable または IEnumerator にボックス化するコードを生成しないためです (回避できる場合)。
私たちが見るとき
foreach(X x in c)
最初に、c に GetEnumerator というメソッドがあるかどうかを確認します。存在する場合は、返される型にメソッド MoveNext とプロパティ current があるかどうかを確認します。存在する場合、foreach ループは、それらのメソッドとプロパティへの直接呼び出しを使用して完全に生成されます。「パターン」が一致しない場合にのみ、インターフェースの検索に戻ります。
これには 2 つの望ましい効果があります。
まず、コレクションがたとえば int のコレクションであるが、ジェネリック型が発明される前に作成された場合、Current の値をオブジェクトにボックス化し、それを int にボックス化解除するというボックス化のペナルティはかかりません。Current が int を返すプロパティである場合は、それをそのまま使用します。
次に、列挙子が値型の場合、列挙子を IEnumerator にボックス化しません。
前述のように、BCL チームはこれについて多くの調査を行い、ほとんどの場合、列挙子の割り当てと割り当て解除のペナルティが十分に大きいため、値型にする価値があることを発見しました。いくつかのクレイジーなバグを引き起こします。
たとえば、次のように考えてください。
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h = somethingElse;
}
h を変異させる試みが失敗することを期待するのは当然のことであり、実際にそうです。コンパイラは、破棄が保留されているものの値を変更しようとしていること、および破棄する必要があるオブジェクトが実際には破棄されない可能性があることを検出します。
今、あなたが持っていたとします:
struct MyHandle : IDisposable { ... }
...
using (MyHandle h = whatever)
{
h.Mutate();
}
そこで何が起こるの?h が読み取り専用フィールドである場合に、コンパイラが行うことを合理的に期待するかもしれません:コピーを作成し、破棄する必要がある値の内容をメソッドが破棄しないようにするために、コピーを変更します。
ただし、これは、ここで何が起こるべきかについての私たちの直感と矛盾します。
using (Enumerator enumtor = whatever)
{
...
enumtor.MoveNext();
...
}
using ブロック内で MoveNext を実行すると、列挙子が構造体であるか参照型であるかに関係なく、次の列挙子に移動することが期待されます。
残念ながら、現在の C# コンパイラにはバグがあります。このような状況にある場合、一貫性のない戦略を選択します。今日の動作は次のとおりです。
残念ながら、仕様ではこの問題に関するガイダンスはほとんど提供されていません。一貫性のないやり方をしているため、何かが壊れていることは明らかですが、何をするのが正しいのかはまったく明らかではありません。