C# 基础回顾: Delegate 前世今生

注:本系列并不是要介绍什么新知识,仅仅是对已有知识的整理归纳,也是为了系列的完整性。

就像维基或大多数资料所描述的一样,Delegate(委托,代理)是一个类似 C++ 中的函数指针,只不过 c# 的 Delegate 是类型安全(返回类型和参数都需要明确指定)的。这货在 C# 1.0 的时候就已经存在了,如果你从来没用过,... 请面壁思过。

本文会描述一下 Delegate 随着 C# 的进化而做出的改变,Delegate 与 反射、接口的关系。


Delegate & delegate

首先,还是先来把这两个东东说一下,这两个从字面上看的唯一区别就是大小写不同,它们都存在于 .Net 中,有些童鞋可能搞不清楚这两个的关系:前者(Delegate)表示的是委托类型,所有委托都继承自该类型。而后者则是一个语言的关键字,用于创建 Delegate 。


C# 1.0 & 1.1

在 1.0 的年代,如果想要创建一个委托,必须先使用 delegate 关键字声明一个与想要被委托的方法签名一致的委托类型,然后在构造这个委托的实例的时候把被委托的方法作为参数传给委托类型。

还是举个例子比较容易理解:

class Program
{
    //声明一个委托类型
    delegate void MyDelegate(int number);

    static void Main(string[] args)
    {
        //实例化一个委托类型
        MyDelegate test = new MyDelegate(MyMethod);
        
        //调用委托
        test(1);

        Console.ReadLine();
    }

    //被委托的方法
    static void MyMethod(int number)
    {
        Console.WriteLine("MyMethod {0}", number);
    }
}

//输出:MyMethod 1

上面用 delegate 关键字声明了一个叫做 MyDelegate 的委托类型,该委托可以用于需要一个 int 类型参数和无返回值的方法。

当我们需要调用一个委托的时候,首先要构造一个委托实例,该步骤是通过 new MyDelegate(MyMethod) 实现的。

有了这个委托实例之后,便可以像直接使用 MyMethod 一样使用委托实例了,而这个过程实际上是调用了委托类型的 Invoke 方法。


上面演示如何委托一个静态的方法,也可以用来委托一个实例方法:

class Program
{
    delegate void MyDelegate(int number);

    static void Main(string[] args)
    {
        Program p = new Program();
        
        //传递实例方法
        MyDelegate test = new MyDelegate(p.MyMethod);
        test(1);

        Console.ReadLine();
    }

    void MyMethod(int number)
    {
        Console.WriteLine("this.HashCode {0}", this.GetHashCode());
        Console.WriteLine("MyMethod {0}", number);
    }
}

//输出:
this.HashCode 22008501
MyMethod 1

C# 2.0

委托推断

1.0 的写法可以完美的实现委托,只是有点太啰嗦了。每次想委托某一个方法,还必须先用 new MyDelegate 方式包装一次,在 2.0 的时候,委托支持自动推断,因此不需要再包装了:

delegate void MyDelegate(int number);

static void Main(string[] args)
{
    MyDelegate test = MyMethod;
    test(1);

    Console.ReadLine();
}

static void MyMethod(int number)
{
    Console.WriteLine("MyMethod {0}", number);
}

直接把方法赋值给委托就 OK 了,编译器会自动推断。这个特性经常在事件(events)中被使用。


匿名方法

但是为了使用委托必须创建一个额外方法(i.e. MyMethod),尽管这个方法除了传递给委托外再无其它地方会调用它。在 C# 2.0 的时候,微软提供了匿名方法:

delegate void MyDelegate(int number);

static void Main(string[] args)
{    
    MyDelegate test = delegate(int number)
    {
        Console.WriteLine("MyMethod {0}", number);
    };
    test(1);

    Console.ReadLine();
}

匿名方法也就是指没有名称的方法,声明时仍旧利用了 delegate 关键字,编译器会自动生成一个静态的方法来表示这个匿名方法。

