.Net delegate 那点事

本文通过一个虚构的故事背景,详细解释了委托的基本概念及其在C#中的应用。从紧耦合问题出发,逐步介绍了接口、委托、事件等机制,以及异步通知的多种方式,最后扩展到了策略模式、泛型委托等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Delegate那点事


1,紧耦合
从前,在一块奇异的土地上,有个工人名叫彼得,他非常勤奋,对他的老板总是百依百顺。但是他的老板是个吝啬的人,从不信任别人,坚决要求随时知道彼得的工作进度,以防止他偷懒。但是彼得又不想让老板呆在他的办公室里站在背后盯着他,于是就对老板做出承诺:无论何时,只要我的工作取得了一点进展我都会及时让你知道。彼得通过周期性地使用“回调”他的老板来实现他的承诺,如下:
class Worker {
    public void Advise(Boss boss) { _boss = boss; }
    public void DoWork() {
        Console.WriteLine(“工作:工作开始”);
        if( _boss != null )_boss.WorkStarted();

       Console.WriteLine(“工作: 工作进行中”);
        if( _boss != null )_boss.WorkProgressing();

       Console.WriteLine("“工作: 工作完成”");
        if( _boss != null ) {
            int grade =_boss.WorkCompleted();
           Console.WriteLine(“工人的工作得分=” + grade);
    }
}
private Boss _boss;
}

classBoss {
    public void WorkStarted() { /* 老板不关心。*/ }
    public void WorkProgressing() { /*老板不关心。*/ }
    public int WorkCompleted() {
        Console.WriteLine(“时间差不多!”);
        return 2; /* 总分为10*/
    }
}

classUniverse {
    static void Main() {
        Worker peter = new Worker();
        Boss boss = new Boss();
        peter.Advise(boss);
        peter.DoWork();

       Console.WriteLine(“Main: 工人工作完成”);
        Console.ReadLine();
    }
}


2.接口

现在,彼得成了一个特殊的人,他不但能容忍吝啬的老板,而且和他周围的宇宙也有了密切的联系,以至于他认为宇宙和周围的其他事物也会对他的工作进度也感兴趣。

不幸的是,他必须也给宇宙或周围的事物添加一个回调函数Advise来实现同时向他老板和宇宙报告工作进度。

彼得想要把潜在的通知的列表和这些通知的实现方法分离开来,于是他决定把方法分离为一个接口:

interfaceIWorkerEvents {
    void WorkStarted();
    void WorkProgressing();
    int WorkCompleted();
}

classWorker {
    public void Advise(IWorkerEvents events) { _events = events;}
    public void DoWork() {
        Console.WriteLine(“工作:工作开始”);
        if( _events != null )_events.WorkStarted();

       Console.WriteLine(“工作: 工作进行中”);
        if(_events != null )_events.WorkProgressing();

       Console.WriteLine("“工作: 工作完成”");
        if(_events != null ) {
            int grade =_events.WorkCompleted();

           Console.WriteLine(“工人的工作得分=” + grade);
            }
    }
    private IWorkerEvents _events;
}

classBoss : IWorkerEvents {
    public void WorkStarted() { /* 老板不关心。*/ }
    public void WorkProgressing() { /* 老板不关心。*/ }
    public int WorkCompleted() {
        Console.WriteLine(“时间差不多!”);
        return 3; /* 总分为10*/
    }
}

classCollage : IWorkerEvents {
    public void WorkStarted() { /* 同事不关心。?*/ }
    public void WorkProgressing() { /*同事关心。?不能一起打牌了*/ }
    public int WorkCompleted() {
        Console.WriteLine(“太好了!下班了”);
        return 3; /* 总分为10,不关心评分*/
    }
}

classUniverse {
    static void Main() {
        Worker peter = new Worker();
        Boss boss = new Boss();
        peter.Advise(boss);
        peter.DoWork();

       Console.WriteLine(“Main: 工人工作完成”);
        Console.ReadLine();
    }
}



