這個做法比較動態類型與靜態類型各自的優勢,還有介紹如何使用 dynamic 將物件宣告成動態物件。

C# 一直以來都是靜態的語言,但同時也支持動態語言的特性讓我們在特定情況下獲得動態類型開發的好處。

下面這段程式碼使用了 dynamic 類型宣告了區域變數 value,第一次迭代時 value10 並且編譯器自己會知道該用 Int32 的方法版本, 第二次迭代 valueAA 並且編譯器自己會知道該用 String 的方法版本。

void Main()
{
	dynamic value;
	for (Int32 demo = 0; demo < 2; demo++)
	{
		value = (demo == 0) ? (dynamic)5 : (dynamic)"A";
		value = value + value;
		M(value);
	}
}
private static void M(Int32 n)
{
	Console.WriteLine("M(Int32): " + n);
}
private static void M(String s)
{
	Console.WriteLine("M(String): " + s);
}

上面那段程式碼能夠運行主要就是因為 dynamic 類型的物件可以跳過某些靜態類型的檢查,大部分情況下 dynamicobject 在行為上是相同的, 但是 dynamic 類型的物件 value 會延後到執行時期才決定實際的類型,接下來才能決定符號 + 實際要做什麼事情,評估該使用哪一個方法的過程其實跟泛型有點類似, 只不過泛型是在編譯時期就決定該使用哪個版本,動態類型則是要在執行時期才能決定使用哪個版本。

泛型同樣也是預設將類型參數 T 當成 object 類型,我們必須使用 where 子句對泛型加上約束來擴充類型參數 T 所擁有的方法,例如下面這個例子。

void Main()
{
	var e = new MyEmployee()
	{
		Name = "Allen"
	};
	var manager = new EmployeeManager<MyEmployee>();
	Console.WriteLine(manager.GetEmployeeName(e));
}

public class MyEmployee : IEmployee
{
	public string Name { get; set; }
	public string GetName()
	{
		return this.Name;
	}
}

	public interface IEmployee
{
	public string Name { get; set; }
	public string GetName();
}

public class EmployeeManager<T> where T : IEmployee
{
	public string GetEmployeeName(T employee)
	{
		return employee.GetName();
	}
}

但是類型參數 T 沒辦法透過泛型約束直接加上特定的方法,例如上面的例子我想要讓類型參數 T 能夠使用 GetName 方法,那我就要透過 另一個介面 IEmployee 來提供這個方法,我並沒有辦法直接用某種寫法讓類型參數 T 直接支援 GetName 方法,所以關鍵就是 我們必須提早知道類型參數 T 的實際的類型才有辦法做出更多的功能。

假如我們想要寫一個方法它會依賴操作子 + 就很難做到,很明顯 object 類型別沒有覆寫操作子 +, 所以是不可能把兩個編譯器預設的 object 相加起來,除非我們要加上一個有覆寫操作子 +的約束才有可能相加,所以下面這段寫法會報錯。

public class EmployeeManager<T>
{
	public T Add(T n)
	{
		return n + n;
	}
}

這個要到 .NET 7 以後才有提供 IAdditionOperators 介面才能做到類似的功能。

void Main()
{
	var e = new MyEmployee()
	{
		Name = "Allen",
		Age = 30
	};
	var manager = new EmployeeManager<int>();
	Console.WriteLine(manager.Add(e.Age, e.Age));
}

public class MyEmployee : IEmployee
{
	public string Name { get; set; }
	public int Age { get; set; }
}

public interface IEmployee
{
	public string Name { get; set; }
	public int Age { get; set; }
}

public class EmployeeManager<T> where T : IAdditionOperators<T, T, T>
{
	public T Add(T left, T right)
	{
		return left + right;
	}
}

但如果今天用的是 dynamic 就能很輕鬆的做到,因為它能夠跳過針對類型的編譯檢查,所以你可以認為 dynamic 就是任何可能的類型, 所以只要未來是 int 就會呼叫 int 的操作子 + 將兩個數值相加,如果未來是 string 就會呼叫 string 的操作子 + 將兩個字串聯起來。

public static dynamic Add(dynamic left, dynamic right)
{
	return left + right;
}

因為只要確認執行期別有覆寫操作子 + 就好,所以下面這些寫法除了最後一個都能正確運行,並且宣告 answer 也要使用 dynamic 或是 var。

void Main()
{
	dynamic answer = Add(5, 5);
	answer = Add(5.5, 7.3);
	answer = Add(5, 12.3);
	answer = Add("Hello, ", "World!");
	answer = Add(new MyClass(), new MyClass());
}

public class MyClass {}

雖然動態型別很方便但是也是有缺點,第一個就上面例子中 MyClass 相加的那段程式碼,很明顯我們在寫程式的時候就已經知道不可能可以相加, 並且編譯器因為動態型別的原因所以它會跳過檢查,一直到執行期別才發現錯誤,所以缺點之一就是編譯器沒辦法在我們寫程式的途中提供協助, 另一個缺點就是綁定真實型別的過程需要額外的運算和檢查,所以會影響程式運行的效率。

如果你知道編譯時期的參數類別那可以透過泛型與傳入 Lambda 表達式達到類似 dynamic 的效果, 但實際上這種寫法會產生很多沒有意義的程式碼,例如我們還要再額外提供一個處理加法的 AddMethod 才能運作。

void Main()
{
	var lambdaAnswer = Add(5, 5, (a, b) => a + b);
	var lambdaAnswer2 = Add(5.5, 7.3, (a, b) => a + b);
	var lambdaAnswer3 = Add(5, 12.3, (a, b) => a + b);
	var lambdaLabel = Add("Here is ", "a label", (a, b) => a + b);
	var finalLabel = Add("something", 3, (a, b) => a + b.ToString());
}

public static TResult Add<T1, T2, TResult>(T1 left, T2 right, Func<T1, T2, TResult> AddMethod)
{
	return AddMethod(left, right);
}

或者也可以透過建立 expression tree 來產生一個處理加法的委派,但這種方法每次運行加法都要建立一個委派所以不是很有效率。

public static TResult AddExpression<T1, T2, TResult>(T1 left, T2 right)
{
	var leftOperand = Expression.Parameter(typeof(T1), "left");
	var rightOperand = Expression.Parameter(typeof(T2), "right");
	var body = Expression.Add(leftOperand, rightOperand);
	var lambda = Expression.Lambda<Func<T1, T2, TResult>>(body, leftOperand, rightOperand);
	return lambda.Compile()(left, right);
}

可以透過一個靜態欄位將這個委派緩存起來能夠稍微改進效率,但這種寫法在使用上當處理兩個不同型別的加法時靈活度反而沒有直接用 dynamic 來的好, 因為要處理兩個不同型別的加法反而要在程式碼加上不少的轉換邏輯,那不如把這段工作交給 dynamic 自行處理。

public static class BinaryOperator<T1, T2, TResult>
{
	static Func<T1, T2, TResult> compiledExpression;
	public static TResult Add(T1 left, T2 right)
	{
		if (compiledExpression == null)
			createFunc();
		return compiledExpression(left, right);
	}
	private static void createFunc()
	{
		var leftOperand = Expression.Parameter(typeof(T1), "left");
		var rightOperand = Expression.Parameter(typeof(T2), "right");
		var body = Expression.Add(leftOperand, rightOperand);
		var lambda = Expression.Lambda<Func<T1, T2, TResult>>(body, leftOperand, rightOperand);
		compiledExpression = lambda.Compile();
	}
}

權衡之下還是使用單一個類型參數等之後在轉型比較實用。

public static class BinaryOperators<T>
{
	static Func<T, T, T> compiledExpression;
	public static T Add(T left, T right)
	{
		if (compiledExpression == null)
			createFunc();
		return compiledExpression(left, right);
	}
	private static void createFunc()
	{
		var leftOperand = Expression.Parameter(typeof(T), "left");
		var rightOperand = Expression.Parameter(typeof(T), "right");
		var body = Expression.Add(leftOperand, rightOperand);
		var adder = Expression.Lambda<Func<T, T, T>>(body, leftOperand, rightOperand);
		compiledExpression = adder.Compile();
	}
}

Summary

當方法需要支援多種類型使用 dynamic 會比較方便,但是靜態型別的好處就是速度比動態型別快的多,因此如果類型有限可以使用 expression tree 建立委派 來達到更快的速度,但還是建議首選介面搭配泛型,畢竟速度是最快的同時又能獲得編譯器檢查與最佳化的好處。