浅谈C#委托和事件(转载)

Posted 莫水千流

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈C#委托和事件(转载)相关的知识,希望对你有一定的参考价值。

委托给了C#操作函数的灵活性,我们可使用委托像操作变量一样来操作函数,其实这个功能并不是C#的首创,早在C++时代就有函数指针这一说法,而在我看来委托就是C#的函数指针,首先先简要的介绍一下委托的基本知识:

委托的定义
委托的声明原型是
delegate <函数返回类型> <委托名> (<函数参数>)
例子:public delegate void CheckDelegate(int number);//定义了一个委托CheckDelegate,它可以注册返回void类型且有一个int作为参数的函数
这样就定义了一个委托,但是委托在.net内相当于声明了一个类(在后面的代码中会讲到确实如此),类如果不实例化为对象,很多功能是没有办法使用的,委托也是如此.

委托的实例化
委托实例化的原型是
<委托类型> <实例化名>=new <委托类型>(<注册函数>)
例子:CheckDelegate _checkDelegate=new CheckDelegate(CheckMod);//用函数CheckMod实例化上面的CheckDelegate 委托为_checkDelegate
在.net 2.0开始可以直接用匹配的函数实例化委托:
<委托类型> <实例化名>=<注册函数>
例子:CheckDelegate _checkDelegate=CheckMod;//用函数CheckMod实例化上面的CheckDelegate 委托为_checkDelegate
现在我们就可以像使用函数一样来使用委托了,在上面的例子中现在执行_checkDelegate()就等同于执行CheckMod(),最关键的是现在函数CheckMod相当于放在了变量当中,它可以传递给其它的CheckDelegate引用对象,而且可以作为函数参数传递到其他函数内,也可以作为函数的返回类型

 

用匿名函数初始化委托

上面为了初始化委托要定义一个函数是不是感觉有点麻烦,另外被赋予委托的函数一般都是通过委托实例来调用,很少会直接调用函数本身。

在.net 2.0的时候考虑到这种情况,于是匿名函数就诞生了,由于匿名函数没有名字所以必须要用一个委托实例来引用它,定义匿名函数就是为了初始化委托

匿名函数初始化委托的原型:

<委托类型> <实例化名>=new <委托类型>(delegate(<函数参数>){函数体});

当然在.net 2.0后可以用:

<委托类型> <实例化名>=delegate(<函数参数>){函数体};

例子:

复制代码
        delegate void Func1(int i);
        delegate int Func2(int i);

        static Func1 t1 =new Func1(delegate(int i)
        {
            Console.WriteLine(i);
        });

        static Func2 t2;

        static void Main(string[] args)
        {
            t2 = delegate(int j)
            {
                return j;
            };
            t1(2);
            
            Console.WriteLine(t2(1));
            
        }
复制代码

 

当然在.net 3.0的时候又有了比匿名函数更方便的东西lambda表达式,这儿就不说了。


泛型委托
委托也支持泛型的使用
泛型委托原型:
delegate <T1> <委托名><T1,T2,T3...> (T1 t1,T2 t2,T3 t3...)
例子:
delegate T2 A<T1,T2>(T1 t);//定义有两个泛型(T1,T2)的委托,T2作为委托函数返回类型,T1作为委托函数参数类型

static int test(int t)
{
      return t;
}

static void Main(string[] args)
{
      A<int, int> a =test;//将泛型委托委托<T1,T2>实例化为<int,int>,即表示有一个int类型参数且返回类型是int的函数,所以将test用来实例化委托
      Console.WriteLine(a(5));//输出5
}



委托的多播性
在上面实例化委托的时候看到:必须将一个匹配函数注册到委托上来实例化一个委托对象,但是一个实例化委托不仅可以注册一个函数还可以注册多个函数,注册多个函数后,在执行委托的时候会根据注册函数的注册先后顺序依次执行每一个注册函数
函数注册委托的原型:
<委托类型> <实例化名>+=new <委托类型>(<注册函数>)
例子:CheckDelegate _checkDelegate=new CheckDelegate(CheckMod);//将函数CheckMod注册到委托实例_checkDelegate
在.net 2.0开始可以直接将匹配的函数注册到实例化委托:
<委托类型> <实例化名>+=<注册函数>
例子:CheckDelegate _checkDelegate+=CheckMod;//将函数CheckMod注册到委托实例_checkDelegate
之后我们还可以注册多个函数到委托上:
例子:_checkDelegate+=CheckPositive;//将函数CheckPositive注册到委托实例_checkDelegate
        _checkDelegate();//执行这个委托实例会先执行CheckMod()再执行CheckPositive()