不幸的是,每当彼得忙于通过接口的实现和老板交流时,就没有机会及时通知宇宙和同事了。(其实现在他也不知道怎么通知宇宙,呼叫上帝???)

至少他应该忽略身在远方的老板的引用,好让其他实现了IWorkerEvents的对象得到他的工作报告。

 

3,委托

他的老板还是抱怨得很厉害。“彼得!”他老板吼道,“你为什么在工作一开始和工作进行中都来烦我?!我不关心这些事件。你不但强迫我实现了这些方法,而且还在浪费我宝贵的工作时间来处理你的事件,特别是当我外出的时候更是如此!你能不能不再来烦我?”

于是,彼得意识到接口虽然在很多情况都很有用,但是当用作事件时,“粒度”不够好。他希望能够仅在别人想要时才通知他们,于是他决定把接口的方法分离为单独的委托,每个委托都像一个小的接口方法:

来看委托的定义

委托定义及基本知识

回调(callback)函数是Windows编程的一个重要部分。如果您具备C或C++编程背景,就曾在许多WindowsAPI中使用过回调。VB添加了AddressOf关键字后,开发人员就可以利用以前一度受到限制的API了。回调函数实际上是方法调用的指针,也称为函数指针,是一个非常强大的编程特性。.NET以委托的形式实现了函数指针的概念。它们的特殊之处是,与C函数指针不同,.NET委托是类型安全的。

基本上说当要把方法传送给其他方法时,需要使用委托。要了解它们的含义,可以看看下面的代码:inti = int.Parse("99");

我们习惯于把数据作为参数传递给方法,如上面的例子所示。所以,给方法传送另一个方法听起来有点奇怪。而有时某个方法执行的操作并不是针对数据进行的,而是要对另一个方法进行操作,这就比较复杂了。在编译时我们不知道第二个方法是什么,这个信息只能在运行时得到,所以需要把第二个方法作为参数传递给第一个方法。

委托定义   -----delegate string GetAString();

其语法类似于方法的定义,但没有方法体,定义的前面要加上关键字delegate。因为定义委托基本上是定义一个新类,所以可以在定义类的任何地方定义委托,既可以在另一个类的内部定义,也可以在任何类的外部定义,还可以在命名空间中把委托定义为顶层对象。根据定义的可见性,可以在委托定义上添加一般的访问修饰符:public、private和protected等:publicdelegate string GetAString(); 实际上,“定义一个委托”是指“定义一个新类”。委托实现为派生于基类System.MulticastDelegate的类,System.MulticastDelegate又派生于基类System.Delegate。

实例话委托-- GetAString firstStringMethod = new GetAString(x.ToString);

delegatevoid WorkStarted();
delegate void WorkProgressing();
delegate int WorkCompleted();

classWorker {
    public void DoWork() {
        Console.WriteLine(“工作:工作开始”);
        if( started != null ) started();

       Console.WriteLine(“工作: 工作进行中”);
        if( progressing != null )progressing();

       Console.WriteLine("“工作: 工作完成”");
        if( completed != null ) {
            int grade =completed();
           Console.WriteLine(“工人的工作得分=” + grade);
        }
    }
    public WorkStarted started;
    public WorkProgressing progressing;
    public WorkCompleted completed;
}

classBoss {
    public int WorkCompleted() {
    Console.WriteLine("Better...");
    return 4; /* 总分为10 */
}
}

classCollage {
    public void WorkProgressing () {
    Console.WriteLine("busying.");
    return 4; /* 总分为10 */
}
}

 

classUniverse {
    static void Main() {
        Worker peter = new Worker();
        Boss boss = new Boss();
        peter.completed = newWorkCompleted(boss.WorkCompleted);

peter.Progressing = new WorkProgressing (Collage. WorkProgressing);
        peter.DoWork();

       Console.WriteLine(“Main: 工人工作完成”);
        Console.ReadLine();
    }
}


4,静态监听者

