C# 使用 Binder 類自定義反射 update 2013.1.26


在利用 Type 類進行反射時,經常用到 GetMethod 和 GetProperty 反射方法與屬性,或者使用 InvokeMember 直接調用類型成員。這些方法都具有一個 System.Reflection.Binder 類型的 binder 參數,而這個參數一般都是設置為 null 的,很少使用。

事實上,這個 binder 參數是很強大的,它可以幾乎完全控制反射的工作方式(這里用幾乎,是因為它受到了 RuntimeType 實現時的一些限制),只不過默認情況下使用的 System.DefaultBinder 類已經足夠的使用了,因此不用太過於在意這個參數。

下面將會以我實現的 PowerBinder 類作為例子,解釋 Binder 類到底是做什么的,以及如何實現自己的 Binder 類。PowerBinder 的實現與 DefaultBinder 的邏輯是基本相同的,區別在於添加了對泛型方法和強制類型轉換的支持,同時進行了部分改進,下面給出一個與 DefaultBinder 對比的例子:

class TestClass
{
	public static void TestMethod(int value) { }
	public static void TestMethod2<T>(T value) { }
}
Type type = typeof(TestClass);
Console.WriteLine(type.GetMethod("TestMethod", new Type[] { typeof(long) }));
Console.WriteLine(type.GetMethod("TestMethod", BindingFlags.Static | BindingFlags.Public, PowerBinder.CastBinder,
	new Type[] { typeof(long) }, null));
Console.WriteLine(type.GetMethod("TestMethod2", new Type[] { typeof(string) }));
Console.WriteLine(type.GetMethod("TestMethod2", BindingFlags.Static | BindingFlags.Public, PowerBinder.DefaultBinder,
	new Type[] { typeof(string) }, null));

這個例子是分別用 DefaultBinder 和 PowerBinder 反射獲取 TestClass 類的方法,得到的結果如下所示:

null
Void TestMethod(Int32)
null
Void TestMethod2[String](System.String)

可以看到,有了泛型方法和強制類型轉換的支持,在反射調用方法時會更加靈活方便,而且自定義 Binder 類的好處是很容易重用,而且能夠使用 .Net 提供的相關接口。

一、Binder 類介紹

首先來看 Binder 類是如何控制反射的工作方式的。它在 MSDN 中的解釋是“從候選者列表中選擇一個成員,並執行實參類型到形參類型的類型轉換。”,也就是說在執行反射時,會由 Type 類選出一組可能的 MethodBase、PropertyInfo 或 FieldInfo,然后由 Binder 類來決定到底要使用哪個方法、屬性或字段,或者干脆哪個都不選;而且實參到形參的類型轉換(會在 Invoke 時使用)也是由 Binder 類來控制的,所以說它可以幾乎完全控制反射的工作方式——唯一的不足就是候選者列表是由 Type 類提供的。

這里列出了 Binder 類需要重寫的方法和簡要的說明,每個方法的參數的具體解釋可以參考 MSDN

  1. FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, Object value, CultureInfo culture) 方法:
    當利用 Type.InvokeMember 方法訪問字段時,先由 InvokeMember 方法選出與 name 和 bindingFlags 匹配的字段,然后由 BindToField 選擇與 value 最匹配的字段。
  2. MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref Object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out Object state) 方法:
    當利用 Type.InvokeMember 方法調用方法或訪問屬性時,先由 InvokeMember 方法選出與 name 和 bindingFlags 匹配的方法(屬性則使用相應的 SetMethod 或 GetMethod),然后由 BindToMethod 選擇與 args 最匹配的方法。這個方法非常復雜,由於 InvokeMember 方法允許通過參數名稱指定參數,因此參數的順序和個數與方法的形參可能並不匹配,需要由 BindToMethod 方法將參數數組調整為正確的順序,並且要求 ReorderArgumentArray 方法配合附加的 out state 參數,將被改變的參數數組順序還原為被傳入時的順序。
  3. Object ChangeType(Object value, Type type, CultureInfo culture) 方法:
    這個方法用於在利用反射設置值時(例如 FieldInfo.SetValue 和 MethodBase.Invoke),對類型進行轉換。MSDN 建議只進行擴寬強制,這樣不會丟失數據,但也可以實現自己的邏輯。
  4. void ReorderArgumentArray(ref Object[] args, Object state) 方法:
    這個方法是與 BindToMethod 成對使用的,根據 BindToMethod 的 out state 參數,還原 args 的參數順序。兩個方法的實現也必須相對應。
  5. MethodBase SelectMethod(BindingFlags bindingAttr, MethodBase[] match, Type[] types, ParameterModifier[] modifiers) 方法:
    當利用 Type.GetMethod 反射方法時,先由 GetMethod 方法選出與 name、bindingFlags、callConvention 和參數數量匹配的方法,然后由 SelectMethod 選擇與 types 最匹配的方法。
  6. PropertyInfo SelectProperty(BindingFlags bindingAttr, PropertyInfo[] match, Type returnType, Type[] indexes, ParameterModifier[] modifiers) 方法:
    當利用 Type.GetProperty 反射屬性時,先由 GetProperty 方法選出與 name、bindingFlags 和參數數量匹配的屬性,然后由 SelectProperty 選擇與 returnType 和 indexes 最匹配的屬性。

