委托的使用
- 聲明委托類型
delegate 返回值 委托字段名(參數列表)
delegate void VarName(string input);
- 創建/使用一個與對應的委托簽名(參數/返回值完全匹配)一樣的方法
- void 與 非void 的返回類型是不匹配的,原因在於 系統的 JIT(Just-In-Time)階段需要知道,在執行方法時返回值是否會留在棧上。
- 創建委托實例
- 創建的表達式形式取決於是靜態方法還是實例方法
靜態方法:直接指定類型名
VarName varName1; //Delegate
varName1 = new VarName(StaticClass.method); //static method in delegate實例方法:需要先創建類型的實例,這個實例稱為「操作的目標」
調用委托實例時,會為這個對象調用方法。
VarName varName2; //Delegate
Instance instance = new Instance();
varName2 = new VarName(instance.method); //instance method in delegate
- 創建的表達式形式取決於是靜態方法還是實例方法
如果委托實例本身不能被回收,委托實例會阻止它的操作目標被作為垃圾回收。這可能會造成內存泄漏。
尤其是當操作目標的預期存活周期比委托實例本身要短,而如果委托實例本身無法被回收,就會導致操作目標的存活周期被延長。
- 調用委托實例
- 如果有一個委托的變量,可以直接將其視為方法本身
- 委托相當於指定一些代碼在特定時間執行(如單撃一個按鈕後發生某事)
合并和刪除委托
委托類(System.Delegate)中有兩個靜態方法
Combine(通過+=/+操作符調用)
Remove(通過-=/-操作符調用)
委托是不易變的。與String類型相似,實例被創建後,有關它的一切就不能改變
Combine和Remove都會操作現有的實例並創建一個新實例,不會更改原始委托對象。
調用委托時,所有操作將順序執行
如果委托的簽名有一個非void的返回類型,則Invoke的返回值是最後一個操作的返回值
假設現在有一個操作列表(a, b, c),操作b的過程中拋出一個異常,異常會立即傳播,並阻止執行其他後續操作。
委托的內部實現
在非托管C/C++中,非成員函數的地址只是一個內存地址,不攜帶任何額外信息,不是類型安全的
而.NET的委托則可視為「類型安全」的回調機制
將方法綁定到委托時,C#和CLR允許引用類型的協變性和逆變性
協變:方法能返回從委托類型的返回類型派生的一個類型
逆變:方法的參數可以是委托類型的參數類型的基類
delegate object MyCallback(FileStream fs);
class Program
{
static void Main(string[] args)
{
CallBackMethods cm = new CallBackMethods();
MyCallback callback = cm.SomeMethod;
}
}只有引用類型才支持協變性和逆變性
編譯器和CLR如何協同工作來實現委托
以上面的委托聲明代碼為例:
delegate object MyCallback(FileStream fs);
編譯器實際上會為這段代碼定義一個完整的類
換成C#代碼的話,大概會是這樣的
private class MyCallback:System.MulticastDelegate
{
//構造器
public MyCallback(Object @object, IntPtr method);
public virtual void Invoke(FileStream value);
public virtual IAsyncResult BeginInvoke(FileStream fs, AsyncCallback callback, Object @object);
public virtual Object EndInvoke(IAsyncResult result);
}
元數據
TypeDef #1 (02000002)
-------------------------------------------------------
TypDefName: CLR_Ch17.MyCallback (02000002)
Flags : [NotPublic] [AutoLayout] [Class] [Sealed] [AnsiClass] (00000100)
Extends : 0100000C [TypeRef] System.MulticastDelegate
Method #1 (06000001)
-------------------------------------------------------
MethodName: .ctor (06000001)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
RVA : 0x00000000
ImplFlags : [Runtime] [Managed] (00000003)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
2 Arguments
Argument #1: Object
Argument #2: I
2 Parameters
(1) ParamToken : (08000001) Name : object flags: [none] (00000000)
(2) ParamToken : (08000002) Name : method flags: [none] (00000000)
Method #2 (06000002)
-------------------------------------------------------
MethodName: Invoke (06000002)
Flags : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
RVA : 0x00000000
ImplFlags : [Runtime] [Managed] (00000003)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Object
1 Arguments
Argument #1: Class System.IO.FileStream
1 Parameters
(1) ParamToken : (08000003) Name : fs flags: [none] (00000000)
Method #3 (06000003)
-------------------------------------------------------
MethodName: BeginInvoke (06000003)
Flags : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
RVA : 0x00000000
ImplFlags : [Runtime] [Managed] (00000003)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Class System.IAsyncResult
3 Arguments
Argument #1: Class System.IO.FileStream
Argument #2: Class System.AsyncCallback
Argument #3: Object
3 Parameters
(1) ParamToken : (08000004) Name : fs flags: [none] (00000000)
(2) ParamToken : (08000005) Name : callback flags: [none] (00000000)
(3) ParamToken : (08000006) Name : object flags: [none] (00000000)
Method #4 (06000004)
-------------------------------------------------------
MethodName: EndInvoke (06000004)
Flags : [Public] [Virtual] [HideBySig] [NewSlot] (000001c6)
RVA : 0x00000000
ImplFlags : [Runtime] [Managed] (00000003)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Object
1 Arguments
Argument #1: Class System.IAsyncResult
1 Parameters
(1) ParamToken : (08000007) Name : result flags: [none] (00000000)
該類一共有4個方法:
構造方法
Invoke
BeginInvoke
EndInvoke
訪問權限與委托的聲明一致
由於委托實際上是一個類,因此能定義類的地方都能定義委托
所有委托類型都派生自MulticastDelegate
MulticastDelegate派生自Delegate,Delegate再派生自Object
這是C#的歷史遺留問題,FCL本該只有一個委托類
MulticastDelegate有三個最重要的私有字段
Field Type Description _target System.Object 這個字段引用的是回調方法的操作對象;如果是靜態方法,該值為null _methodPtr System.IntPtr 內部的整數值,CLR用它標識要回調的方法 _invocationList System.Object 通常為null。構造委托鏈時引用一個委托數組
所有委托類型都有一個接受兩個參數的構造器
參數
Object @object
IntPrt method
當我們構造一個委托時,C#編譯器會分析源代碼來確定引用的對象和方法
對象的引用會被傳給構造器的@object參數,並保存在_target中
方法的標識值會被傳遞至構造器的method參數,並保存在_methodPtr中
該值從MethodDef/MemberRef元數據中的token獲得
每個委托對象都是一個包裝器,其中包裝了一個方法和調用該方法時操作的對象
CallBackMethods cm = new CallBackMethods();
MyCallback myCallback = new MyCallback(cm.SomeMethod);
MyCallback myStaticCallback = new MyCallback(CallBackMethods.SomeStaticMethod);
委托鏈
delegate void TestDelegate(int n);
class Program
{
static void Main(string[] args)
{
TestDelegate d_Base = null;
TestDelegate d1 = new TestDelegate(cm.TestMethod_A);
TestDelegate d2 = new TestDelegate(cm.TestMethod_B);
TestDelegate d3 = new TestDelegate(cm.TestMethod_C);
d_Base = (TestDelegate)Delegate.Combine(d_Base, d1);
d_Base = (TestDelegate)Delegate.Combine(d_Base, d2);
d_Base = (TestDelegate)Delegate.Combine(d_Base, d3);
}
}首次執行Combine時:
d_Base = (TestDelegate)Delegate.Combine(d_Base, d1);
Combine方法發現試圖合併null和d1,直接返回d1的引用對象
再次執行Combine時:
d_Base = (TestDelegate)Delegate.Combine(d_Base, d2);
Combine方法發現d_Base已引用了一個委托對象(d1),Combine會構造一個新的委托對象
初始化其私有字段(具體的值不重要)
重要的是_invocationList會被初始化為引用一個「委托對象數組」
第一個元素被初始化為引用包裝了TestMethod_A方法的委托
第二個元素被初始化為引用包裝了TestMethod_B方法的委托
d_Base被設為引用新建的委托對象
最後一次執行Combine時:
d_Base = (TestDelegate)Delegate.Combine(d_Base, d3);
再次執行上面第二次執行Combine的流程
再新增一個委托對象並被d_Base所引用,d_Base的_invocationList引用一個包含了TestMethod_A/B/C的委托對象數組
之前新增的委托和_invocationList(包含TestMethodA/B)將會進行GC
示意圖:
委托鏈的調用
class Program
{
static void Main(string[] args)
{
//...
d_Base(30);
}
}
public class CallBackMethods
{
public void TestMethod_A(int n)
{
Console.WriteLine($"A: {n}");
}
public void TestMethod_B(int n)
{
Console.WriteLine($"B: {n}");
}
public void TestMethod_C(int n)
{
Console.WriteLine($"C: {n}");
}
}Output:
A: 30
B: 30
C: 30
在d_Base引用的委托上調用Invoke時,會發現私有字段_invocationList != null,並執行一個循環遍歷數組中的所有元素,依次調用每個元素包裝的方法
如果包裝的方法有返回值,可以在調用委托時接收其返回值,但只會接收到委托鏈中最後一個委托對象的返回值
delegate int TestRetDelegate(int n);
static void Main(string[] args)
{
TestRetDelegate retD_Base = null;
TestRetDelegate retD1 = new TestRetDelegate(cm.TestRetMethod_A);
TestRetDelegate retD2 = new TestRetDelegate(cm.TestRetMethod_B);
retD_Base = (TestRetDelegate)Delegate.Combine(retD_Base, retD1);
retD_Base = (TestRetDelegate)Delegate.Combine(retD_Base, retD2);
int result = retD_Base(100);
Console.WriteLine($"Result: {result}");
}
public class CallBackMethods
{
public int TestRetMethod_A(int n)
{
Console.WriteLine($"Ret A: {n}");
return n * 100;
}
public int TestRetMethod_B(int n)
{
Console.WriteLine($"Ret B: {n}");
return n / 2;
}
}在該例子中,委托鏈先調用TestRetMethod_A,再調用TestRetMethod_B,各傳遞(100)作為實參
TestRetMethod_A按理返回10000
TestRetMethod_B按理返回50
最後result的輸出為50,也就是TestRetMethod_B的返回值
如果需要獲取更多的控制,可以使用MulticastDelegate提供的實例方法:GetInvocationList,顯式調用鏈中的每一個委托
GetInvocationList內部重新構造了一個數組,元素依次指向_invocationList的每個委托,並返回該數組的引用
Delegate[] retDs = retD_Base.GetInvocationList();
object result_A = retDs[0].Method.Invoke(cm, new object[] { 100 });
Console.WriteLine($"Result_A: {result_A}"); //Output: 10000
object result_B = retDs[1].DynamicInvoke(new object[] { 100 });
Console.WriteLine($"Result_B: {result_B}"); //Output: 50
移除委托
d_Base = (TestDelegate)Delegate.Remove(d_Base, d1);
Remove被調用時,會掃瞄第一個實參(d_Base)所引用的委托內部的_invocationList
在裡面查找 _target字段和 _methodPtr字段與第二個參數匹配的委托,找到則移除
移除後剩餘一個數據項,d_Base引用直接指向該剩餘的委托對象
移除後剩餘多個數據項,新建一個不包含移除對象委托的_invocationList的委托對象,並讓d_Base引用該新對象
移除後剩餘零個數據項,返回null
如果有多於一個匹配項,每次Remove只會移除1個匹配項
委托與事件
事件不是委托類型的字段,更像是一個委托類型的屬性
我們對事件的操作只有 +=/-=和對其的Invoke
因此事件更像對委托的一種保護機制,為委托提供了一個額外的封裝層
using System;
class Test
{
public event EventHandler MyEvent
{
//當我們在使用 +=/-=時,實際上是在調用 add 和 remove方法
//我們所能做的只有「訂閱」和「取消訂閱」
add //+=
{
Console.WriteLine ("add operation");
}
remove //-=
{
Console.WriteLine ("remove operation");
}
}
static void Main()
{
Test t = new Test();
t.MyEvent += new EventHandler (t.DoNothing);
t.MyEvent -= null;
}
void DoNothing (object sender, EventArgs e)
{
}
}事件之所以容易被人當成委托,是因為C#提供了一個事件的shortcut,字段風格的聲明方式。
在字段風格的事件聲明中,編譯器會將聲明轉換成一個具有默認add/remove實現的事件和一個私有的委托類型的字段。
因此,類外的代碼只能看見事件。表面上似乎能調用一個事件,但實際上是調用存儲在字段中的委托實例。
public event VarName MyEvent; //事件
public delegate void VarName(string input); //委托
委托與Lambda表達式
表達式的類型并非委托類型,但它可以通過隱式/顯式轉換成一個委托實例
匿名函數 = 匿名方法 + Lambda表達式
兩者在很多情況下可以使用相同的轉換規則
Func<T1-16, TResult>
T1-16:可獲16的類型參數
TResult:返回類型參數
使用 => 操作符,告訴編譯器我們正使用一個Lambda表達式
Func<string, int> returnLength;
returnLength = (string text) => { return text.Length; }
//單一表達式作為主體
//該表達式的值是Lambda的結果
//只指定表達式,不使用大括號,不使用return語句,也不添加分號
returnLength = (string text) => text.Length;
//隱式類型的參數列表
//由編譯器去推斷,不需要顯式聲明(整個列表就是顯式/隱式)
//如果有out/ref參數,就必須使用顯式類型
returnLength = (text) => text.Length;
//單一參數
//如果只需要一個參數,可以省略括號
returnLength = text => text.Length;
高階(High-order)函數
Lambda表達式的主體本身包含另一個Lambda表達式
Lambda表達式的參數是另一個委托
如:List的過濾/排序/操作
//C# Code
public class Film
{
public string Name { get; set; }
public int Year { get; set; }
}
static void Main()
{
var films = new List<Film>
{
new Film{ Name = "The Wizard of Oz", Year = 1939 },
new Film{ Name = "Jaws", Year = 1975 },
new Film{ Name = "Singing in the Rain", Year = 1952 },
};
Action<Film> print = film => Console.WriteLine($"Name={film.Name}, Year={film.Year}");
films.ForEach(print);
films.FindAll(film => film.Year < 1960).ForEach(print);
films.Sort((f1, f2) => f1.Name.CompareTo(f2.Name));
films.ForEach(print);
}
編譯器看到lambda表達式,會自動為其創建一個匿名函數和委托類型變量
static void Main(string[] args)
{
LambdaTest(obj => Console.WriteLine(obj), 50);
}
public static void LambdaTest(TestDelegate d, int n)
{
d?.Invoke(n);
}方法名字的生成不是固定的,本次生成為”<>c”
委托類型變量名字的生成也不是固定的,本次生成為”<>9__0_0″
代碼大概會被編譯成這個樣子
[CompilerGenerated]
private static TestDelegate <>9__0_0; //自動生成的委托類型名
static void Main(string[] args)
{
if(<>9__0_0 == null)
{
<>9__0_0 = new TestDelegate(<Main>b__0_0)
}
LambdaTest(<>9__0_0, 50);
}
[CompilerGenerated]
private static void <Main>b__0_0(Object obj)
{
Console.WriteLine(obj);
}
另外,如果Lambda表達式中使用到了一些局部變量的話,C#也會為我們完成一些複雜的過程
int n = 50;
static void Main(string[] args)
{
LambdaTest(obj => Console.WriteLine(obj), n);
}C#會定義一個輔助類
該類為打算傳給回調代碼的每個值都定義一個字段
回調代碼會定義成輔助類中的實例方法
構造輔助類的實例,用局部變量的值來初始化輔助類裡的字段
最後構造綁定到輔助對象/實例的委托對象
示例:
//Origin CSharp
internal sealed class AClass
{
public static void UsingLocalVariableInTheCallbackCode(int numToDo)
{
int[] squares = new int[numToDo];
AutoResetEvent done = new AutoResetEvent(false);
for (int n = 0; n < squares.Length; n++)
{
ThreadPool.QueueUserWorkItem(
obj => {
int num = (int)obj;
squares[num] = num * num;
if(Interlocked.Decrement(ref numToDo) == 0)
{
done.Set();
}
}, n);
}
done.WaitOne();
for (int n = 0; n < squares.Length; n++)
{
Console.WriteLine($"Index :{n}, Square={squares[n]}");
}
}
}
//Rewrite By CSharp
internal sealed class AClass
{
public static void UsingLocalVariableInTheCallbackCode(int numToDo)
{
//Auto Added
WaitCallback callback1 = null;
//Support Class Instance
<>c_DisplayClass2 class1 = new <>c_DisplayClass2();
//Init Support class fields
class1.numToDo = numToDo;
class1.squares = new int[class1.numToDo];
class1.done = new AutoResetEvent(false);
for(int n = 0; n < class1.squares.Length; n++)
{
if(callback1 == null)
{
//新建的委托對象綁定到輔助對象及其匿名實例方法
callback1 = new WaitCallback(
class1.<UsingLocalVariablesInTheCallbackCode>b__0);
}
ThreadPool.QueueUserWorkItem(callback1, n);
}
class1.done.WaitOne();
for (int n = 0; n < class1.squares.Length; n++)
{
Console.WriteLine($"Index :{n}, Square={class1.squares[n]}");
}
}
}
//Support Class
[CompileGenerated]
private sealed class <>c_DisplayClass2:Object{
public int[] squares;
public int numToDo;
public AutoResetEvent done;
public <>c_DisplayClass2 { }
public void <UsingLocalVariablesInTheCallbackCode>b__0(Object obj)
{
int num = (int)obj;
squares[num] = num * num;
if(Interlocked.Decrement(ref numToDo) == 0)
{
done.Set;
}
}
}
無論是把Lambda表達式當作方法實參還是賦給委托實例,編譯器都會為Lambda表達式創建一個方法
因為編譯器知道以後如果代碼再次被調用,可以重用委托實例,所以會把它緩存下來
委托與反射
編譯時在對委托的所有必要信息存在未知時創建委托
System.Delegate.MethodInfo.CreateDelegate
使用Delegate的實例方法DynamicInvoke進行調用
參考書目
- 《CLR via C#》(第4版) Jeffrey Richter
- 《深入理解C#》(第3版) Jon Skeet