【本系列是我的 C# in Depth 第四版讀書筆記,背景故事在這裡

C# 4 帶來的一大改變是 dynamic,讓靜態型別語言也能享受動態型別語言獨有的福利,再加上 Optional Parameter,簡化了與 COM+ 世界溝通的成本。而 Gereric Variance (官方翻譯是泛型介面變異數或差異,直接用原文清楚一點)我們幾乎每天在用,像是 List<string> 自動轉型成 IEnumerable<object>: IEnumerable<object> strings = new List<string> { "a", "b", "c" };,string 繼承自 object,可以轉換天經地義;但 Dictionary<string, string> 卻不能轉型成 Dictionary<string, object>! 這限制讓我一頭霧水,現在終於知道為什麼了。

Dynamic Typing 動態型別

  • 放棄編譯階段檢查,提供高彈性,但隱含執行期間出錯的風險
static void Add(dynamic d)
{
    Console.WriteLine(d + d);
}

Add("text"); //texttext
Add(10); //20
Add(TimeSpan.FromMinutes(45)); //01:30:00
Add(true); //RuntimeBinderException: 無法將運算子 '+' 套用至類型 'bool' 和 'bool' 的運算元
  • Dynamic 參數 Overloading 問題
static void SampleMethod(int value) // 10
static void SampleMethod(decimal value) //10.5m 跟 10L (long 適用 decimal,即使其值可放入 int)
static void SampleMethod(object value) //"text" 
static void CallMethod(dynamic d) {
    SampleMethod(d); //執行期決定適用哪一個
}
CallMethod(10);
CallMethod(10.5m);
CallMethod(10L);
CallMethod("text");
  • 「決定方法、欄位名稱是什麼」的程序叫做 Binding (繫結、綁定),動態型別的 Binding 幾乎都發生在執行期間,Compiler 產生一段 IL 程式碼進行 Binding 找到目標再執行動作,稱為 Dynamic Binding
  • dynamic 在 IL 是一個標註 [Dynamic] 的 object
  • Dynamic 型別需要透過 Dynamic Binding 決定呼叫方法
//但有些編譯時期就能發現的錯誤
dynamic d = new object();
int invalid1 = "text".Substring(0, 1, 2, d); //沒有四個參數的組合
bool invalid2 = string.Equals<int>("foo", d); //無泛型Equals
string invalid3 = new string(d, "broken"); //沒有第二個參數是字串的建構式
char invalid4 = "text"[d, d]; //沒有兩個參數的 Indexer
  • 只要有 dynamic 參與的運算,結果也會是 dynamic,但有些例外
dynamic d = xxx;
d.ToStrion(); // dynamic
new SomeType(d) // SomeType
d is SomeType // true 或 false
d as SomeType // SomeType
  • dynamic 比 Reflection 更有彈性,不一定要真的做出類別,例如:DapperRow
  • ExpandoObject 既然要動態就動個痛快 - ExpandoObject
  • 使用 dynamic 簡化 Json.NET JObject 操作
  • DynamicObject 寫個 dynamic 變形蟲物件
  • C# 的功能實作可能落在 C# Compiler、CLR、Framework Libraries 三者。var 全靠 Compiler 完成,LINQ 需要 Compiler 跟 Framework Library 配合,dyanamic 則要三者合作實現。
    Framework 增加 DLR(Dynamic Language Runtime, System.Dynamic),需要第三方程式庫 Microsoft.CSharp (所以你如果沒用 dynamic 可以不參照),IL 部分多了 [Dynamic] Attribute。
    簡單的 dynamic text = "hello world"; string world = text.Substring(6); 會被編譯成用到 CSharpArgumentInfo、CallSiteBinder、CallSite 的一大段程式,細節瑣碎,有興趣可參考書中解析。
  • dynamic 與泛型
// 以下無效,不可使用
class DynamicSequence : IEnumerable<dynamic>
class DynamicListSequence : IEnumerable<List<dynamic>>
class DynamicConstraint1<T> : IEnumerable<T> where T : dynamic
class DynamicConstraint2<T> : IEnumerable<T> where T : List<dynamic>
// 以下 OK
class DynamicList : List<dynamic>
class ListOfDynamicSequences : List<IEnumerable<dynamic>>
IEnumerable<dynamic> x = new List<dynamic> { 1, 0.5 }.Select(x => x * 2);
  • dynamic 與擴充方法
    (書中 List 4.10 範例怪怪的,.NET Framework 應該沒有實作 TimeSpan * 2,要 .NET Core 2 才有實作乘運算子,而實測 List<dynamic> 的 .Any() 也不會出錯)
  • dynamic 與匿名函式
