這里所謂的“泛型方法的類型推斷”,指的是根據已有的方法實參的類型,推斷出泛型方法的類型實參。例如一個泛型方法 void Method<T>(T[] args),如果我給出方法實參類型是 int[],那么希望能夠推斷出 T = int。
這個問題是我在測試上一篇隨筆《C# 使用 Binder 類自定義反射》中的類時發現的,當時為了能夠讓 PowerBinder 支持泛型方法綁定,完成了一些簡單的類型推斷工作,但是它只能支持直接使用泛型參數 T 作為參數類型,對於 T[],IList<T> 這種復雜一些的情況是不能處理的。
或者舉個復雜點的例子,對於下面的泛型方法定義:
void Method<T>(IList<T> a, params T[] args);
再給出參數類型為:
{ typeof(IList<int>), typeof(int[]) }
{ typeof(IList<int[]>), typeof(int[]) }
{ typeof(IList<int[]>), typeof(int[][]) }
我希望能夠正確的推斷出 T 的類型分別為 int、int[] 和 int[]。
后來參考了《CSharp Language Specification》v5.0 中 7.5.2 類型推斷一節,規范中給出了 C# 中進行類型推斷的兩階段算法,算法分為兩階段主要是為了支持實參表達式和匿名函數的推斷,而我的需求則要簡單很多,只要支持普通的參數就可以了。又參考了 7.5.2.13 方法組轉換的類型推斷一節,最終得到了下面的簡化算法。
首先對幾個名詞進行區分:類型形參、類型實參、方法形參和方法實參。
對於泛型方法定義
void Method<T>(T a),其中的T是類型形參,T a是方法形參。對於相應的封閉泛型方法的調用
Method<int>(10),其中的int就是類型實參,10就是方法實參。
泛型方法的類型推斷,從形式上來定義,就是對給定泛型方法 Tr M<X1, …, Xn>(T1 x1, …, T_m x_m),其中 Tr 是返回值,X1, …, Xn 是類型形參,T1, …, T_m 是方法形參,和一個委托類型 D(U1 x1, …, U_m x_m),找到一組類型實參 S1, …, Sn,使表達式 M<S1, …, Sn> 與 D 兼容(D 可由M<S1, …, Sn> 隱式轉換而來)。
該算法首先認為所有 Xi 均未固定(即沒有預設值),並從 D 的每個實參類型 Ui 到 M 的對應形參類型 Ti 進行下限推斷(前提是 Ti 包含類型形參,即ContainsGenericParameters == true),但是如果 xi 為 ref 或 out 形參,則從 Ui 到 Ti 進行精確推斷。如果沒有為任何 Xi 找到界限,則類型推斷將失敗。否則,所有將 Xi 均固定到對應的 Si,它們是類型推斷的結果。下面給出詳細的推斷算法,這里的算法經過了我的修改,與原規范並不完全相同。
一、精確推斷
這里的精確推斷指的是對於給定的實參類型 U,找到合適的形參類型 V,使得 U == V。
按如下所述從類型 U 到類型 V 進行精確推斷:
- 如果
V是Xi之一,則將U添加到Xi的精確界限集中。 -
否則,通過檢查是否存在以下任何一種情況來確定集合
V1, …, V_k和U1, …, U_k:V是數組類型V1[…],U是具有相同秩的數組類型U1[…]。V是類型V1?,U是類型U1?。V是構造類型C<V1, …, V_k>並且U是構造類型C<U1, …, U_k>。
如果存在以上任意情況,則從每個
Ui到對應的Vi進行精確推斷。 - 否則,類型推斷將失敗。
二、下限推斷
這里的下限推斷指的是對於給定的實參類型 U,找到合適的形參類型 V,使得 V.IsImplicitFrom(U)。
按如下所述從類型 U 到類型 V 進行下限推斷:
- 如果
V是Xi之一,則將U添加到Xi的下限界限集中。 - 否則,如果
V為V1?類型,而U為U1?類型,則從U1到V1進行下限推斷。 - 否則,如果
V是數組類型V1[…],U是具有相同秩的數組類型U1[…],或者V是一個IEnumerable<V1>、ICollection<V1>或IList<V1>,U是一維數組類型U1[],如果不知道U1是引用類型,則從U1到V1進行精確推斷,否則進行下限推斷。 - 否則,如果
V是構造類、結構、接口或委托類型C<V1, …, V_k>,並且存在唯一類型C<U1, …, U_k>,使U等於、(直接或間接)繼承自或者(直接或間接)實現C<U1, …, U_k>(“唯一性”限制表示對於interface C<T>{} class U: C<X>, C<Y>{},不進行從U到C<T>的推斷,因為U1可以是X或Y。),則從每個Ui到對應的Vi進行推斷,如果不知道U1是引用類型,則進行精確推斷,否則推斷依賴於C的第i個類型參數:- 如果該參數是協變的,則進行下限推斷。
- 如果該參數是逆變的,則進行上限推斷。
- 如果該參數是固定的,則進行精確推斷。
- 否則,類型推斷將失敗。
三、上限推斷
這里的上限推斷指的是對於給定的實參類型 U,找到合適的形參類型 V,使得 U.IsImplicitFrom(V)。
按如下所述從類型 U 到類型 V 進行上限推斷:
- 如果
V是Xi之一,則將U添加到Xi的上限界限集中。 - 否則,如果
V為V1?類型,而U為U1?類型,則從U1到V1進行上限推斷。 - 否則,如果
V是數組類型V1[…],U是具有相同秩的數組類型U1[…],或者V是一維數組類型V1[],U是一個IEnumerable<U1>、ICollection<U1>或IList<U1>,如果不知道U1是引用類型,則從U1到V1進行精確推斷,否則進行上限推斷。 - 否則,如果
U是構造類、結構、接口或委托類型C<U1, …, U_k>,V是等於、(直接或間接)繼承自或者(直接或間接)實現唯一類型C<V1, …, V_k>的類、結構、接口或委托類型(“唯一性”限制表示如果我們有interface C<T>{} class V<Z>: C<X<Z>>, C<Y<Z>>{},則不進行從C<U1>到V<Q>的推斷。也不進行從U1到X<Q>或Y<Q>的推斷。),則從每個Ui到對應的Vi進行推斷,如果不知道U1是引用類型,則進行精確推斷,否則推斷依賴於C的第i個類型參數:- 如果該參數是協變的,則進行上限推斷。
- 如果該參數是逆變的,則進行下限推斷。
- 如果該參數是固定的,則進行精確推斷。
- 否則,類型推斷將失敗。
四、固定
固定是為了根據之前的算法得到的界限集,推斷出類型參數的合適的值。
具有界限集的類型變量 Xi 按如下方式固定:
- 候選類型集
Ui是在Xi的界限集中的所有類型的集合。 - 然后我們依次檢查
Xi的每個界限:對於Xi的每個精確界限U,將與U不同的所有類型Ui都從候選集中移除(要求U == Ui)。對於Xi的每個下限U,將不存在從U進行的隱式轉換的所有類型Ui都從候選集中移除(要求Ui.IsImplicitFrom(U))。對於Xi的每個上限U,將不存在從其到U進行的隱式轉換的所有類型Ui都從候選集中移除(要求U.IsImplicitFrom(Ui))。 - 如果在剩下的候選類型
Ui中,存在唯一類型V,該類型可由其他所有候選類型經隱式轉換而來,則將Xi固定到V(也就是說,要求V是其中最通用的類型)。 - 否則,類型推斷將失敗。
以上就是泛型方法的類型推斷算法,其中只考慮了方法實參和方法形參一一對應的情況,如果需要處理 params T[] 參數,則需要對最后一個參數進行特殊處理,並分別使用 T 和 T[] 進行一次類型推斷。做兩次類型推斷,就是為了判斷是否是方法的展開形式的調用。
或者說,對於泛型方法定義
void Method<T>(T a, params T[] args);
如果參數為 { typeof(int), typeof(int[]) } 和 { typeof(int[]), typeof(int[]) },雖然 T[] 對應的實參是相同的,但推斷出的 T 卻是不同的,這就需要利用兩次類型推斷來處理。
這個算法的實現加上注釋大概有 500 多行,這里就不再貼出,基本就是按照上面的 4 步來的,只是在一些細節上采用了更高效的做法。所有源碼可以見這里。
