2012-01-02 53 views
109

我有一個private readonly列表LinkLabel s(IList<LinkLabel>)。我後來添加LinkLabel s到這個列表,這些標籤添加到FlowLayoutPanel喜歡如下:從x到y的協變陣列轉換可能導致運行時異常

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. 這是一個用戶控件,不會被多個對象訪問以設置標籤,因此保留代碼不會影響它。

回答

121

它的意思是這個

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也是編譯時合法的。問題是數組實際上並不是控件的數組。在運行時,它仍然是一個LinkLabels數組。因此,分配或寫入將引發異常。

+0

我理解運行時/編譯時差,如你的例子,但不是從特殊類型轉換爲基類型合法嗎?此外,我已經鍵入列表,我將從'LinkLabel'(專用類型)到'Control'(基本類型)。 – TheVillageIdiot 2012-01-02 19:16:31

+1

是的,從LinkLabel轉換爲Control是合法的,但這不同於此處所發生的情況。這是關於從'LinkLabel []'轉換爲'Control []'的轉換警告,這仍然是合法的,但是會有運行時問題。所有改變的是數組被引用的方式。數組本身沒有改變。看到這個問題?該數組仍然是派生類型的數組。該引用是通過一個基本類型的數組。因此,爲基類型分配一個元素是合法的。但運行時類型不支持它。 – 2012-01-02 19:21:28

+0

在你的情況下,我不認爲這是一個問題,你只是使用數組添加到控件列表。 – 2012-01-02 19:22:03

8

該警告是由於這樣的事實,你可以比LinkLabel通過Control[]參考理論上增加Control其他的LinkLabel[]。這會導致運行時異常。

轉換髮生在這裏,因爲AddRange需要Control[]

更一般的情況是,如果您不能按照剛剛列出的方式修改容器,那麼只將派生類型的容器轉換爲基類型的容器纔是安全的。數組不滿足該要求。

2

隨着VS 2008,我沒有得到這個警告。這對.NET 4.0來說必須是新的。
說明:根據Sam Mackrill的說法,Resharper會顯示警告。

C#編譯器不知道AddRange將不會修改傳遞給它的數組。由於AddRange具有Control[]類型的參數,因此理論上可以嘗試將TextBox分配給該陣列,這對於真實的Control陣列是完全正確的,但該陣列實際上是LinkLabels的陣列,並且不會接受這樣的分配。

在c#中製作數組協同變體是微軟的一個糟糕的決定。雖然首先能夠將派生類型的數組分配給基本類型的數組似乎是一個好主意,但這可能會導致運行時錯誤!

+1

我從Resharper得到這個警告 – 2013-12-09 10:18:03

11

我會盡力澄清安東尼佩格拉姆的答案。

通用類型是在某種類型的參數協變,當它返回所述類型的值(例如中TResultFunc<out TResult>返回實例中,TIEnumerable<out T>返回實例)。也就是說,如果有東西返回TDerived的實例,那麼也可以使用這樣的實例,就好像它們是TBase一樣。

當接受所述類型的值時,泛型類型在某些類型參數上是逆變的(例如Action<in TArgument>接受TArgument的實例)。也就是說,如果某件事需要TBase的實例,那麼也可以通過TDerived的實例。

接受和返回某種類型實例(除非在泛型類型簽名中定義了兩次,例如CoolList<TIn, TOut>)的泛型類型對於相應的類型實參不是協變或逆變的,這似乎很合邏輯。例如,List在.NET 4中定義爲List<T>,而不是List<in T>List<out T>

某些兼容性原因可能會導致Microsoft忽略該參數,並使數組協變爲其值類型參數。也許他們進行了一次分析,發現大多數人只使用數組就好像它們是隻讀的(也就是說,它們只使用數組初始化器將一些數據寫入數組),因此,這些優點大大超過了可能的運行時所帶來的缺點寫入數組時有人嘗試使用協方差時發生錯誤。因此它被允許但不被鼓勵。

至於你原來的問題,list.ToArray()創建一個新的LinkLabel[]從最初的名單複製的數值,並且,擺脫(合理的)警告的,你需要在Control[]傳遞給AddRangelist.ToArray<Control>()將執行該作業:ToArray<TSource>接受IEnumerable<TSource>作爲其參數並返回TSource[]; List<LinkLabel>實現只讀IEnumerable<out LinkLabel>,這要歸功於IEnumerable協方差,可以傳遞給接受IEnumerable<Control>作爲其參數的方法。

6

最直接的「解決方案」

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

現在,因爲你是協變變List<LinkLabel>IEnumerable<Control>沒有更多的關注,因爲它是不可能的項目「添加」到一個枚舉。

5

問題的根本原因是在其他的答案正確描述,而且解決了警告,你可以寫:

_list.ForEach(lnkLbl => flPanel.Controls.Add(lnkLbl)); 
+1

一個很好的實用解決方案! – 2014-07-10 09:19:36

1

這個怎麼樣?

flPanel.Controls.AddRange(_list.OfType<Control>().ToArray()); 
+0

與'_list.ToArray ()'相同的結果。 – jsuddsjr 2016-02-25 18:31:12

相關問題