Effective C# 18.定義最少與足夠的約束 Effective C# 18.定義最少與足夠的約束 (Always Define Constraints That Are Minimal and Sufficient)

Published on Tuesday, October 15, 2024

接下來的幾個做法都會與泛型相關,這個做法建議了泛型中使用約束的幾個方式,首先複習一下泛型的基礎知識與名詞。


開放 & 封閉泛型類型(open & close generic type) & 類型參數(type parameter)

下面這段程式碼是一個常見的泛型 Class 寫法,可以輸入一個類型參數(type parameter) TValue, 這個 TValue 只是個 placeholder 因此使用者可以傳入任意類型作為參數,所以可以稱 DictionaryStringKey<> 是一個開放泛型類型(open generic type)

class DictionaryStringKey<TValue> : Dictionary<String, TValue> { }

Type t = typeof(Dictionary<,>);  // 也是 open generic type

有開放類型那就有封閉泛型類型(close generic type)

Type t = typeof(DictionaryStringKey<Guid>); // close generic type

所以根據上面的描述可以得出定義為: type parameter 只有部分傳入還有其它空位等待傳入的泛型類型就是個開放泛型類型(open generic type)。 反之 type parameter 全部都定義完成就稱為封閉泛型類型(close generic type)

泛型類型定義(generic type definition)

泛型可以理解成一個模板,等到運行時才會把你指定的 type arguments 轉換成實際的機器碼,運行以下程式碼可以判斷出這個類型 是不是一個 generic type definition,還可以透過 GetGenericTypeDefinition 方法把這個類型的定義讀取出來。

public class Test
{
	public static void Main()
	{
		Console.WriteLine("\r\n--- Get the generic type that defines a constructed type.");
     
		Dictionary<string, Test> d = new Dictionary<string, Test>();

		Type constructed = d.GetType();
		DisplayTypeInfo(constructed);

		Type generic = constructed.GetGenericTypeDefinition();
		DisplayTypeInfo(generic);
	}

	private static void DisplayTypeInfo(Type t)
	{
		Console.WriteLine("\r\n{0}", t);
		Console.WriteLine("\tIs this a generic type definition? {0}",
			t.IsGenericTypeDefinition);
		Console.WriteLine("\tIs it a generic type? {0}",
			t.IsGenericType);
		Type[] typeArguments = t.GetGenericArguments();
		Console.WriteLine("\tList type arguments ({0}):", typeArguments.Length);
		foreach (Type tParam in typeArguments)
		{
			Console.WriteLine("\t\t{0}", tParam);
		}
	}
}

--- Get the generic type that defines a constructed type.

System.Collections.Generic.Dictionary`2[System.String,UserQuery+Test]
    Is this a generic type definition? False
    Is it a generic type? True
    List type arguments (2):
        System.String
        UserQuery+Test

System.Collections.Generic.Dictionary`2[TKey,TValue]
    Is this a generic type definition? True
    Is it a generic type? True
    List type arguments (2):
        TKey
        TValue

generic type definition 是一個相當重要的概念,首先編譯器會把它編譯成 IL 碼,要注意編譯器這個時候還沒有把我們要的 type arguments 帶入進去, 要等到 JIT 運行時才會傳入 type arguments 編譯出實際的機械碼,不過還會分成兩種情況,一個是傳入的是參考型別一個是值型別。

因為參考型別是傳入地址,所以 JIT 會幫所有參考型別建構共用機械碼,所以以下的程式碼在執行期間其實用的都是同一段機械碼。

List<string> stringList = new List<string>();
List<Stream> OpenFiles = new List<Stream>();
List<MyClassType> anotherList = new List<MyClassType>();

值型別不一樣 JIT 會幫每個值型別都建立不同的機械碼,所以以下的程式碼在執行期間都會額外建立單獨的機械碼,雖然這樣的設計會需要額外的成本 ,但是可以避免裝箱與拆箱的行為產生。

List<double> doubleList = new List<double>();
List<int> markers = new List<int>();
List<MyStruct> values = new List<MyStruct>();

接下來回到做法 18 的建議,這裡提到了約束這個概念,以下面這個泛型方法為例,這個方法會將傳入的參數進行比較所以傳入的類型需要實作 IComparable 這個介面,所以光是檢查邏輯是否有實做就佔了許多程式碼。

public static bool AreEqual<T>(T left, T right)
{
	if (left == null)
		return right == null;
	if (left is IComparable<T>)
	{
		IComparable<T> lval = left as IComparable<T>;
		if (right is IComparable<T>)
			return lval.CompareTo(right) == 0;
		else
			throw new ArgumentException( "Type does not implement IComparable<T>", nameof(right));
	}
	else // failure
	{
		throw new ArgumentException("Type does not implement IComparable<T>", nameof(left));
	}
}

有了約束之後就可以把程式碼精簡成下面這樣,這裡的 where 代表 T 必須要實作 IComparable 這個介面,所以編譯器可以直接假設傳入的參數 一定有實做 IComparable 介面就不用我們一一去檢查了。

public static bool AreEqual2<T>(T left, T right)
   where T : IComparable<T> =>
	   left.CompareTo(right) == 0;

如果沒有約束的話編譯器就無從判斷傳入的參數是什麼類型的,只能把它當成底層的 Object 類型,所以只能使用 Object 提供的那幾個基礎方法。

當然也可以添加多個約束,但是越多的約束會導致使用者的困擾所以建議是只約束需要的介面即可。

public static bool AreEqual3<T>(T left, T right)
   where T : IComparable<T>, IEquatable<T> =>
	   left.CompareTo(right) == 0;

Summary

這個做法複習了泛型的基本知識與名詞還有介紹了約束的概念,我們可以運用約束讓編譯器在編譯時期就先了解傳入的參數是什麼類型的, 並且可以大量減少檢查的邏輯,但並不是增加越多約束越好還是要視情況來添加約束。