二、支持泛型方法和強制類型轉換的 PowerBinder 類

接下來就是詳細解釋 PowerBinder 是如何實現每個方法的。

2.1 實現 BindToField 方法

先再次列出方法簽名,以方便參考: FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, Object value, CultureInfo culture)。

這個方法其實很少被使用,僅當父類和子類定義了同名字段時才可能使用(否則根本不能定義同名的字段)。下面是 BindToField 方法的實現流程圖,在這個流程圖中也顯示出了 RuntimeType 類為我們做的一些工作。

這個方法的實現還是很簡單的,不過有些地方需要詳細解釋一下。

  • 如果設置了 GetField 標志,RuntimeType 會將 value 設置為一個特殊的值 Empty.Value(這是一個內部類),所以這時候 value 是完全不可用的。
  • 如果設置了 SetField 標志而 value == null,這時只要求 FieldType 是引用類型即可,即任何可以接受 null 值的類型。
  • 如果按照 value 的類型篩選字段仍然得到多個可選字段,可以嘗試從匹配的字段中找到最匹配的那個。例如有兩個字段,其類型分別是 int 和 long,與 short 類型的值更匹配的顯然是 int 而不是 long。這一匹配方式在后面也會多次用到。

至於如何選擇定義在子類中的字段,可以簡單的按照 FieldInfo 被定義的深度來選擇,深度比較深的就意味着是在子類中定義的。

.Net 4.0 中的 RuntimeType 類的實現有個小小的問題,現在假設類型 C 具有一個 string[] 類型的字段 F,當想通過 InvokeMemver 將 F[1] 設置為 "b" 時(一種很少見的用法,可能很多人都不知道),可以使用下面的代碼(更多信息請參見 MSDN):

typeof(C).InvokeMember("F", BindingFlags.SetField, null, c, new Object[] {1, "b"}, null, null, null);

但是,此時 BindToField 方法的 value 參數得到的不是要設置的值 "b",也不是 string[] 類型的值,而是那個索引 1,這就導致 BindToField 是不可能通過類型選擇合適的字段的(甚至可能選擇錯誤)。下面就是一個例子:

class TestClass
{
	public string[] TestField = new string[] { "XXX" };
}
class TestSubClass
{
	public new int TestField;
}

當調用

typeof(TestSubClass).InvokeMember("TestField", BindingFlags.SetField, null, new TestSubClass(), new object[] { 0, "XXX2" }, null, null, null);

時,就不能正確的反射到 TestClass.TestField,會拋出 ArgumentException。不過還好,這個問題幾乎不可能遇到,即使真的出現這種問題,先獲取字段對應的數組,再獲取或設置數組指定索引的值就可以完美解決了。

下面是實現的代碼:

