dotnet 委託的實現解析(2)開放委託和封閉委託 (Open Delegates vs. Closed Delegates)

時風拖拉機發表於2022-03-22

前言

這是個人對委託的理解系列第二篇,部分翻譯自 Open Delegates vs. Closed Delegates – SLaks.Blog,好像還沒人翻譯過,加上部分個人理解。希望能對大家理解委託有所幫助。

正文

.Net支援兩種委託:開放委託和封閉委託。open delegatesclosed delegates

譯者注:這裡不是作者這麼分的,確實寫在dotnet的官方文件和註釋裡。當然翻譯的名稱值得考量。

封閉委託:

當你建立一個指向例項物件方法的委託時,這個例項物件就被儲存在委託的Target屬性中,這個屬性(也就是包含方法的例項物件)被當作方法的第一個引數傳給委託指向的方法。對於例項物件的方法來說,就是this,對於靜態方法來說,就是方法的第一個引數。這樣的委託被稱為封閉委託(closed delegates)因為他們以js閉包的方式把靜態方法的第一個引數或者例項方法的this封進了委託例項之中。

譯者注: 這裡說的很抽象,意思就是封閉委託會把某些引數作為Target屬性帶進委託中。後面有例子說明。

開放委託

我們同樣也能建立一個不隱含傳第一個引數的開式委託,這種委託不使用Target屬性,相反,所有委託目標方法的引數都是按委託的形參列表實際傳遞的,包括第一個引數。因此,一個指向給定方法的開放委託一定比指向同一個方法的封閉委託多傳一個引數(也就是this或者第一個引數)。開放委託一般用於指向靜態方法(同理:封閉委託一般用於例項方法)。當你建立一個指向靜態方法的委託時,你一般不想這個委託持有這個方法的第一個引數。

譯者注:原文比較抽象,結合後面的例子比較容易理解。

簡短說明

除了上面說的這兩種通用的情況來說(也就是靜態方法建立開放委託,例項方法建立封閉委託),在.Net 2.0和之後我們還能建立指向例項方法的開放委託和指向靜態方法的封閉委託。但是這樣C#就沒有相應的語法糖支援,只能通過Delegate.CreateDelegate方法來建立。

區分這兩種委託主要看Target屬性,原文的例子不太有些不太容易理解。這裡結合自己的理解。重新寫了例子,讀完本文的例子可以再去看原文的例子就比較容易理解了。

一般情況的例子

一般情況的封閉委託(例項方法)


internal class HelloWorld
    {
        public void HelloWorldInstance() => Console.WriteLine("hello world");

        public delegate void SayHi();

        public void Main()
        {
            var helloWorldInstance = new SayHi(HelloWorldInstance);
        }
    }

這裡實際呼叫的System.Private.CoreLib中的Delegate.CoreCLR.cs程式碼為:


[System.Diagnostics.DebuggerNonUserCode]
private void CtorClosed(object target, IntPtr methodPtr)
{
    if (target == null)
        ThrowNullThisInDelegateToInstance();
    this._target = target;
    this._methodPtr = methodPtr;
}

看名字就很容易理解,注意這裡對Target的賦值且一定不為null

一般情況的開放委託(靜態方法)


internal class HelloWorld
    {
        public static void HelloWorldStatic(string msg) => Console.WriteLine(msg);

        public delegate void SayHiWithParam(string msg);

        public void Main()
        {
            var helloWorld3 = new SayHiWithParam(HelloWorldStatic);
            helloWorld3("hello world");
        }
    }

這裡實際呼叫的System.Private.CoreLib中的MulticastDelegate.cs程式碼為:

[System.Diagnostics.DebuggerNonUserCode]
private void CtorOpened(object target, IntPtr methodPtr, IntPtr shuffleThunk)
{
    this._target = this;
    this._methodPtr = shuffleThunk;
    this._methodPtrAux = methodPtr;
}

注意:

  • 這裡的target實際為null,所以_target的賦值物件是this,而建立出的委託的Target屬性為null
  • 這裡_methodPtr的賦值物件是shuffleThunk

特殊情況的例子

前面說了,不一般情況下我們需要使用通過Delegate.CreateDelegate方法來建立委託。

特殊情況的封閉委託(靜態方法)

    internal class HelloWorld
    {
        public static void HelloWorldStatic(string msg) => Console.WriteLine(msg);

        public void Main()
        {
            var closed = Delegate.CreateDelegate(typeof(Action), "a", typeof(HelloWorld).GetMethod(nameof(HelloWorld.HelloWorldStatic)));
            closed.DynamicInvoke();
        }
    }

這裡實際呼叫的System.Private.CoreLib中的Delegate.cs程式碼為:

// V2 api: Creates open or closed delegates to static or instance methods - relaxed signature checking allowed.
public static Delegate CreateDelegate(Type type, object? firstArgument, MethodInfo method) => CreateDelegate(type, firstArgument, method, throwOnBindFailure: true)!;

特別需要注意:

  • 這裡建立的委託型別為:typeof(Action)不是typeof(Action<string>)因為,"a"做為第一個引數已經是委託的Target屬性了。

特殊情況開放委託(例項方法)

    public class HelloWorld
    {

        public void HelloWorldInstance() => Console.WriteLine("hello world");

        public void Main()
        {
            var open = Delegate.CreateDelegate(typeof(Action<HelloWorld>), typeof(HelloWorld).GetMethod(nameof(HelloWorld.HelloWorldInstance)));
            open.DynamicInvoke(this);
        }
    }

同樣特別需要注意:

  • 這裡建立的委託型別為:typeof(HelloWorld)不是typeof(Action<string>)
  • 可以注意到委託的Target屬性為null

原文的例子較難一點,感興趣的可以通過前文連結檢視。接著我們回到原文。

原文中的其他注意點

原文中提供了其他部分需要注意的部分包括

var allNumbers = Enumerable.Range(1, Int32.MaxValue);
Func<int, IEnumerable<int>> countTo = allNumbers.Take;

countTo這裡可以作為IEnumerable<int>的例項方法來使用。

  • 作者最後說,除了擴充套件方法,特殊情況的委託的應用只佔很小一部分,但對我們對委託的理解很重要。

me:不明覺厲。以及以上是這個系列的第二篇,有機會希望帶大家更深入的理解委託。