dynamic func = x => x * 2; //無效,無法判定要建立什麼委派
dynamic function = (Func<dynamic, dynamic>) (x => x * 2); //這樣可以
Console.Write(func(0.75)); 

dynamic source = new List<dynamic>{ 5, 2.75 };
dynamic result = source.Select(x => x * 2); //不行,不知 source 是 List

List<dynamic> source = new List<dynamic>{ 5, 2.75 };
Console.WriteLine(source.Select(x => x * 2)); //可執行,知道 source 是 List
IEnumerable<dynamic> query = source
    .AsQueryable()
	//CS1963 An expression tree may not contain a dynamic operation
    .Select(x => x * 2); 
  • dynamic 與 Explicit Interface Implementation
List<int> list1 = new List<int>();
//CS1061 'List<int>' does not contain a definition for 'IsFixedSize' and no extension method 'IsFixedSize' accepting a first argument of type 'List<int>' could be found 
Console.WriteLine(list1.IsFixedSize); 

IList list2 = list1;
Console.WriteLine(list2.IsFixedSize);
//RuntimeBinderException 'System.Collections.Generic.List<int>' 不包含 'IsFixedSize' 的定義
dynamic list3 = list1;
Console.WriteLine(list3.IsFixedSize);
  • 作者偏好靜態型別不愛 dynamic,好處是提早在編輯時發現問題、開發工具可幫較多忙、API 傳回值及參數更明確好規劃、效能較佳
  • dynamic 在某些時候可以取代 Reflection,省去搞 PropertyInfo、MethodInfo 的功夫

Optional Parameter

static void Method(int x, int y = 5, int z = 10)
{
    // x 必要, y, z 有預設值為選擇性
    // out/ref 參數不能有預設值
    // 預設值必須是編譯時期常數,接受 default(T),new ValueType() (如 Guid/CancellationToken)
    // 勿與 params 混用(雖然合法)
    Console.WriteLine("x={0}; y={1}; z={2}", x, y, z);
}
Method(1, 2, 3);
// 1 是 Positional Argument, y: 2 是 Named Argument
Method(1, y: 2);
// Named Argument 可不依順序
Method(x: 1, y: 2, z: 3);
Method(z: 3, y: 2, x: 1);
Method(1, 2);
Method(1, z: 3);
Method(1);
Method(x: 1);
  • 改版時更換參數名稱會造成不相容
public static Method(int x, int y = 5, int z = 10)
public static Method(int a, int b = 5, int c = 10)
  • 改版時改參數預設值,IL 中預設值寫在呼叫端(如下圖),故等呼叫端重新 Compile 才會生效。技巧: int 改用 int?,傳 null 由方法決定預設值。
  • Overloading 問題,避免以選擇性參數區別

COM Interoperability 改良

在 C# 4 之前,VB 在 COM 整合上比 C# 方便,C# 4 加入強化。

  • C# 4 之前,主機上必須有要 PIA (Primary Interop Assembly,廠商提供)且版本必須跟編譯用的相同。C# 4 起改為連結(Link) PIA (VS2010 Embed Interop Types 選項設為 True),PIA 必要部分嵌入 DLL,執行時不需 PIA,也不用版本完全一致。
  • 回傳 VARIANT 值改對映 dynamic,不需轉型至指定 DOM 型別也能存取:
var app = new Application { Visible = true };
app.Workbooks.Add();
Worksheet sheet = app.ActiveSheet;
Range start = sheet.Cells[1, 1];
Range end = sheet.Cells[1, 20];
sheet.Range[start, end].Value = Enumerable.Range(1, 20).ToArray();
  • 新加入的選擇性參數帶來救贖
//以前要這樣寫
doc.SaveAs2(ref fileName, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing, ref missing,
    ref missing, ref missing);
// C# 4
doc.SaveAs2(FileName: "demo2.docx");
  • Named Indexer
Application app = new Application { Visible = false };
//以前
object missing = Type.Missing;
SynonymInfo info = app.get_SynonymInfo("method", ref missing);
Console.WriteLine("'method' has {0} meanings", info.MeaningCount);
//C# 4
info = app.SynonymInfo["index"];
Console.WriteLine("'index' has {0} meanings", info.MeaningCount);

Generic Variance

泛型介面中的 T 不同時,何時可以轉換,何時不行?

// 可以相容
IEnumerable<string> strings = new List<string> { "a", "b", "c" };
IEnumerable<object> objects = strings;

// 不能相容
IList<string> strings = new List<string> { "a", "b", "c" };
//CS0266 Cannot implicitly convert type 'System.Collections.Generic.IList<string>' to 'System.Collections.Generic.IList<object>'. 
IList<object> objects = strings;
//原因:IEnumerable<T> 只輸出,IList<T> 可以 Add() 輸入,若允許轉換可能發生 List<string>.Add(object)

// 可以相容,stringAction 傳 string 到 objectAction,轉 object 沒問題
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction;
stringAction("Print me");

術語:

  • Convariance - 值只用於輸出
  • Contravariance - 值用於輸入
  • Invariance - 值同時用於輸入與輸出

Variance 只發生在 Interface 及 Delegate,不適用 Class 及 Struct,且是針對型別參數,故「IEnumerable<T> is covariant」的真正意思是「IEnumerable<T> is covariant in T」

public interface IEnumerable<out T> // Convariance
public delegate void Action<in T> // Contravariance
public interface IList<T> //Invariance
public TResult Func<in T, out TResult>(T arg) // T 是 Contravariance,TResult 是 Convariance

public delegate void InvalidCovariant<out T>(T input) //無效宣告
public interface IInvalidContravariant<in T> //無效宣告
{
    T GetValue();
}

public class SimpleEnumerable<T> : IEnumerable<T>
{ }
// 不能 SimpleEnumerable<string> 轉 SimpleEnumerable<object>,不適用在類別 
// 可以 SimpleEnumerable<string> 轉 IEnumerable<object>,因為 Convariance 只發生在介面或委派

註:我現在才發現 IEnumerable<T> 介面定義時真的有寫 out T:

要了解 Convariance、Contravariance 型別參數的介面或委派的轉換,要知道以下定義。

  • 涉及泛型 T 轉換,稱為 Variance Conversion
  • Variance Conversion 是一種 Reference Conversion,Reference Conversion 不改變涉及的值,只改變編譯期間的型別
  • Identity Conversion,轉換前後型別相同,例如:string x = (string)"test",將 dynamic 轉成 object (從 Compiler 角度有差別,但 Runtime 角度二者相同)

只有 Implicit Reference Conversion 跟 Identity Conversion 可成功

IEnumerable<string> to IEnumerable<object> //Implicit Reference Conversion, class to baseclass
IEnumerable<string> to IEnumerable<IConvertible> //Implicit Reference Conversion, class to interface
IEnumerable<IDisposable> to IEnumerable<object> //Implicit Reference Conversion, ref type to object

IEnumerable<object> to IEnumerable<string> // 不允許,object 到 string 是 Explicit Reference Conversion 
IEnumerable<string> to IEnumerable<Stream> // 不允許,轉不了
IEnumerable<int> to IEnumerable<IConvertible> // 不允許,int 可轉 IConvertible,但這是 Boxing Conversion,不是 Reference Convesion
IEnumerable<int> to IEnumerable<long> // 不允許,int 可轉 long,但它不是 Reference Convesion

若有多型別參數,一個一個看:

  • T 為 Covariant - 必須 Implicit Reference Conversion 或 Identity Conversion
  • T 為 Contravariant - 必須 Implicit Reference Conversion 或 Identity Conversion
  • T 為 Invariant - 必須 Implicit Reference
Func<object, int> to Func<string, int> //可以
// string -> object 為 Implicit Reference Conversion
// int -> int 為 Identity Conversion
Func<dynamic, string> to Func<object, IConvertible> //可以
// object -> dynamic 為 Identity Conversion
// string -> IConvertible 為 Implicit Reference Conversion
Func<string, int> to Func<object, int>  //不行
// object -> string 沒有 Implicit Conversion

My notes for C# in Depth part 5


Comments

Be the first to post a comment

Post a comment