这样,彼得不会再拿他老板不想要的事件来烦他老板了,但是他还没有把宇宙放到他的监听者列表中。因为宇宙是个包涵一切的实体,看来不适合使用实例方法的委托(想像一下,实例化一个“宇宙”要花费多少资源…..),于是彼得就需要能够对静态委托进行挂钩,委托对这一点支持得很好:

classUniverse {
    static void WorkerStartedWork() {
        Console.WriteLine("Universenotices worker starting work");
    }

   static int WorkerCompletedWork() {
        Console.WriteLine("Universepleased with worker's work");
        return 7;
    }

   static void Main() {
        Worker peter = new Worker();
        Boss boss = new Boss();
        peter.completed = newWorkCompleted(boss.WorkCompleted);
        peter.started = newWorkStarted(Universe.WorkerStartedWork);
        peter.completed = newWorkCompleted(Universe.WorkerCompletedWork);
        peter.DoWork();

       Console.WriteLine(“Main: 工人工作完成”);
        Console.ReadLine();
    }
}


5.事件

不幸的是,宇宙太忙了,也不习惯时刻关注它里面的个体,它可以用自己的委托替换了彼得老板的委托。这是把彼得的Worker类的的委托字段做成public的一个无意识的副作用。同样,如果彼得的老板不耐烦了,也可以决定自己来激发彼得的委托if( peter.completed != null ) peter.completed();
彼得不想让这些事发生,他意识到需要给每个委托提供“注册”和“反注册”功能,这样监听者就可以自己添加和移除委托,但同时又不能清空整个列表也不能随意激发彼得的事件了。彼得并没有来自己实现这些功能,相反,他使用了event关键字让C#编译器为他构建这些方法:

classWorker {
...
    public event WorkStarted started;
    public event WorkProgressing progressing;
    public event WorkCompleted completed;
}


彼得知道event关键字在委托的外边包装了一个property,仅让C#客户通过+=和-=操作符来添加和移除,强迫他的老板和宇宙正确地使用事件。

staticvoid Main() {
    Worker peter = new Worker();
    Boss boss = new Boss();
    peter.completed += new WorkCompleted(boss.WorkCompleted);
    peter.started += new WorkStarted(Universe.WorkerStartedWork);
    peter.completed += newWorkCompleted(Universe.WorkerCompletedWork);
    peter.DoWork();

   Console.WriteLine(“Main: 工人工作完成”);
    Console.ReadLine();
}


6.“收获”所有结果

到这时,彼得终于可以送一口气了,他成功地满足了所有监听者的需求,同时避免了与特定实现的紧耦合。但是他注意到他的老板和宇宙都为它的工作打了分,但是他仅仅接收了一个分数。面对多个监听者,他想要“收获”所有的结果,于是他深入到代理里面,轮询监听者列表,手工一个个调用:

publicvoid DoWork() {
    ...
    Console.WriteLine("“工作: 工作完成”");
    if( completed != null ) {
        foreach( WorkCompleted wc incompleted.GetInvocationList() ) {
            int grade =wc();
           Console.WriteLine(“工人的工作得分=” + grade);
        }
    }
}


7.异步通知:A.激发& 忘掉

同时,他的老板和宇宙还要忙于处理其他事情,也就是说他们给彼得打分所花费的事件变得非常长:

classBoss {
    public int WorkCompleted() {
        System.Threading.Thread.Sleep(3000);
        Console.WriteLine("Better...");return 6; /* 总分为10*/
    }
}

classUniverse {
    static int WorkerCompletedWork() {
        System.Threading.Thread.Sleep(4000);
        Console.WriteLine("Universe ispleased with worker's work");
        return 7;
    }
    ...
}
很不幸,彼得每次通知一个监听者后必须等待它给自己打分,现在这些通知花费了他太多的工作事件。于是他决定忘掉分数,仅仅异步激发事件:

publicvoid DoWork() {
    ...
    Console.WriteLine("“工作: 工作完成”");
    if( completed != null ) {
        foreach( WorkCompleted wc incompleted.GetInvocationList() )
        {
           wc.BeginInvoke(null, null);
        }
    }
}

