2009-07-30 94 views
112

看看下面:爲什麼'ref'和'out'支持多態?

class A {} 

class B : A {} 

class C 
{ 
    C() 
    { 
     var b = new B(); 
     Foo(b); 
     Foo2(ref b); // <= compile-time error: 
        // "The 'ref' argument doesn't match the parameter type" 
    } 

    void Foo(A a) {} 

    void Foo2(ref A a) {} 
} 

爲什麼上面的編譯時錯誤發生的呢?這與refout參數都會發生。

回答

155

=============

UPDATE:我用這個答案這個博客條目的基礎:

Why do ref and out parameters not allow type variation?

更多請見博客頁面關於這個問題的評論。感謝您的好問題。

=============

讓我們假設你有類AnimalMammalReptileGiraffeTurtleTiger,具有明顯的子類關係。

現在假設你有一個方法void M(ref Mammal m)M可以讀取和寫入m


你可以通過Animal類型來M一個變量?

不是。該變量可能包含Turtle,但M將假定它只包含哺乳動物。 A Turtle不是Mammal

結論1ref參數不能做成「更大」。 (還有更多的動物比哺乳動物,所以變數也越來越「大」,是因爲它可以包含更多的東西。)


你可以通過Giraffe類型的變量M

M可以寫信給m,並M可能要編寫一個Tigerm。現在,您已將Tiger放入實際爲Giraffe類型的變量中。

結論2ref參數不能做得「小」。


現在考慮N(out Mammal n)

您能否將Giraffe類型的變量傳遞給N

N可以寫信給n,並N可能要編寫一個Tiger

結論3out參數不能設置爲「較小」。


你可以通過Animal類型來N一個變量?

嗯。

那麼,爲什麼不呢? N無法讀取n,它只能寫入它,對吧?你寫一個Tiger到一個Animal類型的變量,你就全部設置好了,對吧?

錯誤。規則不是「N只能寫入n」。

的規則,簡要:

1)N有可能寫入nN正常返回。 (如果N拋出,全盤皆輸。)

2)N有它從n讀的東西之前寫的東西n

允許這一系列事件:

  • 聲明Animal類型的字段x
  • 通過x作爲out參數到N
  • NTiger寫入n,這是x的別名。
  • 在另一個線程上,有人將Turtle寫入x
  • N嘗試讀取n的內容,並發現它認爲是Mammal類型的變量的Turtle

很明顯,我們想使這種非法。

結論4out參數不能設置爲「較大」。


最後得出結論無論ref也不out參數可能有所不同它們的類型。否則就是打破可驗證的類型安全。

如果這些問題在基本類型理論中引起您的興趣,請考慮閱讀my series on how covariance and contravariance work in C# 4.0

+6

+1。使用真實世界的例子清楚地展示問題(例如 - 用A,B和C解釋使得難以證明爲什麼它不起作用),這是一個很好的解釋。 – 2009-07-30 15:25:51

+4

我感到謙卑的閱讀這個思維過程。我想我最好回到書本上來! – 2009-09-22 00:44:46

+0

在這種情況下,我們真的不能使用Abstract類變量作爲參數並傳遞它的派生類對象! – 2009-09-23 05:48:11

27

因爲在這兩種情況下,您必須能夠將值分配給ref/out參數。

如果您嘗試將b傳遞給Foo2方法作爲參考,並且在Foo2中您試圖指定a = new A(),則這將無效。
你不能寫入相同的理由:

B b = new A(); 
+1

Ninj'd由4秒! :) – CannibalSmith 2009-07-30 14:58:54

+0

+1直接點,並解釋完美的原因。 – 2009-07-30 15:32:56

2

因爲給Foo2一個ref B將導致畸形的對象,因爲Foo2只知道如何填寫的BA一部分。

4

考慮:

class C : A {} 
class B : A {} 

void Foo2(ref A a) { a = new C(); } 

B b = null; 
Foo2(ref b); 

這將違反類型安全

+0

這是更多的不清楚的推斷類型的「B」,由於var是那裏的問題。 – 2011-11-04 10:37:16

+0

我猜你的意思是=> B b = null; – 2015-09-23 23:36:27

+0

@amiralles - 是的,那個'var'是完全錯誤的。固定。 – 2015-09-24 07:00:46

9

你跟協方差(和逆變)的經典OOP問題所困擾,請參閱wikipedia:竟有這樣的事實可能違抗直觀的期望,在數學上不可能讓派生類代替基本代替變量(可賦值)參數(還有容器的項目是可賦值的,出於同樣的原因),同時仍尊重Liskov's principle。爲什麼在現有答案中勾畫了這些內容,並在這些wiki文章和鏈接中進行了更深入的探索。

OOP語言似乎這樣做,同時保持傳統的靜態類型安全性是「作弊」(插入隱藏的動態類型檢查,或要求編譯時檢查所有源的檢查);基本的選擇是:或者放棄這個協變並接受從業者的困惑(就像C#在這裏做的那樣),或者轉向動態類型方法(如第一個OOP語言,Smalltalk所做的那樣),或者轉向不可變的(single-賦值)數據,就像函數式語言一樣(在不變性的情況下,你可以支持協變性,也可以避免其他相關的難題,例如在可變數據世界中不能有Square子類Rectangle的事實)。

0

是不是編譯器告訴你它會讓你明確地轉換對象,以確保你知道你的意圖是什麼?

Foo2(ref (A)b) 
+0

不能這樣做,「一個ref或out參數必須是一個可分配的變量」 – 2011-11-04 10:31:46

0

從安全的角度來看有道理,但我寧願它如果編譯器給了一個警告而不是錯誤的,因爲有按引用傳遞polymoprhic對象的合法用途。例如

class Derp : interfaceX 
{ 
    int somevalue=0; //specified that this class contains somevalue by interfaceX 
    public Derp(int val) 
    { 
    somevalue = val; 
    } 

} 


void Foo(ref object obj){ 
    int result = (interfaceX)obj.somevalue; 
    //do stuff to result variable... in my case data access 
    obj = Activator.CreateInstance(obj.GetType(), result); 
} 

main() 
{ 
    Derp x = new Derp(); 
    Foo(ref Derp); 
} 

這不會編譯,但它會工作嗎?

0

如果你使用你的類型的實際例子,你會看到它:

SqlConnection connection = new SqlConnection(); 
Foo(ref connection); 

現在你有你的函數,它的祖先Object):

void Foo2(ref Object connection) { } 

什麼可能是錯誤的呢?

void Foo2(ref Object connection) 
{ 
    connection = new Bitmap(); 
} 

你好不容易到Bitmap分配給您的SqlConnection

這並不好。


與他人再試一次:

SqlConnection conn = new SqlConnection(); 
Foo2(ref conn); 

void Foo2(ref DbConnection connection) 
{ 
    conn = new OracleConnection(); 
} 

你釀的OracleConnection過頂你SqlConnection的。