public override FieldInfo BindToField(BindingFlags bindingAttr, FieldInfo[] match, object value,
	CultureInfo culture)
{
	int idx = 0;
	Type valueType = null;
	if (value != null) { valueType = value.GetType(); }
	bool setField = (bindingAttr & BindingFlags.SetField) != 0;
	if (setField)
	{
		// 在設置 SetField 標志時,根據 value 的類型進行選擇。
		for (int i = 0; i < match.Length; i++)
		{
			if (CanChangeType(match[i].FieldType, valueType))
			{
				match[idx++] = match[i];
			}
		}
		if (idx == 0)
		{
			// 沒有可匹配的字段。
			return null;
		}
		else if (idx > 1 && valueType != null)
		{
			// 多個可匹配字段,嘗試尋找類型匹配的最好的字段。
			int len = idx;
			idx = 1;
			for (int i = 1; i < len; i++)
			{
				// 嘗試進一步匹配字段類型。
				int cmp = FindMostSpecificType(match[0].FieldType, match[i].FieldType, valueType);
				if (cmp == 0)
				{
					match[idx++] = match[i];
				}
				else if (cmp == 2)
				{
					match[0] = match[i];
					idx = 1;
				}
			}
		}
	}
	else
	{
		idx = match.Length;
	}
	// 多個可匹配字段,尋找定義深度最深的字段。
	int min = 0;
	bool ambig = false;
	for (int i = 1; i < idx; i++)
	{
		// 比較定義的層級深度。
		int cmp = CompareHierarchyDepth(match[min], match[i]);
		if (cmp == 0)
		{
			ambig = true;
		}
		else if (cmp == 2)
		{
			min = i;
			ambig = false;
		}
	}
	if (ambig)
	{
		throw ExceptionHelper.AmbiguousMatchField();
	}
	return match[min];
}

其中用到的 FindMostSpecificType(Type type1, Type type2, Type type) 方法,是在兩個類型 type1 和 type2 中,選擇與 type 最接近的類型。例如在類型 short 和 int 中,與 long 最接近的顯然是 int 類型,而與 sbyte 最接近的則是 short 類型。 具體的做法,就是判斷 type1 和 type2 中哪個可以從 type 類型隱式轉換而來,沒有數據的丟失顯然是更好的;如果都可以從 type 類型隱式轉換而來,那么就選擇 type1 和 type2 中更窄的那個(更接近 type);如果都不可以從 type 類型隱式轉換,那么就選擇更寬的那個,以減少數據丟失。

2.2 實現 BindToMethod 方法

方法的簽名為 MethodBase BindToMethod(BindingFlags bindingAttr, MethodBase[] match, ref Object[] args, ParameterModifier[] modifiers, CultureInfo culture, string[] names, out Object state)。

這個方法是重寫 Binder 類時最復雜的方法,它需要處理的情況非常多,包括參數名映射,可選參數、params 參數和泛型方法。我將 BindToMethod 方法的實現分成了下面的五個步驟。為了簡便起見,這里與 System.DefaultBinder 一樣不對 modifiers 參數進行處理(它一般都是用於 COM 組件的)。下面就是 BindToMethod 方法的實現流程圖,雖然看起來不是很復雜,但其中的每一個步驟都需要做很多的工作。

2.2.1 處理 names 參數

names 參數允許參數不按順序傳入,所以首先要對 names 參數進行檢查,要求 names 中不能有同名參數。在 DefaultBinder 並沒有做這個檢查,所以當存在同名參數時,會出現詭異的 IndexOutOfRangeException。

接下來根據 names 參數調整參數的位置,就是在方法的參數列表中尋找與 names 中的名稱相同的參數,直到所有參數名稱都被匹配(如果有未被匹配的參數名稱,那么認為這個方法就不是想要的),在這里定義映射 map 來保存參數與 names 的匹配關系:如果 names[i] == params[j].Name,則 map[j] = i。

以方法

void TestMethod(int value1 = 11, int value2 = 22, int value3 = 33)

舉例來說:

  1. 若 names = null, 則 map = {0, 1, 2},表示參數都是按順序傳遞的。
  2. 若 names = {"value2", "value1", "value3"}, 則 map = {1, 0, 2}。
  3. 若 names = {"value3"}, 則 map = {1, 2, 0},即 names 的個數小於參數的個數時,剩余的參數會按順序傳遞。

具體的實現為:

private static bool CreateParamOrder(int[] paramOrder, ParameterInfo[] parameters, string[] names)
{
	if (names.Length > parameters.Length)
	{
		return false;
	}
	for (int i = 0; i < paramOrder.Length; i++)
	{
		paramOrder[i] = -1;
	}
	// 找到與參數名稱對應的參數索引,names.Length <= parameters.Length。
	for (int i = 0; i < names.Length; i++)
	{
		int j;
		for (j = 0; j < parameters.Length; j++)
		{
			if (string.Equals(parameters[j].Name, names[i], StringComparison.Ordinal))
			{
				paramOrder[j] = i;
				break;
			}
		}
		// 未找到的參數名稱,匹配失敗。
		if (j == parameters.Length)
		{
			return false;
		}
	}
	// 依次填充剩余的 args 的參數順序。
	int idx = names.Length;
	for (int i = 0; i < paramOrder.Length; i++)
	{
		if (paramOrder[i] == -1)
		{
			paramOrder[i] = idx++;
		}
	}
	return true;
}

2.2.2 處理泛型方法

接下來就是對泛型方法的支持了,如果函數簽名是 TestMethod<T>,這里需要將開放的泛型參數 T 替換為合適的類型,以得到相應的封閉泛型方法(例如 TestMethod<int>)。如果泛型參數 T 只對應一個參數 p,把 p 的類型作為 T 的類型即可。如果對應多個參數 p1, p2 ... pn,則要選擇 pi 的類型,使得其他參數的類型都可以隱式轉換為 pi 的類型。如果沒有對應任何參數,那么顯然是不能推導出類型實參的,直接返回。

如果泛型參數 T 對應着兩個參數 pi 和 pj,其中 p1, p2 ... pn 都可以隱式轉換為 pi 和 pj 的類型,那么泛型參數 T 的類型既可以選擇 pi 的類型,也可以選擇 pj 的類型,但到底使用哪個,程序是不能確定的,因此要求類型實參的推導必須是唯一的。

需要注意的是,這里使用的都是隱式類型轉換,而不是顯式類型轉換,這是由於顯示類型轉換很容易導致找不到唯一的類型實參的推導,因此只遵循通常的原則。

2.2.3 根據參數類型篩選

然后就是根據參數類型進行過濾,即依次比較 params[i].ParameterType 是否可以從 args[map[i]] 的類型轉換而來。而具體的比較又要分為三種情況分別討論,

  1. params.Length > args.Length 這種情況意味着部分參數沒有給出,因此要求沒有給出值的參數(map[i] >= args.Length)具有默認值(DefaultValue != DBNull.Value),而且同時指定了 BindingFlags.OptionalParamBinding 標志。特別的,若最后一個參數是 params 參數,是沒有默認值的,需要特殊處理一下。
    System.DefaultBinder 類在這里有個問題,就是要求默認參數總是在參數列表的末尾,即使是使用 names 更改參數順序也不允許默認參數出現在參數列表的中間。拿之前定義的 TestMethod 舉例來說,若傳入 name = {"value2"}, args = {1},System.DefaultBinder 會拋出 IndexOutOfRangeException。在我實現的 PowerBinder 中,則允許默認參數出現在任意位置。
  2. params.Length < args.Length 這種情況意味着給出的參數多於方法的參數,即方法必須包含 params 參數,這時就需要檢查 args 的額外參數值能否強制類型轉換到 params 參數的基礎類型。
  3. params.Length == args.Length 這就是最常見的情況,但是要根據最后一個參數是否是 params 參數進行額外的判斷,因為參數值既可以是數組的一個元素,也可以是只包含一個元素的數組。

具體的實現代碼如下:

private bool CheckMethodParameters(MatchInfo method, Type[] types, bool optionalParamBinding)
{
	if (method == null)
	{
		return false;
	}
	int len = method.Parameters.Length;
	if (len == 0)
	{
		// 判斷包含變量參數的方法。
		return types.Length == 0 || (method.Method.CallingConvention & CallingConventions.VarArgs) != 0;
	}
	else if (len > types.Length)
	{
		// 方法形參過多,要求指定可選參數綁定。
		if (!optionalParamBinding)
		{
			return false;
		}
		// 參數必須有默認值,最后一個參數可能是 params 參數,因此稍后進行檢查。
		int i = 0;
		for (; i < len - 1; i++)
		{
			if (method.ParamOrder[i] >= types.Length && method.Parameters[i].DefaultValue == DBNull.Value)
			{
				return false;
			}
		}
		// 檢查最后一個參數是否有默認值,或者是 params 參數。
		if (method.ParamOrder[i] >= types.Length && method.Parameters[i].DefaultValue == DBNull.Value)
		{
			if ((method.ParamArrayType = GetParamArrayType(method.Parameters[i])) == null)
			{
				return false;
			}
		}
		// 檢查其它參數是否可以進行類型轉換。
		return CheckParameters(method, types, types.Length);
	}
	else if (len < types.Length)
	{
		len--;
		// 方法形參過多,要求具有 params 參數。
		if ((method.ParamArrayType = GetParamArrayType(method.Parameters[len])) == null)
		{
			return false;
		}
		// 檢查參數是否可以進行類型轉換。
		for (int i = len; i < types.Length; i++)
		{
			if (!CanChangeType(method.ParamArrayType, types, method.ParamOrder[i]))
			{
				return false;
			}
		}
		return CheckParameters(method, types, len);
	}
	else
	{
		// 參數數量相等。
		len--;
		if ((method.ParamArrayType = GetParamArrayType(method.Parameters[len])) != null)
		{
			// 判斷是否需要展開 params 參數。
			if (!CanChangeType(method.ParamArrayType, types, method.ParamOrder[len]))
			{
				// 不需要展開 params 參數。
				method.ParamArrayType = null;
			}
			else
			{
				// 需要展開 params 參數。
				if (!CanChangeType(method.ParamArrayType, types, method.ParamOrder[len]))
				{
					return false;
				}
			}
		}
		else
		{
			// 沒有 params 參數。
			len++;
		}
		return CheckParameters(method, types, len);
	}
}

2.2.4 進一步匹配方法

通過上面的參數類型匹配,可能找到多個合適的方法,那么現在就需要在這些方法中,找到最合適的那個,其基本思想就是看哪個函數的簽名與 args 的類型最為接近,實現起來跟 FindMostSpecificType 接近,只不過需要同時考慮多個類型。

如果參數類型同樣接近,那么類型特化的方法總是優於泛型方法,子類定義的方法總是優於父類定義的方法(通過比較層級深度)。

2.2.5 保存與調整參數順序

由於參數的順序需要根據 names 或默認參數進行調整,所以需要更改參數數組以匹配方法的簽名,在這之前則需要保存舊的參數順序,以用於之后的 ReorderArgumentArray 方法還原參數數組。這里為了簡便起見,直接將參數數組復制一份(淺復制)保存,這樣還原的時候直接替換就可以了。

對參數數組的調整首先要根據 names 調整順序,接下來對默認參數和 params 參數進行處理,方式則類似於 2.2.3 中匹配參數類型,只不過是需要將多的參數包裝為數組,或者將缺少的參數使用默認值補齊。實現的代碼如下所示:

private static void UpdateArgs(MatchInfo match, ref object[] args, bool orderChanged, out object state)
{
	// 最簡單的參數完全不需要調整的情況。
	if (match.Parameters.Length == 0 ||
		(match.Parameters.Length == args.Length && match.ParamArrayType == null && !orderChanged))
	{
		state = null;
		return;
	}
	// 保存舊的參數狀態。
	object[] oldArgs = args;
	state = oldArgs;
	args = new object[match.Parameters.Length];
	int end = match.Parameters.Length - 1;
	// 根據名稱調整參數順序,同時使用默認值填充剩余參數。
	for (int i = match.ParamArrayType == null ? end : end - 1; i >= 0; i--)
	{
		if (match.ParamOrder[i] < oldArgs.Length)
		{
			args[i] = oldArgs[match.ParamOrder[i]];
		}
		else
		{
			args[i] = match.Parameters[i].DefaultValue;
		}
	}
	if (match.Parameters.Length >= oldArgs.Length)
	{
		// 對 params 參數進行判斷。
		if (match.ParamArrayType != null)
		{
			Array paramsArray = null;
			if (match.ParamOrder[end] < oldArgs.Length)
			{
				// 最后一個參數是只有一個元素的數組。
				paramsArray = Array.CreateInstance(match.ParamArrayType, 1);
				paramsArray.SetValue(oldArgs[match.ParamOrder[end]], 0);
			}
			else
			{
				// 最后一個參數是空數組。
				paramsArray = Array.CreateInstance(match.ParamArrayType, 0);
			}
			args[end] = paramsArray;
		}
	}
	else
	{
		// 參數過多,將多余的參數包裝為一個數組。
		if ((match.Method.CallingConvention & CallingConventions.VarArgs) == 0)
		{
			Array paramsArray = Array.CreateInstance(match.ParamArrayType, oldArgs.Length - end);
			for (int i = 0; i < paramsArray.Length; i++)
			{
				paramsArray.SetValue(oldArgs[match.ParamOrder[i + end]], i);
			}
			args[end] = paramsArray;
		}
	}
}

