# overload covariant contravariance (2/23) ###### tags: `overload` `covariant` `contravariance` `Generics` `constraints` `note` ## as operator > The as operator explicitly **converts** the result of an expression to a given reference or nullable value type. If the conversion isn't possible, the as operator returns null. Unlike a cast expression, **the as operator never throws an exception**.\[1] ...用法範例待補 ## Operator overloading > A user-defined type can overload a predefined C# operator. That is, a type can provide the custom implementation of an operation in case one or both of the operands are of that type.\[2] - 可以自訂義operator,但須定義在Type內。 - Console.Write(obj)對應非string Type obj的運作模式其實是會印出obj.ToString()的結果。 其根本原因為,**所有(?)**Class都是由Object繼承出來的,MSDN對Object的描述如下 > Supports **all classes** in the .NET class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all .NET classes; **it is the root of the type hierarchy**. 而Console.Write()函式之Overloads - Write(Object)如下 ```csharp public static void Write (object? value); ``` > If value is null, nothing is written and no exception is thrown. **Otherwise, the ToString method of value is called to produce its string representation**, and the resulting string is written to the standard output stream. 可看到,write是可接受null的(object? value),並且若value非null會自動印出ToString()的回傳字串。 ## override ToString()? 那現在問題來了,我們能不能用override亂玩ToString()這個Method的Return Type呢? **答案是不能** 首先MSDN有一段描述如下 > C# 9.0(.NET 5.) support **covariant** return types 所以就算我們override ToString(),ToString()可以回傳的ReturnType必須滿足 ```csharp ReturnType is String ``` 並且因為 > The is operator doesn't consider user-defined conversions. 那定義一個inherit from String 的Class呢? **答案也是不能** 因為String是sealed class。 > When applied to a class, the sealed modifier prevents other classes from inheriting from it. 所以我們目前玩不壞C#的ToString() XD。 ## 何謂covariant? 把class下所有東西,想像成定義這個類別的限制,越多限制會讓這個類別越小,而將適用小類別的東西當成大類別,便稱為covariant。 ### 以多邊形舉例 - 多邊形:由N條直線連接成的封閉形狀,有N個角 - 四邊形:由4條直線組成的多邊形 - 矩形:四個角都是直角的四邊形 - 正方形:四個邊等長的矩形 按照上面的定義,把正方形當成多邊形,就是covariance。 但是多邊形不能當成正方形! ### 再以狗來舉例 ```csharp 狗 一隻狗 = new 黃金獵犬(); // 黃金獵犬也算是一種狗 // error 吉娃娃 很吵的一隻狗 = new 狗(); // 那隻狗可能不夠吵,不配當吉娃娃,吉娃娃之恥 ``` 而被宣告出來的大物品也只能使用大容器定義下的功能/資料。 ```csharp 狗 不知道哪來的狗 = new 吉娃娃(); // error 不知道哪來的狗.發瘋(); // 就算他是被assign一個吉娃娃,但這個"不知道哪來的狗"是被視為狗 // 而狗並沒有發瘋這個功能,吉娃娃才有 ``` ### Method中的covariance 另外想像一個情境: 我們有一個Method,可以計算一台車子(Car)的評分。 而在每一台車子下定義該Method完全無法達到Code Reuse。 故我們想到,把計算評分的Method定義在Car的外面,使用Generic Method讓其適用於所有Car。 ```csharp public interface ICar { } public class RollsRoyce:ICar { } public static double Rating<T>(T car) where T:ICar { return 0.0; // We can calculate with proterties of ICar } ``` Rating() 會將`RollsRoyce`以`ICar`的型式讀入,但如同上面物件宣告的限制,在Rating() 中,我們只能使用`ICar`中就定義好的properties與Methods。 ```csharp Console.WriteLine(Rating(new RollsRoyce())); ``` ## 何謂contravariance? 若說covariant是 > 將小物品,放到大類別,**當成**一個大物品。 那contravariance為covariant的相反,是不是 > :x:將大物品,放到小類別,**變成**一個小物品。:x: 之所以打叉:x:,是因為**對於物件的指派**,contravariance並不適用! 也非常地合邏輯,舉例來說,吉娃娃有許多的功能是普通的狗所沒有的。 今天有一隻狗,我們不能把它當成外星生物吉娃娃,硬逼他像吉娃娃那樣發瘋! 既然物件的指派不適用,**那Method的指派呢**? 可以! 想像以下情境, 市面上有一項商品(Method)---只適用於吉娃娃的特製狗屋,因為他能安撫吉娃娃的身心,降低吉娃娃發瘋的頻率。 另外也有普通的狗屋,而商品櫃就像以下程式碼 ```csharp delegate void 狗屋(狗 您的愛犬); delegate void 吉娃娃狗屋(吉娃娃 外星生物); static void 普通狗屋(狗 您的愛犬) { } static void 特製狗屋(吉娃娃 外星生物) { } ``` 我買了一間這個狗屋,就如同以下程式碼 ```csharp 吉娃娃狗屋 家庭救星 = 特製狗屋 ``` 過幾天我同學也想買,但那間公司缺貨,所以很缺德的只賣給我同學一間普通的狗屋,就會像以下程式碼 ```csharp 吉娃娃狗屋 假的家庭救星 = 普通狗屋 ``` 因為`吉娃娃`是`狗`的subclass,所以普通狗屋也適用於吉娃娃(covariance),所以`普通狗屋`是可以當成`吉娃娃狗屋`來使用的,這就是contravariance。 ## contravariance搭配delegate delegate常用於串聯Event,正是使用contracariance的好時機! ```csharp class A { } class B:A { } class C:B { } static void MethodA(A a) { Console.WriteLine("A"); } static void MethodB(B b) { Console.WriteLine("B"); } static void MethodC(C c) { Console.WriteLine("C"); } delegate void DelC(C c); static void Main(string[] args) { DelC TestContravariance = (DelC)MethodA + MethodB + MethodC; TestContravariance(new C()); } /* Output: A B C */ ``` ## constraints延伸閱讀 延伸上面勞斯萊斯的情境,若我們希望把Rating\<ICar>當作預設Method,利用overloads讓Rating根據不同車種套用不同計算方式呢? #### 作法一(CS0111): both Generic Method different constraint ```csharp public static double Rating<T>(T car) where T:ICar { return 0.0; } public static double Rating<T>(T car) where T:RollsRoyce { return 1.0; } ``` 事實上,這種作法會導致錯誤訊息如下 > Type 'Program' already defines a member called 'Rating' with the same parameter types (CS0111) 其原因為,**constraints並不被視為Method Signatures的一種**,MSDN-Methods描述如下 > Methods are declared in a **class, struct, or interface** by **specifying the access** level such as public or private, **optional modifiers** such as abstract or sealed, the **return value**, the **name** of the method, and **any method parameters**. **These parts together are the signature of the method**.\[4] 並且 > A **return type** of a method **is not** part of the signature of the method for the purposes of **method overloading**. However, it is part of the signature of the method when determining the compatibility between a delegate and the method that it points to. 可以看到,Method Signatures並不包含generics constraints,故作法一的兩種函式對C#而言是完全一樣的,才會有CS0111的錯誤訊息。 #### 作法二:一個Generic一個指定DataType ```csharp public static double Rating<T>(T car) where T:ICar { return 0.0; } public static double Rating(RollsRoyce car) { return 1.0; } ``` 很順利的,這種做法可以讓Rating讀入new RollsRoyce()時,優先選擇下面的overload,且也可以指定選擇Generic Method。 ```csharp Console.WriteLine(Rating<ICar>(new RollsRoyce())); // Output: 0 Console.WriteLine(Rating(new RollsRoyce())); // Output: 1 ``` #### 新問題-base vs. generic? ```csharp public class RollsRoyce2:RollsRoyce { } Console.WriteLine(Rating(new RollsRoyce2())); // Output: 0 ``` 繼承自RollsRoyce的RollsRoyce2作為parameter時,C#竟然優先選擇了Generic Method去處理RollsRoyce2。 C#關於overload resolution的描述為,會優先選擇**nearest**的overload,而此處直覺是距離RollsRoyce2較近的是RollsRoyce。看來C#所謂的nearest還是要看一下overload resolution,蠻有趣的,但吃飽太閒= =。 ## 心得 1. 編譯器有自己的一套決定overload Method哪個最接近的判斷依據,MSDN沒有寫,要去看C# Language Specification。 2. Covariance vs. Contravariance,差別在一個僅適用於儲存,另一個僅適用於被讀入的parameters。 3. 還不懂cast expression的轉換過程是怎麼運作的,但想先跳過不深入下去了= = 4. Delegate除了像是Method interface,還可以發揮類似cast()運算的功能,讓Method pass by value時將各個Method放入Delegate中再相加。 #### 延伸閱讀:[overload resolution](https://docs.microsoft.com/en-us/dotnet/visual-basic/reference/language-specification/overload-resolution) ## References \[1] (as-operator)[https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/type-testing-and-cast#as-operator] \ \[2] (operator-overloading)[https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading] \ \[3] (method overloading based on generic constraints)[https://stackoverflow.com/questions/12529966/method-overloading-based-on-generic-constraints] \ \[4] (MSDN-Methods)[https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/methods] \ \[5] (Member Overloading)[https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/member-overloading]