7.异步通知:B.轮询

这使得彼得可以通知他的监听者,然后立即返回工作,让进程的线程池来调用这些代理。随着时间的过去,彼得发现他丢失了他工作的反馈,他知道听取别人的赞扬和努力工作一样重要,于是他异步激发事件,但是周期性地轮询,取得可用的分数。

publicvoid DoWork() {
    ...
    Console.WriteLine("“工作: 工作完成”");
    if( completed != null ) {
        foreach( WorkCompleted wc incompleted.GetInvocationList() ) {
            IAsyncResultres = wc.BeginInvoke(null, null);
            while(!res.IsCompleted )

{

Console.WriteLine(“doanthother job“);

System.Threading.Thread.Sleep(100);

}
            int grade =wc.EndInvoke(res);
           Console.WriteLine(“工人的工作得分=” + grade);
        }
    }
}

7.异步通知:C.等待

IAsyncResult.AsyncWaitHandle属性获取用于等待异步操作完成的WaitHandle

WaitHandle.WaitOne方法阻塞当前线程,直到当前的WaitHandle收到信号

使用WaitHandle,则在异步调用完成之后,但在通过调用EndInvoke结果之前,可以执行其他处理

publicvoid DoWork() {
    ...
    Console.WriteLine("“工作: 工作完成”");

   if (completed != null)

   {

      foreach (WorkCompleted wc incompleted.GetInvocationList())

         {

            IAsyncResult res =wc.BeginInvoke(null, null);

             If(!res.AsyncWaitHandle.WaitOne(100,true));

{

Console.WriteLine(“doanthother job“);

}

             int grade = wc.EndInvoke(res);

             Console.WriteLine("worker'sgrade:" + grade);

          }

     }

}

 


7 异步通知:D.委托

不幸地,彼得有回到了一开始就想避免的情况中来,比如,老板站在背后盯着他工作。于是,他决定使用自己的委托作为他调用的异步委托完成的通知,让他自己立即回到工作,但是仍可以在别人给他的工作打分后得到通知:

   public void DoWork() {
        ...
        Console.WriteLine("“工作:工作完成”");
        if( completed != null ) {
            foreach(WorkCompleted wc in completed.GetInvocationList() ) {
               wc.BeginInvoke(new AsyncCallback(WorkGraded), wc);
            }
        }
    }

   private void WorkGraded(IAsyncResult res) {
        WorkCompleted wc =(WorkCompleted)res.AsyncState;
        int grade = wc.EndInvoke(res);
        Console.WriteLine(“工人的工作得分=”+ grade);
    }


宇宙中的幸福

彼得、他的老板和宇宙最终都满足了。彼得的老板和宇宙可以收到他们感兴趣的事件通知,减少了实现的负担和非必需的往返“差旅费”。彼得可以通知他们,而不管他们要花多长时间来从目的方法中返回,同时又可以异步地得到他的结果。彼得知道,这并不*十分*简单,因为当他异步激发事件时,方法要在另外一个线程中执行,彼得的目的方法完成的通知也是一样的道理。

 

现在我们应该理解delegate的存在和它是如何帮助彼得的了。除了这个让我们再来看看其他的应用吧。

启动线程

   在C#中,可以告诉计算机并行运行某些新的执行序列。这种序列就称为线程,在基类System.Threading.Thread的一个实例上使用方法Start(),就可以开始执行一个线程。如果要告诉计算机开始一个新的执行序列,就必须说明要在哪里执行该序列。必须为计算机提供开始执行的方法的细节,即Thread.Start()方法必须带有一个参数,该参数定义了要由线程调用的方法。

usingSystem;

usingSystem.Threading;

 

classTest

{

    static void Main()

    {

        ThreadStart threadDelegate = newThreadStart(Work.DoWork);

        Thread newThread = newThread(threadDelegate);

        newThread.Start();

        Work w = new Work();

        w.Data = 42;

        threadDelegate = newThreadStart(w.DoMoreWork);

        newThread = new Thread(threadDelegate);

        newThread.Start();

    }

}

 