2013.1.26 更新:在后來對 PowerBinder 的一些測試過程中,發現了現有算法對一些參數順序被更改的情況不能正確處理,例如下面的類:

class TestClass
{
	public static string TestMethod(int ivalue, string svalue);
	public static string TestMethod(string svalue, int ivalue, string evalue = "str");
}

兩個方法重載的區別就是參數的順序,和第二個方法包含一個可選參數。根據通常的想法,下面的代碼:

typeof(TestClass).InvokeMember("TestMethod", 
	BindingFlags.Static | BindingFlags.Public | BindingFlags.OptionalParamBinding | BindingFlags.InvokeMethod, 
	PowerBinder.DefaultBinder, null, new object[] { "ff", 10 }, null, null, new string[] { "svalue", "ivalue" });

調用的方法應該是第一個,即 TestMethod(int ivalue, string svalue),因為它可以與參數完全匹配。但事實上,由 PowerBinder 選擇出來的反而是第二個方法,分析代碼發現,是在 FindMostSpecific 方法中對參數類型進行進一步匹配時,使用的是這樣的代碼:

FindMostSpecificType(match1.Parameters[i].ParameterType, match2.Parameters[i].ParameterType, types[i])

其中並沒有沒有考慮到參數順序的影響,所以根據參數順序的不同,選擇的結果也是不同的,某些情況下就會導致錯誤。這里要修改也比較容易,因為已經有 ParamOrder 記錄參數順序的映射,再定義一個逆映射(即若 map[i] = j,則 revMap[j] = i),這樣才能夠保證比較的的確是對應的參數,如下面的代碼所示:

Type type1, type2;
// 得到 types[i] 實際對應的方法參數。
int idx = match1.ParamOrderRev[i];
if (match1.ParamArrayType != null && idx >= p1Len)
{
	type1 = match1.ParamArrayType;
}
else
{
	type1 = match1.Parameters[idx].ParameterType;
}
idx = match2.ParamOrderRev[i];
if (match2.ParamArrayType != null && idx >= p2Len)
{
	type2 = match2.ParamArrayType;
}
else
{
	type2 = match2.Parameters[idx].ParameterType;
}

這樣的處理在 System.DefaultBinder 中是存在的,但是它並沒有使用逆映射,而是使用原先的 ParamOrder,不知道這是一個 Bug,還是有什么我忽略掉的地方。

除此之外,在完成參數比較之后,如果所有參數都不能比較出優劣(注意,不能是部分參數第一個方法好,部分參數第二個方法好),可以比較兩個方法的參數數量,數量較少的方法一定更合適。

而在對泛型方法的支持上,現在只能支持直接使用泛型參數 T 作為參數類型,對於 T[],IList<T> 之類的情況是不能處理的。要想支持這些情況需要做很多工作,如果確定要做的話,我會另寫一篇單獨說明這個問題。

2.3 實現 ChangeType 方法

這個方法用於實現類型轉換,它的邏輯需要和 BindToField 和 BindToMethod 相匹配,即如果 BindToXXX 方法只選擇可以隱式類型轉換的類型,那么 ChangeType 同樣只需要處理隱式類型轉換;如果 BindToXXX 方法對現實類型轉換提供支持,ChangeType 也必須提供同樣的支持。

System.DefaultBinder 只支持內置的隱式類型轉換,所以直接拋出 NotSupportedException 就完成工作了。而我的 PowerBinder 支持完整的隱式類型轉換和顯式類型轉換(包括對 Nullable<T>,枚舉和自定義類型轉換)的支持,因此實現起來會復雜很多,但其原理已經在之前的 C# 判斷類型間能否隱式或強制類型轉換中闡述了,所以這里就不再詳細說明,可以自行看源代碼:

public override object ChangeType(object value, Type type, CultureInfo culture)
{
	if (allowCast)
	{
		return ConvertExt.ChangeType(value, type, culture);
	}
	// 隱式類型轉換。
	if (type.IsByRef)
	{
		type = type.GetElementType();
	}
	// 總是可以轉換為 Object。
	if (type.TypeHandle.Equals(typeof(object).TypeHandle))
	{
		return value;
	}
	// 對 Nullable<T> 的支持。
	bool nullalbe = TypeExt.NullableAssignableFrom(ref type);
	if (value == null)
	{
		if (!nullalbe && type.IsValueType)
		{
			throw ExceptionHelper.CannotCastNullToValueType();
		}
		return null;
	}
	if (type.IsInstanceOfType(value))
	{
		return value;
	}
	// 檢測用戶定義類型轉換。
	RuntimeTypeHandle conversionHandle = type.TypeHandle;
	RuntimeTypeHandle valueTypeHandle = value.GetType().TypeHandle;
	ConversionMethod method;
	if (ConversionCache.GetTypeOperators(valueTypeHandle).TryGetValue(conversionHandle, out method) &&
		(method.ConversionType & ConversionType.ImplicitTo) == ConversionType.ImplicitTo)
	{
		return MethodInfo.GetMethodFromHandle(method.ToMethod).Invoke(null, new object[] { value });
	}
	if (ConversionCache.GetTypeOperators(conversionHandle).TryGetValue(valueTypeHandle, out method) &&
		(method.ConversionType & ConversionType.ImplicitFrom) == ConversionType.ImplicitFrom)
	{
		return MethodInfo.GetMethodFromHandle(method.FromMethod).Invoke(null, new object[] { value });
	}
	return value;
}

2.4 實現 ReorderArgumentArray 方法

這個方法是最簡單的,實現的方式也在 2.2 節實現 BindToMethod 方法中說明了,這里直接略過。

2.5 實現 SelectMethod 方法

這個方法也沒有必要詳細說明,因為它的實現已經完整包含在 BindToMethod 中了,只不過不必考慮 names 參數而已,可以認為是“2.2.2 處理泛型方法”,“2.2.3 根據參數類型篩選”和“2.2.4 進一步匹配方法”這三節的內容的組合。

不過,這里還是有些細節問題。在 DefaultBinder 中,SelectMethod 是不會考慮可選參數、params 參數和泛型方法的,我的 PowerBinder 決定要加入對他們的支持。但是在 RuntimeType 中,會對方法的參數數量進行初步篩選,如果沒有設置 BindingFlags.InvokeMember、BindingFlags.CreateInstance、BindingFlags.GetProperty 或 BindingFlags.SetProperty 之一的話,或過濾掉所有參數數量不相等的方法。因此,如果希望使用 PowerBinder 得到可選參數或 params 參數,需要設置以上的四個標志之一才可以,當然,BindingFlags.OptionalParamBinding 也是不能忘記的。

2.6 實現 SelectProperty 方法

相對於方法的選擇,屬性的選擇簡單了很多,只要考慮屬性的類型和索引參數就可以了,可選參數、params 參數和泛型等復雜的東西全部與屬性無關。其流程圖如下所示:

其中用到的屬性類型匹配類似於 BindToField 中的實現,索引參數的匹配則類似於方法參數的匹配,這里就不詳細解釋了。

以上就是 PowerBinder 的實現原理,從 Binder 類的實現過程中,也可以看到反射的效率為什么這么低下——需要由 RuntimeType 類選出特定類型的所有字段、屬性或方法,然后根據名稱進行第一次過濾,再由 Binder 類通過各種復雜的判斷才能夠得到反射的結果。

PowerBinder 類包含了兩個靜態屬性:DefaultBinder 和 CastBinder,其中 DefaultBinder 不支持強制類型轉換,CastBinder 則提供了對強制類型轉換的支持,泛型方法和用戶自定義類型轉換是兩個類支持的,所有源代碼可見 PowerBinder.cs


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM