dotnet 委托的实现解析(2)开放委托和封闭委托 (Open Delegates vs. Closed Delegates)

前言

这是个人对委托的理解系列第二篇,部分翻译自 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)
  • 可以注意到委托的Target属性为null

原文的例子较难一点,感兴趣的可以通过前文链接查看。接着我们回到原文。

原文中的其他注意点

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

countTo这里可以作为IEnumerable<int>的实例方法来使用。

  • 作者最后说,除了扩展方法,特殊情况的委托的应用只占很小一部分,但对我们对委托的理解很重要。

me:不明觉厉。以及以上是这个系列的第二篇,有机会希望带大家更深入的理解委托。