2010-09-23 33 views
6

我已經寫了一個MVCContrib Html助手csharp的擴展方法,並且對通用約束的形式感到驚訝,它的表面似乎是循環的通過類型參數引用它自己。爲什麼這個泛型約束似乎有一個循環引用編譯

這就是說,該方法編譯和按需要工作。

我很想有人解釋爲什麼這個工作,如果存在更直觀的直觀語法,如果沒有人知道爲什麼?

這裏是編譯和功能代碼,但我已經刪除了T列表的例子,因爲它陰雲密佈的問題。 以及使用列表<T>的類似方法。

namespace MvcContrib.FluentHtml 
{ 
    public static class FluentHtmlElementExtensions 
    { 
    public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value) 
     where T: TextInput<T> 
    { 
     if (value) 
      element.Attr("readonly", "readonly"); 
     else 
      ((IElement)element).RemoveAttr("readonly"); 
     return element; 
    } 
    } 
} 

/*analogous method for comparison*/ 
    public static List<T> AddNullItem<T>(this List<T> list, bool value) 
     where T : List<T> 
    { 
     list.Add(null); 
     return list; 
    } 

在約束T中的第一種方法:的TextInput <Ť>似乎所有的意圖和目的,是圓形的。但是如果我把它註釋掉我得到一個編譯錯誤:

「類型‘T’不能在泛型類型或方法作爲類型參數‘T’‘MvcContrib.FluentHtml.Elements.TextInput <牛逼>’ 。 沒有從'T'到'MvcContrib.FluentHtml.Elements.TextInput <T>'的裝箱轉換或類型參數轉換。「

和在列表<Ť>情況下的誤差(或多個):

「的最好重載方法匹配 'System.Collections.Generic.List.Add(T)' 具有一些無效的參數 參數1:無法從「<空>」轉換到「T」」

我能想象更直觀的定義將是一個包括2種類型,參考噸Ó一般類型和在約束類型例如參考:

public static TextInput<T> ReadOnly<T,U>(this TextInput<T> element, bool value) 
    where U: TextInput<T> 

public static U ReadOnly<T,U>(this U element, bool value) 
    where U: TextInput<T> 

但既不這些編譯的。

+0

如已回答已經這不是圓形的,但作爲一個側面說明它可以創建循環繼承,有時可以編譯,有時不會(例如添加,刪除或重命名文件和文件夾可能導致編譯隨機成功或失敗)。所以循環繼承的錯誤確實存在。 (VS2010) – AnorZaken 2015-02-24 02:32:46

回答

10

更新:這個問題是我的blog article on the 3rd of February 2011的基礎。感謝您的好問題!


這是合法的,它不是循環的,它是相當普遍的。我個人不喜歡它。

我不喜歡其理由是:

1)它是過度聰明;正如你所發現的那樣,聰明的代碼對於不熟悉類型系統複雜性的人來說很難直觀地理解。 2)它不能很好地映射到我對泛型類型「表示」的直覺。我喜歡用類來表示事物的類別,用泛型類來表示參數化類別。我清楚地知道,「字符串列表」和「數字列表」都是兩種列表,不同之處僅在於列表中的事物類型。它對我來說很不清楚「T的一個TextInput,其中T是T的TextInput」。不要讓我想。

3)這種模式經常被用來試圖在類型系統中實施一個實際上不能在C#中強制執行的約束。即這一個:

abstract class Animal<T> where T : Animal<T> 
{ 
    public abstract void MakeFriends(IEnumerable<T> newFriends); 
} 
class Cat : Animal<Cat> 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

這裏的想法是「貓的動物的一個小類只能與其他貓交朋友。」

的問題是,所需的規則實際上並沒有強制執行:

class Tiger: Animal<Cat> 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

現在老虎可以交朋友貓,但不能與老虎。

實際上使在C#這個工作你需要做這樣的事情:

abstract class Animal 
{ 
    public abstract void MakeFriends(IEnumerable<THISTYPE> newFriends); 
} 

其中「THISTYPE」是一個神奇的新的語言功能,它的意思是「壓倒一切的類被要求填寫自己的在此輸入」。

class Cat : Animal 
{ 
    public override void MakeFriends(IEnumerable<Cat> newFriends) {} 
} 

class Tiger: Animal 
{ 
    // illegal! 
    public override void MakeFriends(IEnumerable<Cat> newFriends) { ... } 
} 

不幸的是,這不是類型安全之一:

Animal animal = new Cat(); 
animal.MakeFriends(new Animal[] {new Tiger()}); 

如果規則是「動物交朋友與任何同類」,那麼動物可以交朋友的動物。但一隻貓只能與貓交朋友,而不是老虎!在參數位置的東西必須有效地反向;在這個假設的情況下,我們會要求協方差,這是行不通的。

我似乎有點離題。回到這個奇怪的循環模式的主題:我儘量只使用這種模式,共同的,容易理解像其他的答案中提到的那些情況:

class SortedList<T> where T : IComparable<T> 

也就是說,我們需要每個T是相當於每其他T如果我們有任何希望對它們進行排序的列表。

要實際被標記爲圓形,必須有在依賴關係的善意-善意圓:

class C<T, U> where T : U where U : T 

類型理論的一個有趣的區域(即目前的C#編譯器處理很差)是非面積 - 圓形但是無限期的通用類型。我寫了一個infinitary類型的檢測器,但它沒有將其編譯到C#4編譯器中,對於可能假設的編譯器未來版本並不是高優先級。如果你有興趣infinitary類型的一些例子,或者其中的C#循環檢測弄亂了一些示例,請參閱我的文章:

http://blogs.msdn.com/b/ericlippert/archive/2008/05/07/covariance-and-contravariance-part-twelve-to-infinity-but-not-beyond.aspx

+0

感謝埃裏克,這是一個有趣的領域。我還有其他一些關於約束的問題,但我想我應該將它們作爲另一個問題發佈:-)。 – 2010-09-26 21:48:10

1

你使用它的方式毫無意義。但就在同參數的約束使用泛型參數是很正常的,這裏有一個更明顯的例子:

class MySortedList<T> where T : IComparable<T> 

約束表達的事實,必須有類型T的對象之間的排序,以讓他們進入排序順序。

編輯:我要解構你的第二個例子,其中的約束實際上是錯誤的,但有助於編譯。

有問題的代碼是:

/*analogous method for comparison*/ 
public static List<T> AddNullItem<T>(this List<T> list, bool value) 
    where T : List<T> 
{ 
    list.Add(null); 
    return list; 
} 

也不會沒有限制編譯的原因是值類型不能nullList<T>是引用類型,因此通過強制where T : List<T>可以強制T成爲可以爲空的引用類型。但你也讓AddNullItem幾乎沒用,因爲你不能再調用它List<string>等正確的約束是:

/* corrected constraint so the compiler won't complain about null */ 
public static List<T> AddNullItem<T>(this List<T> list) 
    where T : class 
{ 
    list.Add(null); 
    return list; 
} 

注:我也刪除這是未使用的第二個參數。

但是如果你使用default(T),設置出於這樣的目的,你甚至可以刪除該約束,這意味着nullT是引用類型和任何類型的值都爲零。

/* most generic form */ 
public static List<T> AddNullItem<T>(this List<T> list) 
{ 
    list.Add(default(T)); 
    return list; 
} 

我懷疑你的第一種方法也需要像T : class約束,但因爲我不知道你在使用的所有類我不能肯定地說。

+0

我同意沒有道理,但它編譯並做我想要的。你的例子太簡單了,無法捕獲用例。 – 2010-09-24 00:17:57

+0

對不起,我打開輸入,我的意思是繼續......我在想一個列表<T>其中我可以有一個香蕉列表,並有一個擴展方法,如list.AddNullItem(),所以泛型參數,本身是通用的。 – 2010-09-24 00:25:55

+0

我希望我添加的附加信息將幫助您瞭解爲什麼它沒有約束沒有編譯,但約束不一定是正確的。 – 2010-09-24 00:28:28

0

我只能猜測你發佈的代碼是幹什麼的。這就是說,我可以看到像這樣的泛型約束的優點。在任何需要某種類型的參數的情況下,對於相同類型的參數都可以執行某些操作,這是有道理的。

下面是一個不相關的例子:

public static IComparable<T> Max<T>(this IComparable<T> value, T other) 
    where T : IComparable<T> 
{ 
    return value.CompareTo(other) > 0 ? value : other; 
} 

這樣的代碼將允許你寫類似:

int start = 5; 
var max = start.Max(6).Max(3).Max(10).Max(8); // result: 10 

命名空間FluentHtml是我應該有點提示你這是打算的代碼(以啓用方法調用的鏈接)。

5

原因約束是因爲TextInput類型本身就有這樣的約束。

public abstract class TextInput<T> where T: TextInput<T>{ 
    //... 
} 

還要注意的是TextInput<T>是抽象的,做出這樣的類的實例的唯一方法是從它的CRTP樣的方式獲得:

public class FileUpload : TextInput<FileUpload> { 
} 

擴展方法不是沒有編譯這個約束,這就是爲什麼它在那裏。

原因首先有CRTP是使強類型的方法,而對類實現Fluent Interface,因此考慮這樣的例子:

public abstract class TextInput<T> where T: TextInput<T>{ 
    public T Length(int length) { 
     Attr(length); 
     return (T)this; 
    } 
} 
public class FileUpload : TextInput<FileUpload> { 
    FileUpload FileName(string fileName) { 
     Attr(fileName); 
     return this; 
    } 
} 

所以,當你有一個FileUpload例如,Length回報即使它是在基類中定義的,也是FileUpload的一個實例。這使得語法如下可能:

FileUpload upload = new FileUpload(); 
upload      //FileUpload instance 
.Length(5)     //FileUpload instance, defined on TextInput<T> 
.FileName("filename.txt"); //FileUpload instance, defined on FileUpload 

編輯爲了解決OP的有關遞歸類繼承的意見。這是C++中一個衆所周知的模式,叫做奇怪的循環模板模式。閱讀它here。直到今天,我不知道它可能在C#中。我懷疑這個約束與在C#中使用這種模式有關。

+0

Igor我認爲你關於從約束類型繼承的類型的評論正在成爲問題的一部分,它不會在沒有約束的情況下編譯。但是我真正的問題是關於語法,儘管T似乎指的是類型和約束,即T和T本身的TextInput似乎都是模糊的。 – 2010-09-24 00:56:39

+0

謝謝剛剛讀過CRTP。笑,我的例子有一個'這是設計'類型的感覺,從這個意義上說,你可以將循環定義爲無意的遞歸,或者在這種情況下看起來像循環是真正的遞歸。它也顯示瞭如何在給定的編程語言中變得流利:) – 2010-09-24 01:35:44

+0

哈哈,我在使用術語「流利」(fluent)這個術語時是在夾緊你。這是非常合適的,因爲這種模式被稱爲Fluent Interface(方法鏈接)。 – 2010-09-24 01:46:15

0
public static TextInput<T> ReadOnly<T>(this TextInput<T> element, bool value) 
    where T: TextInput<T> 

讓我們來分析一下:

TextInput<T>是返回類型。

TextInput<T>是被擴展的類型(第一個參數的給靜態方法的類型)

ReadOnly<T>是延伸的類型,其定義包括T,即TextInput<T>功能的名稱。

where T: TextInput<T>是對T的約束從ReadOnly<T>,使得T可以在通用TextInput<TSource>中使用。 (T是TSource!)

我不認爲它是循環的。

如果取出約束,我認爲element試圖被轉換爲泛型類型(不是泛型類型的TextInput),這顯然不起作用。

+0

嗨,傑夫,除了我們得出不同的結論之外,你和我的假設都一樣。這對我來說是圓形的,因爲使用數學或邏輯替換的約束意味着我們應該能夠做到這樣的事情:只讀其中T:TextInput =>只讀>其中T:TextInput =>只讀 >> ....沖洗和無限重複無限:)。 – 2010-09-24 01:21:31

+0

嗨西蒙,如果你想'在哪裏T:TextInput '就像說,「其中T是TextInput的通用部分」,它應該點擊。希望。 :) – 2010-09-24 01:46:44

+0

換句話說,不要將where子句作爲邏輯替代來讀取。不是。它的意思是定義T如何與另一個對象相關,這更像是組合而不是替換。 – 2010-09-24 01:49:45

相關問題