IEnumerable<T>
はco-variantですが、値型をサポートせず、参照型のみをサポートします。以下の簡単なコードは正常にコンパイルされます。
IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;
ただし、string
からint
に変更すると、コンパイルエラーが発生します。
IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;
その理由は [〜#〜] msdn [〜#〜] で説明されています。
分散は参照型にのみ適用されます。バリアント型パラメーターに値の型を指定した場合、その型パラメーターは結果の構築型に対して不変です。
私は検索し、いくつかの質問が理由を言及していることを発見しました値型と参照型の間のボクシング。しかし、それがボクシングが理由である理由はまだ私の心をはっきりさせませんか?
誰かが共分散と反分散が値型をサポートしない理由とboxingがこれにどのように影響するのか、簡単で詳細な説明をお願いします。
基本的に、CLRが値に表現の変更を行う必要がないことを保証できる場合、分散が適用されます。参照はすべて同じように見えるため、表現を変更せずにIEnumerable<string>
をIEnumerable<object>
として使用できます。インフラストラクチャがそれが確実に有効であることを保証している限り、ネイティブコード自体は値で何をしているのかをまったく知る必要はありません。
値型の場合、それは機能しません-IEnumerable<int>
をIEnumerable<object>
として扱うには、シーケンスを使用するコードがボクシング変換を実行するかどうかを知る必要があります。
このトピックの一般的な詳細については、Eric Lippertの 表現とアイデンティティに関するブログ投稿 をお読みください。
編集:Ericのブログ投稿を自分で読み直したところ、少なくともidentityが表現と同じくらいですが、2つはリンクされています。特に:
これが、インターフェイス型およびデリゲート型の共変および反変の変換で、すべての可変型引数が参照型である必要がある理由です。バリアント参照変換が常に同一性を保持するようにするには、型引数を含むすべての変換も同一性を保持する必要があります。型引数のすべての非自明な変換が同一性を保持することを保証する最も簡単な方法は、それらを参照変換に制限することです。
基礎となる表現について考えると、おそらく理解しやすいでしょう(これは実際には実装の詳細ですが)。文字列のコレクションは次のとおりです。
_IEnumerable<string> strings = new[] { "A", "B", "C" };
_
strings
は次の表現を持つと考えることができます。
[0]:文字列参照-> "A" [1]:文字列参照-> "B" [2]:文字列参照-> "C"
これは3つの要素のコレクションであり、各要素は文字列への参照です。これをオブジェクトのコレクションにキャストできます。
_IEnumerable<object> objects = (IEnumerable<object>) strings;
_
基本的には、参照がオブジェクト参照であることを除いて、同じ表現です。
[0]:オブジェクト参照-> "A" [1]:オブジェクト参照-> "B" [2]:オブジェクト参照-> "C"
表現は同じです。参照の扱いが異なるだけです。 _string.Length
_プロパティにはアクセスできなくなりますが、object.GetHashCode()
を呼び出すことはできます。これをintのコレクションと比較します。
_IEnumerable<int> ints = new[] { 1, 2, 3 };
_
[0]:int = 1 [1]:int = 2 [2]:int = 3
これを_IEnumerable<object>
_に変換するには、intをボックス化してデータを変換する必要があります。
[0]:オブジェクト参照-> 1 [1]:オブジェクト参照-> 2 [2]:オブジェクト参照-> 3
この変換にはキャスト以上のものが必要です。
すべてはLSP
(Liskov Substitution Principle)の定義から始まると思います。
q(x)がT型のオブジェクトxについて証明可能なプロパティである場合、SがTのサブタイプであるS型のオブジェクトyに対してq(y)が真でなければなりません。
ただし、int
などの値型は、C#
のobject
の代わりにはなりません。証明は非常に簡単です:
int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);
これは、オブジェクトにsame "reference"を割り当てた場合でも、false
を返します。
実装の詳細に行き着きます。値型は、参照型とは異なる方法で実装されます。
値型を強制的に参照型として処理する場合(つまり、インターフェイスを介して参照するなどして、それらをボックス化する場合)、分散を取得できます。
違いを確認する最も簡単な方法は、単にArray
を考慮することです。値型の配列は、連続して(直接)メモリにまとめられます。参照型の配列は、参照(ポインタ)メモリ;指し示されているオブジェクトは個別に割り当てられます。
他の(関連する)問題(*)は、(ほとんど)すべての参照型が分散目的で同じ表現を持ち、多くのコードが型の違いを知る必要がないため、共分散と反分散が可能です(そして簡単に実装-多くの場合、追加の型チェックを省略します)。
(*)同じ問題のように見えるかもしれません...