classWork

{

    public static void DoWork()

    {

        Console.WriteLine("Static threadprocedure.");

    }

    public int Data;

    public void DoMoreWork()

    {

        Console.WriteLine("Instance threadprocedure. Data={0}", Data);

    }

}

 

 

策略模式(StrategyPattern)的delegate实现

Strategy模式是对算法的封装。即使是一个计算行为,如果其实现有其多样性,为达到易扩展的目的,我们可以将其抽象出来,以接口的形式来定义。利用了面向对象的多态性,在运行时,可以灵活的变更这个算法的具体实现。

例如编写一个类,它带有一个对象数组,并把它们按升序排列。但是,排序的部分过程会涉及到重复使用数组中的两个对象,比较它们,看看哪一个应放在前面。如果要编写的类必须能给任何对象数组排序,那么程这个类必须能够依据比较类型的不同采用不同的策略去比较。

另一个例子是以税收计算为例,假定税收策略分为个人所得税,和企业所得税。根据策略模式,将税收策略抽象为接口ITaxStrategy:

publicinterface ITaxStrategy

{

doubleCalculate(double income);

}

各种税收策略均实现该类。

publicclass PeronalTaxStrategy:ITaxStrategy

{

     public double Calculate(double income)

     {

           //实现;

     }

}

publicclass EnterpriseTaxStrategy:ITaxStrategy

{

     public double Calculate(double income)

     {

           //实现;

     }

}

如果此时有一个公共的类,提供税收的相关操作,其中就包括计算所得税的方法:

publicclass TaxOp

{

     private ITaxStrategy strategy;

     public TaxOp(ITaxStrategy strategy)

     {

           this.strategy = strategy;

     }

 

     public void ChangeStrategy(ITaxStrategy strategy)

      {

           this.strategy = strategy

      }

 

     public double GetTax(double income)

     {

            return strategy.Calculate(income);

     }

}

客户端调用:

publicclass App

{

     public static void Main(string[] args)

     {

           TaxOp op = new TaxOp(new PersonalTaxStrategy());

           Console.WriteLine(“The Personal Tax is :{0}”,op.GetTax(1000));

}

}

 

 

为了达到上面的目的,我们也可以将原来抽象出来的接口修改为委托:

 

publicdelegate double CalculateTax(double income);

对于个人所得税和企业所得税的实现,相应修改为:

publicclass Tax

{

     public static double CalculatePersonalTax(double income)

     {

           //实现;

     }

     public static double CalculateEnterpriseTax(double income)

     {

           //实现;

     }

}

税收的公共类则修改如下:

publicclass TaxOp

{

     private CalculateTax calDel;

     public TaxOp(Calculate calDel)

     {

           this.calDel = calDel;

     }

     public double GetTax(double income)

     {

            return calDel(income);

     }

}

客户端的调用:

publicclass App

{

     public static void Main(string[] args)

     {

           TaxOp op = new TaxOp(new CalculateTax(Tax.CalculatePersonalTax));

           Console.WriteLine(“The Personal Tax is :{0}”,op.GetTax(1000));

}

}

这两种方案的目的都是相同的,就是将算法和算法的使用者分开解耦,达到松耦合高聚合的目的。有过c/c++使用经验的人,可能将这两种方案的区别看成一个是是面向对象的,一个是面向过程的,前者(使用Interfaceclass 或者abstract class作为策略父类)是将方法封装在对象内部,而后者(使用Delegate)则直接对方法进行操作,同时又利用delegate委托来实现扩展,为什么呢,那是因为Delegate的使用方法和和c/c中的++函数指针是一样的,它的行为本质和函数指针一样。但是从OO来看,Delegate可以看成是对具有相同特性的行为(具备相同输入和输出)的一簇函数说进行的普遍意义上的抽象,从这个意义讲,委托是是面向对象的;从微软在c#2.0对委托的的输入和输出的拓展中,更可以看到委托OO的思想。