实际上使用+=符号的时候会判断
如果此时委托还没有实例化(委托实例为null),它会自动用+=右边的函数实例化委托
如果此时委托已经实例化,它会只把+=右边的函数注册到委托实例上
另外有一点需要注意的是,如果对注册了函数的委托实例从新使用=号赋值,相当于是重新实例化了委托,之前在上面注册的函数和委托实例之间也不再产生任何关系,后面的例子会讲到这点!

当然有+=注册函数到委托,也有-=解除注册
例子:_checkDelegate-=new CheckDelegate(CheckPositive);//解除CheckPositive_checkDelegate的注册
        _checkDelegate-=CheckPositive;//.net 2.0开始可以用这种方式解除注册

 

 

另外当在委托和事件(事件的细节将在后面介绍)上注册了多个函数后,如果委托和事件有返回值,那么调用委托和事件时,返回的将是最后一个注册函数的返回值。如下示例代码将做详细解释。

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MultiDelegatesReturn
{
    public delegate int DelMath(int i);//定义委托类DelMath,该委托传入一个int类型参数,返回一个int类型参数

    class Program
    {
        static DelMath dMath;//通过委托类型DelMath定义委托实例dMath
        static event DelMath eMath;//通过委托类型DelMath定义事件实例eMath

        /// <summary>
        /// 将传入的参数i自加后作为函数返回值
        /// </summary>
        static int IncMath(int i)
        {
            i++;
            Console.WriteLine("IncMath has been invoked!");
            return i;
        }

        /// <summary>
        /// 将传入的参数i自减后作为函数返回值
        /// </summary>
        static int DecMath(int i)
        {
            i--;
            Console.WriteLine("DecMath has been invoked!");
            return i;
        }

        static void Main(string[] args)
        {
            int i = 10;//定义int型变量i,初始值为10

            dMath += IncMath;//先将IncMath函数注册到委托实例dMath
            dMath += DecMath;//再将DecMath函数注册到委托实例dMath

            Console.WriteLine("dMath returned:" + dMath(i).ToString());//将int型变量10传入委托实例dMath调用后,返回的结果是9,说明委托实例
            //dMath返回的是后注册的函数DecMath的返回值

            eMath += IncMath;//先将IncMath函数注册到事件实例eMath
            eMath += DecMath;//再将DecMath函数注册到事件实例eMath

            Console.WriteLine("eMath returned:" + eMath(i).ToString());//将int型变量10传入事件实例eMath调用后,返回的结果也是9,说明事件实例
            //eMath返回的也是后注册的函数DecMath的返回值

        }
    }
}
复制代码

 

 

 


c#事件
了解委托之后,就可以来谈谈事件了,C#事件是什么?
c#事件的定义和委托的声明是如此的相似:
event <委托类型> 事件名
例子:public event CheckDelegate checkEvent;
上面的例子声明了个事件叫checkEvent你会发现它只比声明委托实例前多了个关键字event
声明了事件后就可以实例化事件,注册函数到事件,解除事件函数注册其方法和委托的步骤如出一辙:
例子:checkEvent+=new CheckDelegate(CheckMod);//将函数CheckMod注册到事件checkEvent
       checkEvent+=CheckMod;//.net 2.0开始支持这种方法
       checkEvent-=new CheckDelegate(CheckMod);//将函数CheckMod解除对事件checkEvent的注册
       checkEvent-=CheckMod;//.net 2.0开始支持这种方法

从种种迹象都可以看出事件和委托实例是那么的相似,那么为什么不直接用委托还要用到事件呢?其实事件就是对委托的封装,就如同c#类中属性对字段的封装一样,其封装后可以在委托上封装更复杂的逻辑,下面我们来看c#中事件的两种声明方式,来了解事件对委托的封装

隐式声明事件
这种方式声明事件很简单,就如同声明委托实例一样:
event <委托类型> 事件名;
例子:public event CheckDelegate checkEvent;
我们用反射机制来看看这样声明的事件里面装的到底是什么东西

我 们可以看到在事件被编译后自动生成了个private的委托实例checkEvent和两个函数add_checkEvent和 remove_checkEvent,这两个函数分别对应事件的+=/-=操作,另外可以看到在声明了事件后的确是产生了一个和事件同名私有的委托实例 checkEvent,对事件的+=/-=操作都会反映在这个同名委托实例checkEvent上,所以可以在定义事件的类里面直接调用 checkEvent()来执行注册函数和对checkEvent使用=号重新赋值,实际上这里操作的并不是checkEvent事件,而操作的是同名委 托实例checkEvent,因此隐式声明的事件,其实就是由一个委托实例和两个函数封装而成,所有的操作最终都反映在委托实例上。

