共分散と反分散が値タイプをサポートしない理由


149

IEnumerable<T>あるコバリアントが、それは値型、ちょうどのみ参照型をサポートしていません。以下の単純なコードは正常にコンパイルされています。

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

しかし、からstringに変更するintと、コンパイルエラーが発生します。

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

その理由はMSDNで説明されています

分散は参照型にのみ適用されます。バリアント型パラメーターに値の型を指定すると、その型パラメーターは、結果の構成型に対して不変になります。

私が検索したところ、値の型と参照の型の間のボクシングが原因であると述べたいくつかの質問が見つかりました。しかし、ボクシングが理由である理由はまだ私の心をはっきりさせませんか?

共分散と反変が値タイプをサポートしない理由と、ボクシングがこれにどのように影響するかについて、簡単で詳細な説明を誰かに教えてもらえますか?


3
:私の同様の質問にもエリックの答えを参照してくださいstackoverflow.com/questions/4096299/...
ソーン

回答:


126

基本的に、分散は、CLRが値に表現上の変更を加える必要がないことを保証できる場合に適用されます。参照はすべて同じように見えます-したがって、表現を変更せずにをIEnumerable<string>として使用できますIEnumerable<object>。インフラストラクチャが確実に有効であることを保証している限り、ネイティブコード自体は、値を使用して何をしているのかを知る必要はありません。

値型の場合は機能しません-をIEnumerable<int>として扱うにはIEnumerable<object>、シーケンスを使用するコードでボクシング変換を実行するかどうかを知っている必要があります。

このトピックの一般的な詳細については、表現とアイデンティティに関する Eric Lippertのブログ投稿をご覧ください。

編集:私自身、エリックのブログ投稿を読み直しましたが、少なくとも2つはリンクされていますが、それはアイデンティティと同じくらい重要です。特に:

これが、インターフェイス型とデリゲート型の共変変換と反変変換で、すべての可変型引数が参照型である必要がある理由です。バリアント参照変換が常にIDを保持するようにするには、型引数を含むすべての変換もIDを保持する必要があります。型引数のすべての重要な変換がID保持であることを保証する最も簡単な方法は、それらを参照変換に制限することです。


5
@CuongLe:それはある意味では実装の詳細ですが、それが制限の根本的な理由だと思います。
ジョンスキート

2
@AndréCaron:Ericのブログ投稿はここで重要です。これは単なる表現ではなく、アイデンティティの保持でもあります。しかし、表現の保持は、生成されたコードがこれをまったく気にする必要がないことを意味します。
ジョンスキート

1
intは、のサブタイプではないため、正確にはIDを保持できませんobject。表現の変更が必要であるという事実は、これの結果にすぎません。
アンドレ・キャノン

3
intはオブジェクトのサブタイプではないのですか?Int32は、System.Objectから継承するSystem.ValueTypeから継承します。
David Klempfner、

1
@DavidKlempfner @AndréCaronのコメントの言い回しが悪いと思います。などの値タイプにInt32は、「ボックス化」と「ボックス化解除」の2つの表現形式があります。ソースコードレベルでは通常見えない場合でも、コンパイラはコードを挿入して1つの形式から別の形式に変換する必要があります。実際には、「ボックス化された」フォームのみが基になるシステムによってのサブタイプであると見なされobjectますが、値の型が互換性のあるインターフェイスまたは何かの型に割り当てられると、コンパイラは自動的にこれを処理しますobject
スティーブ

10

基礎となる表現について考えると、理解しやすいでしょう(これは実際には実装の詳細ですが)。文字列のコレクションは次のとおりです。

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]:整数= 3

これをan IEnumerable<object>に変換するには、intをボックス化してデータを変換する必要があります。

[0]:オブジェクト参照-> 1
[1]:オブジェクト参照-> 2
[2]:オブジェクト参照-> 3

この変換にはキャスト以上のものが必要です。


2
ボクシングは単なる「実装の詳細」ではありません。ボックス化された値の型は、クラスオブジェクトと同じ方法で格納され、クラスオブジェクトのように、外界が認識できる限り動作します。唯一の違いは、ボックス化された値型の定義内で、thisフィールドを保持するオブジェクトを参照するのではなく、フィールドを格納するヒープオブジェクトのフィールドをオーバーレイする構造体を参照することです。ボックス化された値型のインスタンスが、外側のヒープオブジェクトへの参照を取得するための明確な方法はありません。
スーパーキャット2012年

7

すべてはLSP(Liskov Substitution Principle)の定義から始まると思います。

q(x)がT型のオブジェクトxについて証明可能なプロパティである場合、SがTのサブタイプであるS型のオブジェクトyに対してq(y)は真でなければなりません。

しかし、例えば値の型は、intの代替することはできませんobjectの中でC#。証明は非常に簡単です:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

これは、オブジェクトに同じ「参照」falseを割り当てた場合でも戻ります。


1
私はあなたが正しい原則を使用していると思いますが、作られる証拠intはありません:はのサブタイプではないobjectので、原則は適用されません。あなたの「証明」は中間表現に依存しています。中間表現Integerはのサブタイプでobjectあり、言語は暗黙的な変換を持っています(object obj1=myInt;実際にはobject obj1=new Integer(myInt);に展開されます)。
アンドレ・キャノン

言語は型間の正しいキャストを処理しますが、intの動作は、オブジェクトのサブタイプから予想される動作に対応しません。
Tigran 2012

私の要点は、まさにそれintがのサブタイプではないということですobject。さらに、LSPは適用されません。これはmyIntobj1obj23つの異なるオブジェクトを参照します。1つとint2つ(非表示)Integerです。
アンドレ・キャノン

22
@André:C#はJavaではありません。C#のintキーワードはBCLのエイリアスですSystem.Int32。これは実際にはobject(エイリアスのSystem.Object)サブタイプです。実際、intの基本クラスは、基本クラスがSystem.ValueType誰であるかですSystem.Object。次の式を評価してみてくださいtypeof(int).BaseType.BaseType。理由ReferenceEqualsここで返す偽のことであるint2つの別々の箱に箱詰めされ、各ボックスのアイデンティティは、他のボックスに異なるです。したがって、2つのボックス化操作は、ボックス化された値に関係なく、常に同一ではない2つのオブジェクトを生成します。
Allon Guralnek、2012

@AllonGuralnek:各値の型(System.Int32またはList<String>.Enumerator)は、実際には、格納場所の型とヒープオブジェクトの型(「ボックス化された値の型」と呼ばれることもあります)の2種類を表します。タイプの派生System.ValueType元の保管場所には前者が保持されます。タイプが同じであるヒープオブジェクトは、後者を保持します。ほとんどの言語では、前者から後者への拡大キャストと、後者から前者への縮小キャストが存在します。ボックス化された値の型は値の型の格納場所と同じ型記述子を持ちますが、...
supercat

3

実装の詳細に行き着きます。値型は参照型とは異なる方法で実装されます。

値の型を参照型として強制的に処理する場合(つまり、インターフェイスを介して参照するなどしてボックス化する)、分散を取得できます。

違いを確認する最も簡単な方法は、次のことを単純に検討することArrayです。Value型の配列は連続して(直接)メモリに配置されます。ポイントされているオブジェクトは個別に割り当てられます。

他の(関連する)問題(*)は、(ほとんど)すべての参照型が分散目的で同じ表現を持ち、多くのコードが型間の違いを知る必要がないため、共分散と逆分散が可能です(そして簡単に実装されています-多くの場合、余分な型チェックを省略しただけです)。

(*)同じ問題であるように見えるかもしれません...

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.