泛型委托 (2.0)

          声明方式:

          public delegate void Del<T>(Titem);

          public static void Notify(int i) { }

 

          Del<int> m1 = newDel<int>(Notify);

委托的逆变和协变 (2.0)

          协变允许我们构建委托能指向返回类型及相关继承体系。

          逆变允许构建委托指向多个方法,只要方法参数是存在继承关系的对象。

 

匿名方法(2.0)

到前为止,要想使委托工作,方法必须已经存在(即委托是用方法的签名定义的)。但使用委托还有另外一种方式:即通过匿名方法。匿名方法是用作委托参数的一个代码块。

用匿名方法定义委托的语法与前面的定义并没有什么区别。但在实例化委托时,就有区别了。下面是一个非常简单的控制台应用程序,说明了如何使用匿名方法:

namespaceConsoleApplication1

{

 class Program

 {

   delegate string delegateTest(string val);

 

   static void Main(string[] args)

   {

     string mid = ", middle part,";

 

     delegateTest anonDel = delegate(string param)

     {

       param += mid;

       param += " and this was added to the string.";

       return param;

     };

Console.WriteLine(anonDel("Startof string"));

    }

 }

}

委托delegateTest定义为一个类级变量,它带一个字符串参数。有区别的是Main方法。在定义anonDel时,不是传送已知的方法名,而是使用一个简单的代码块:

{

 param += mid;

 param += " and this was added to the string.";

 return param;

};

可以看出,该代码块使用方法级的字符串变量mid,该变量是在匿名方法的外部定义的,并添加到要传送的参数中。接着代码返回该字符串值。在调用委托时,把一个字符串传送为参数,将返回的字符串输出到控制台上。

匿名方法的优点是减少了系统开销。方法仅在由委托使用时才定义。在为事件定义委托时,这是非常显然的。这有助于降低代码的复杂性,尤其是定义了好几个方法时,代码会显得比较简单。

在使用匿名方法时,必须遵循两个规则。在匿名方法中不能使用跳转语句跳到该匿名方法的外部,反之亦然:匿名方法外部的跳转语句不能跳到该匿名方法的内部。

在匿名方法内部不能访问不安全的代码。另外,也不能访问在匿名方法外部使用的ref和out参数。但可以使用在匿名方法外部定义的其他变量

 

Lambda运算符(3.0)和LINQ

Lambda 表达式”是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式树类型(不再这里讨论)。适用于匿名方法的所有限制也适用于Lambda 表达式。

所有Lambda 表达式都使用 Lambda 运算符=>,该运算符读为“goesto”。该 Lambda 运算符的左边是输入参数(如果有),右边包含表达式或语句块。Lambda 表达式 x => x * x 读作“xgoes to x times x”。可以将此表达式分配给委托类型

delegateint del(int i);

staticvoid Main(string[] args)

{

    del myDelegate = x => x * x;

    int j = myDelegate(5); //j = 25

}

 

这些语句可以通过匿名函数表达为:

delegateint del(int i);

staticvoid Main(string[] args)

{

delmyDelegate = delegate(int x)

{

          Return x*x;

}

    int j = myDelegate(5); //j = 25

}

Lambda 在基于方法的LINQ 查询中用作标准查询运算符方法(如 Where)的参数

 。

LINQ表达:

List<string>fruits =

   new List<string> { "apple","passionfruit", "banana", "mango",

                               "orange", "blueberry", "grape","strawberry" };

IEnumerable<string>query = from f in fruits

                             where f. Length< 6 select f

foreach(string fruit in query)

       {

                Console.WriteLine(fruit);

     }

 

标准lambda表达:

            List<string> fruits =

                new List<string> {"apple", "passionfruit", "banana","mango",

                                "orange","blueberry", "grape", "strawberry" };

 

            IEnumerable<string> query =fruits.Where(fruit => fruit.Length < 6);

 

            foreach (string fruit in query)

            {

                Console.WriteLine(fruit);

            }

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值