(这里我补充下我的个人理解:事实上在一个类的内部是无法定义一个事件后又定义一个和事件同名的委托实例的,如果你在本例中尝试再定义CheckDelegate checkEvent,编译的时候会报错并提示已经定义了名叫checkEvent的委托,原因是因为事件本来就是一种特殊的委托实例(不管是隐式或显式声明的事件都是这样),因此定义和事件同名的委托实例会报错,所以我个人认为.net在编译的时候会把隐式声明的事件编译成为委托实例(和事件同名),本例中的checkEvent事件在编译后也不再是事件转而被编译成了checkEvent委托实例,否则又怎么可能在定义事件的类的内部可以执行事件和对事件赋值呢(这里大家可以看看我给的显式声明事件的例子,那里面有说到),唯一的解释就是隐式声明的事件其实就是委托实例)

显式声明事件
其实显示声明事件就是要自己来手动实现隐式声明事件的一个委托实例
和两个函数:
event <委托类型> 事件名
{
      add
      {
            //将函数注册到自己定义的委托实例
      }

      remove
      {
            //解除函数对自己定义的委托实例的注册
      }
}

例子:private CheckDelegate _checkDelete;
        public event CheckDelegate checkEvent
        {
            add
            {
                _checkDelete = Delegate.Combine(_checkDelete, value) as CheckDelegate;
            }
            remove
            {
                _checkDelete = Delegate.Remove(_checkDelete, value) as CheckDelegate;
            }
        }
//Delegate.Combine和Delegate.Remove是.net库函数,作用是合并委托实例注册函数和移除委托实例注册函数并返回合并和移除后的委托实例,具体解释请查阅MSDN

我们再用反射机制查看显式声明事件编译后的代码