建议:鉴于可维护性的考虑,只有当一个方法体很短且不会有其它地方调用的时候才使用匿名方法代替。

有了匿名方法,就引出了闭包的概念,简单来说就是把变量与方法包裹在一起返回给了调用者,而又因为这个变量只能被该方法使用,所以是对外界封闭的。


泛型委托

还没完呢,让我们再神奇点!

static void Main(string[] args)
{
    Action<int> test = delegate(int number)
    {
        Console.WriteLine("MyMethod {0}", number);
    };
    test(1);

    Console.ReadLine();
}

C# 2.0 的时候提供了 Action<T> 来代替不需要返回值的委托类型。


C# 3.0 (.Net 3.5)

在 C# 2.0 的时候,委托的语法已经大大精减了,但使用 delegate 的语法总觉得很别扭。C# 3.0 的时候进一步减少了代码量,推出了 Lambda 表达式来声明匿名方法。所以上面的代码可以写成:

static void Main(string[] args)
{
    Action<int> test = (int number) =>
    {
        Console.WriteLine("MyMethod {0}", number);
    };
    test(1);

    Console.ReadLine();
}

使用 => (导出符号)进一步简化了代码,该符号前面是参数的声明,后面大括号包围的则是方法体。其实,上面的代码还可以再简化:

static void Main(string[] args)
{
    Action<int> test = number => Console.WriteLine("MyMethod {0}", number);
    test(1);

    Console.ReadLine();
}

是不是很神奇?当传递的参数只有一个,则可以省略括号及类型的声明,编译器可以根据等号左边的类型自动推断。如果方法体只有一句代码,则同样可以去掉大括号。


在 C# 3.0 的时候,提供了 Func<T> 来代替需要返回值的委托类型。


委托 和 事件

这个问题似乎只要是 C# 面试都会问。大部分人给的答案是事件是一种特殊的委托,但委托是一个类型,而事件只能做为类的成员存在(更像是一个属性),本人认为描述成是对委托的封装更为合理。

尽管声明事件的时候会使用委托类型,如下:

public class MyClass 
{
    //声明事件
    public event MyDelegate onDelegated;

    public delegate void MyDelegate(int number);      
}

但实际上,编译器会自动为事件添加两个方法:add 和 remove。上面的完全形态如下:

public class MyClass
{
    private MyDelegate _delegate;
    public event MyDelegate onDelegated
    {
        add
        {
            _delegate += value;
        }
        remove
        {
            _delegate -= value;
        }
    }

    public delegate void MyDelegate(int number);
}

可见,事件在 Delegate 基础上提供了两个接受同一个委托类型实例的方法;又因为事件会自动定义一个私有的委托实例变量,在其它类内部无法直接对该委托实例进行操作,只能通过事件的 +=,-= 进行操作,下面的代码无法通过编译:

public class Program
{
    static void Main(string[] args)
    {
        MyClass obj = new MyClass();
        obj.onDelegated = MyMethod; //编译失败,提示事件 onDelegated 只能出现在 +=,-= 左边
    }

    static void MyMethod(int number)
    {
        Console.WriteLine("MyMethod {0}", number);
    }
}


上面的例子中声明事件的写法并不规范,只用于解释委托和事件的关系,不要在实际项目中使用。推荐的定义的事件的方式:

1、委托类型的命名必须以 EventHandler 结尾且只接收两个参数(sender 和 e),如 SendEventHandler

2、事件的命名则是在去掉 EventHandler 后剩下的部分,如 Send

3、事件参数继承自 EventArgs,且命名以 EventArgs 结尾,如 SendEventArgs

4、定义一个虚方法来触发事件,该虚方法以 On 开头,如 OnSend。这样以便于子类可以修改事件的触发方式。


如:

delegate void SendEventHandler(object sender, SendEventArgs e);

public class SendEventArgs : EventArgs
{
    //...
}

public class MyClass
{
    public event SendEventHandler Send;

    public virtual void OnSend(SendEventArgs e)
    {
        if (Send != null)
        {
            Send(this, e);
        }
    }
}


委托 和 反射

这里所说的反射,指的是通过反射进行方法调用。先来看一个反射的例子:

MyClass obj = new MyClass();

Type type = typeof(MyClass);
MethodInfo method = type.GetMethod("Shout");
method.Invoke(obj, null);

上面的代码,利用反射的方式从类型中找到名叫 Shout 的方法,然后调用该方法。


委托的方式

MyClass obj = new MyClass();

Type type = typeof(MyClass);
MethodInfo method = type.GetMethod("Shout");
Action action = (Action)Delegate.CreateDelegate(typeof(Action), obj, method);

action();

比反射的方式多了一行,这里通过 CreateDelegate 这个静态的方法在运行时创建一个委托实例 (指向 obj.Shout 方法)


性能比较

反射调用方法的性能很差是众人兼知的事情,那委托的性能又怎么样呢?

上图的代码是在 release 模式下 Ctrl + F5 的方式运行的。可见反射的性能是最差的,与委托差距好几个数量级。下面是用于测试的原码:

public class Program
{
    static void Main(string[] args)
    {
        MyClass obj = new MyClass();

        #region Direct
        int count = 10000;
        Stopwatch sw = new Stopwatch();
        sw.Start();
        for (int i = 0; i < count; i++)
        {
            obj.Shout();
        }
        sw.Stop();
        Console.WriteLine("Direct:".PadRight(15) + sw.Elapsed);
        #endregion

        #region Reflection
        Type type = typeof(MyClass);
        MethodInfo method = type.GetMethod("Shout", Type.EmptyTypes, null);
        sw.Restart();
        for (int i = 0; i < count; i++)
        {
            method.Invoke(obj, null);
        }
        sw.Stop();
        Console.WriteLine("Reflection:".PadRight(15) + sw.Elapsed);
        #endregion

        #region Delegate
        sw.Restart();
        Action action = (Action)Delegate.CreateDelegate(typeof(Action), obj, method);
        for (int i = 0; i < count; i++)
        {
            action();
        }
        sw.Stop();
        Console.WriteLine("Delegate:".PadRight(15) + sw.Elapsed);
        #endregion

        Console.ReadLine();
    }
}

public class MyClass
{
    public void Shout()
    {
        //为了减小干扰,所以没有方法体
    }
}

反射在每一次方法调用的时候都会做很多额外的动作:类型验证,参数安全检查等。而委托则相当于一个方法指针,因此性能会远远要好于反射。


注,CreateDelegate 适用于知道方法签名的情况下,但有些时候方法签名是未知的(比如不知道参数个数,参数类型等)。这个时候可以通过 Emit 的方式来创建委托。

委托 和 接口

当我们无法在编码时得知一个对象的类型时,可以通过反射或委托的方式在运行时来解析出该对象的类型及其方法,并进行调用。

如果需要调用的为实例方法(而非静态方法,接口无法用于静态方法),且我们明确知道该实例类型实现XXX接口(且目标方法被定义在该接口中时)。那我们完全可以用接口来代替委托。

MyClass obj = new MyClass(); //假设 MyClass 实现了 IMyClass 接口。

IMyClass interfaceObj = (IMyClass)obj;
interfaceObj.Shout();


使用接口和使用委托来调用方法,在性能上的差距很小,(接口稍快,差距在 10ms 以内)。这两者更多的是一种互补关系(比如,有些时候,接口中并未提供所需要的方法签名时,就不得不使用委托)。


参考资源

Evolution of C# delegate syntax from .NET 1.x to 3.5

Delegates and Events

文章索引

[隐 藏]

本站采用知识共享署名 3.0 中国大陆许可协议进行许可。 ©2014 Charlie Box | 关于本站 | 浙ICP备13014059号