web-dev-qa-db-ja.com

xからyへの共変配列変換により、実行時例外が発生する場合があります

private readonlyLinkLabelsのリスト(IList<LinkLabel>)。後でこのリストにLinkLabelsを追加し、次のようにそれらのラベルをFlowLayoutPanelに追加します。

foreach(var s in strings)
{
    _list.Add(new LinkLabel{Text=s});
}

flPanel.Controls.AddRange(_list.ToArray());

Resharperは警告を表示します:Co-variant array conversion from LinkLabel[] to Control[] can cause run-time exception on write operation

私が理解するのを助けてください:

  1. これはどういう意味ですか?
  2. これはユーザーコントロールであり、ラベルを設定するために複数のオブジェクトからアクセスされることはないため、コードをそのまま保持しても影響はありません。
131
TheVillageIdiot

これが意味するのはこれ

Control[] controls = new LinkLabel[10]; // compile time legal
controls[0] = new TextBox(); // compile time legal, runtime exception

そして、より一般的な用語で

string[] array = new string[10];
object[] objs = array; // legal at compile time
objs[0] = new Foo(); // again legal, with runtime exception

C#では、オブジェクトの配列(この場合はLinkLabels)を基本型の配列(この場合はコントロールの配列)として参照できます。また、配列にControlであるanotherオブジェクトを割り当てることはコンパイル時に有効です。問題は、配列実際にはControlsの配列ではない実行時、それは依然としてLinkLabelsの配列であるということです。そのため、割り当てまたは書き込みは例外をスローします。

144
Anthony Pegram

アンソニーペグラムの答えを明確にしようとします。

ジェネリック型は、その型の値を返すときに、ある型引数に対して共変です(たとえば、Func<out TResult>TResultのインスタンスを返し、IEnumerable<out T>Tのインスタンスを返します)。つまり、何かがTDerivedのインスタンスを返す場合、そのようなインスタンスをTBaseであるかのように扱うこともできます。

ジェネリック型は、その型の値を受け入れる場合、一部の型引数に対して反変です(たとえば、Action<in TArgument>TArgumentのインスタンスを受け入れます)。つまり、何かがTBaseのインスタンスを必要とする場合、TDerivedのインスタンスを渡すこともできます。

何らかのタイプのインスタンスを受け入れ、返すジェネリックタイプ(ジェネリックタイプシグネチャ、たとえばCoolList<TIn, TOut>で2回定義されていない限り)は、対応するタイプ引数で共変でも反変でもないことは非常に論理的なようです。たとえば、Listは、.NET 4ではList<T>List<in T>ではなく、List<out T>として定義されています。

いくつかの互換性の理由により、Microsoftはその引数を無視し、値型引数で配列を共変にしました。おそらく分析を行った結果、ほとんどの人は配列を読み取り専用であるかのようにしか使用せず(つまり、配列初期化子を使用して一部のデータを配列に書き込むだけであるため)、実行可能性に起因する欠点を上回る利点があることがわかりました誰かが配列に書き込むときに共分散を利用しようとするとエラーが発生します。したがって、許可されていますが、推奨されていません。

元の質問に関しては、list.ToArray()は元のリストからコピーされた値で新しいLinkLabel[]を作成します。(合理的な)警告を取り除くには、Control[]AddRangeに渡す必要があります。 。 list.ToArray<Control>()は仕事をします:ToArray<TSource>は引数としてIEnumerable<TSource>を受け入れ、TSource[]を返します。 List<LinkLabel>は、読み取り専用のIEnumerable<out LinkLabel>を実装します。これは、IEnumerable共分散のおかげで、IEnumerable<Control>を引数として受け入れるメソッドに渡すことができます。

13
penartur

最も単純な「解決策」

flPanel.Controls.AddRange(_list.AsEnumerable());

List<LinkLabel>IEnumerable<Control>に共変的に変更しているため、アイテムを列挙可能に「追加」することはできないため、これ以上の心配はありません。

9
Chris Marisic

警告は、LinkLabel[]参照を介してControl[]Control以外のLinkLabelを理論的に追加できるという事実によるものです。これにより、ランタイム例外が発生します。

AddRangeControl[]を取るため、変換はここで行われます。

より一般的には、派生型のコンテナーを基本型のコンテナーに変換することは、後で概説した方法でコンテナーを後で変更できない場合にのみ安全です。配列はその要件を満たしていません。

9
Stuart Golodetz

問題の根本原因は他の回答に正しく記載されていますが、警告を解決するために、いつでも記述できます。

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl));
5
Tim Williams

VS 2008では、この警告は表示されません。これは.NET 4.0の新しいものでなければなりません。
明確化:Sam Mackrillによると、警告を表示するのはResharperです

C#コンパイラは、AddRangeが渡された配列を変更しないことを知りません。 AddRangeにはControl[]型のパラメーターがあるため、理論的にはTextBoxを配列に割り当てようとすることができます。これは、Controlが、配列は実際にはLinkLabelsの配列であり、そのような割り当てを受け入れません。

C#で配列を共変にすることは、Microsoftの悪い決断でした。そもそも派生型の配列を基本型の配列に割り当てることができるのは良い考えかもしれませんが、これは実行時エラーにつながる可能性があります!

これはどう?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray());
1
Sam Mackrill