可 以看到显示声明事件的代码编译后和隐式声明事件的代码几乎相同,只不过这里我们自己定义了事件操作委托实例_checkDelete。另外显式声明的事件 不支持直接调用,就算在定义事件的类里面也不能直接调用显式声明的事件(checkEvent();//这样会报错),应该调用事件委托实例 (_checkDelete();)。

本文例子
俗话说得好说得多不如做得多,现在就把例子发出来,例子中还讲了些东西,可以执行例子看了输出结果后再体会:
首先是个c#类库项目ClassLibrary,里面包含两个类分别是显式声明和隐式声明事件
AutoCheckClass.cs

复制代码
using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibrary
{
    public class AutoCheckClass
    {
        public delegate void CheckDelegate(int number);
        public event CheckDelegate checkEvent;

        public void WriteInner(int n)
        {
            Console.WriteLine(n.ToString());
        }

        public void InitEvent()
        {
            checkEvent = WriteInner;//对事件从新赋值
            //checkEvent = new CheckDelegate(WriteInner);//也可以用委托对事件进行赋值
        }

        public void Exec(int n)
        {
            checkEvent(n);
        }

        /*
         采用这种方式,public event CheckDelegate checkEvent;会自动生成一个private CheckDelegate checkEvent,
         对于public event CheckDelegate checkEvent;的+/-操作都会在编译时反应在private CheckDelegate checkEvent上
         而且add/remove .net在编译的时候会自动生成,不用自己再操心,缺点是每个事件的委托都被封装,无法操作其内部的委托
         
         此外采用这种方式定义的事件,可以在定义事件的类的内部直接对事件进行赋值,例如可以在Exec函数中加上下面这句代码:
         checkEvent = Exec;
         表示该事件可以被匹配的函数或委托赋值初始化。
         并且对事件进行赋值操作,相当于从新初始化事件内部的委托(同名委托实例),会让赋值之前对事件注册的函数都不再与事件产生关系,具体示例请见本类中InitEvent函数的使用效果。
         */
    }
}
复制代码


CheckClass.cs

复制代码
using System;
using System.Collections.Generic;
using System.Text;

namespace ClassLibrary
{
    public class CheckClass
    {
        public delegate void CheckDelegate(int number);
        private CheckDelegate _checkDelete;
        public event CheckDelegate checkEvent
        {
            add
            {
                _checkDelete = Delegate.Combine(_checkDelete, value) as CheckDelegate;
            }
            remove
            {
                _checkDelete = Delegate.Remove(_checkDelete, value) as CheckDelegate;
            }
        }

        public void Exec(int n)
        {
            _checkDelete(n);
            //checkEvent = Exec;注意显示定义事件的方式,不支持对事件直接进行赋值
        }

        /*
         delegate在编译的时候会被net编译成一个类,如下:
         public delegate void CheckDelegate(int number);在编译的时候会编译为下面的类
         public sealed class CheckDelegate:System.MulticastDelegate
         {
            public GreetingDelegate(object @object, IntPtr method);
            public virtual IAsyncResult BeginInvoke(string name, AsyncCallback callback, object @object);
            public virtual void EndInvoke(IAsyncResult result);
            public virtual void Invoke(string name);
         }
         而System.MulticastDelegate继承于System.Delegate,所以下面的代码才会顺利执行
         _checkDelete = Delegate.Combine(_checkDelete, value) as CheckDelegate;
         _checkDelete = Delegate.Remove(_checkDelete, value) as CheckDelegate; 
         采用这种方法可以让你自己指定事件的委托,甚至可以让多个事件使用同一个委托,且自己实现add/remove,可以实现更复杂的逻辑
         
         此外需要注意的是,采用这种方式定义的事件,就算在定义事件的类的内部都无法对事件直接进行赋值,例如先前在另外种定义方式说到的在Exec函数中加上:
         checkEvent = Exec;
         会报错:事件“ClassLibrary.CheckClass.checkEvent”只能出现在 += 或 -= 的左边
         所以在这里我们不应该操作checkEvent,因为它没有同名委托实例,而因该操作_checkDelete
         */
    }
}
复制代码


然后是个控制台项目,需要引入上面的类库的dll文件
Program.cs

复制代码
sing System;
using System.Collections.Generic;
using System.Text;
using ClassLibrary;

namespace DeleGate
{
    class Temp//定义此类是为了在代码中展示函数对委托和事件的另外一种注册方式
    {
        public delegate void TempDelegate(int u);
        public static TempDelegate td;
        public static event TempDelegate ed;
    }

    class Program
    {
        private static void CheckMod(int number)
        {
            if (number % 2 == 0)
                Console.WriteLine("输入的是偶数");
            else
                Console.WriteLine("输入的不是偶数");
        }

        private static void CheckPositive(int number)
        {
            if (number > 0)
                Console.WriteLine("输入的是正数");
            else
                Console.WriteLine("输入的不是正数");
        }

        
        static void Main(string[] args)
        {
            CheckClass cc = new CheckClass();
            cc.checkEvent += new CheckClass.CheckDelegate(CheckMod);
            cc.checkEvent += new CheckClass.CheckDelegate(CheckPositive);

            AutoCheckClass acc = new AutoCheckClass();
            acc.checkEvent += new AutoCheckClass.CheckDelegate(CheckMod);
            acc.checkEvent += new AutoCheckClass.CheckDelegate(CheckPositive);
            //acc.InitEvent();//执行了这个方法后,由于对事件从新赋了值,上面对事件注册的两个函数都会失效
            
            Temp.td = CheckMod;//这表示对委托进行赋值(等同于:Temp.td = new Temp.TempDelegate(CheckMod);),和对事件赋值一样,对委托进行赋值相当于初始化委托,会让赋值之前在委托上注册的函数与委托失去注册关系。
            Temp.td += CheckPositive;
            Console.WriteLine("Temp的结果");
            Temp.td(50);

            Temp.ed += CheckMod;
            Temp.ed += CheckPositive;

            Console.WriteLine("cc的结果");
            cc.Exec(50);
            Console.WriteLine("acc的结果");
            acc.Exec(50);
            

            Console.ReadKey();
        }
    }
}
复制代码

 

 

附加更新补充

调用委托实例的对象并不是调用委托函数的对象

通过前面的例子,我们了解到了,委托其实就是C#中的函数指针,有了委托我们可以像使用变量一样来使用函数。但是请切记调用委托实例的对象,绝不是调用委托函数的对象。这一点我们通过如下例子来说明.

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace DelagateInstanceCall
{
    //DelegateContainer是定义委托类型DelMethod和委托实例delMethod的类
    class DelegateContainer
    {
        public delegate void DelMethod();//定义一个无参数且无返回值的委托类型DelMethod

        public DelMethod delMethod;//定义委托类型DelMethod的委托实例delMethod

        public int i = 100;//定义一个int类型的变量i在类DelegateContainer之中,赋值100
    }

    //MethodDemo是定义委托函数DisplayMethod的类
    class MethodDemo
    {
        protected int i = 200;//定义一个int类型的变量i在类MethodDemo之中,赋值200

        //定义委托函数DisplayMethod
        pub

以上是关于浅谈C#委托和事件(转载)的主要内容,如果未能解决你的问题,请参考以下文章

转载:C# 中的委托

c#委托

浅谈 C#委托

对“xxx”类型的已垃圾回收委托进行了回调。这可能会导致应用程序崩溃损坏和数据丢失。向非托管代码传递委托时,托管应用程序必须让这些委托保持活动状态,直到确信不会再次调用它们。 错误解决一例。(代码片段

转载:C#中事件的由来

C#事件