.Net中的委托与事件(DelegateEvent)

委托

委托的简单运用

一个委托类型定义了该类型的实例能调用的一类方法,这些方法含有同样的返回类型和同样的参数(形参和个数相同)。委托和接口一样,可以定义在类的外部。如下定义的一个委托类型 - Calculator:

	delegate int Calculator (int x);

此委托适用任何有着 int 返回类型和一个 int 类型参数的方法、如:

	static int Double (int x) 
	{
		return x * 2;
	}

创建一个委托实例、将该次方法赋值给委托实例:

	Calculator c = new Calculator(Double);
	//简写
	Calculator cc = Double;

这个方法可以通过委托调用

	int result = c(2);

完整案例:

class Program
    {
        delegate int Calculator(int x);
        static void Main(string[] args)
        {

            Calculator c = new Calculator(Double);

            Calculator cc = Double;

            int result = c(2);

            Console.WriteLine(result);
        }


        static int Double(int x)
        {
            return x * 2;
        }
    }

效果展示:
在这里插入图片描述

用委托实现插件式编程

我们可以利用 “委托是一个能把方法作为参数传递的对象” 这一特点,来实现一种插件编程
例如,我们又一个Utility类,这个类实现一个通用方法(Calculate),用来执行任何又一个整型参数和整形返回值的方法。如下:

	delegate int Calculator(int num;
	public class program
	{
		static int Double(int num)
		{
			return num * 2;
		}
		static void Main(string[] args)
		{
			int[] values = { 1, 2, 3, 4 };
                UtiLity.Calculate(values, Double);

                foreach (int i in values)
                {
                    Console.Write(i + " ");
                    Console.ReadKey();
                }
		}
	}
	public class UtiLity
    {
		public static void Calculate(int[] values, Calculator c)
		{
			for (int i = 0; i < values.Length; i++)
			{
				values[i] = (values[i]);
			}
		}
	}

这个例子中的 Utility 是固定不变的,程序实现了整数的 Double 功能。我们可以把这个 Double 方法看作是一个插件,如果将来还要实现诸多如平方,求立方的算计,我们只需向程序中不断添加插件就可以了。

如果 Double 方法是临时的,只调用一次,若在整个程序中不会又第二次调用,那么我们可以在Main方法中更简洁更灵活的使用这种插件式编程,无需先定义方法,使用Lamda表达式即可,如:

	Utility.Calculate(values,x => x * 2);

效果展示:
在这里插入图片描述

多播委托

所有的委托实例都又多播的功能。所谓多播,就像是一群程序员在瞬聘网填好了求职意向后,某天有个公司发布了一个和这些程序员求职意向刚好相匹配的工作,然后这些求职者都被通知了 - “有一份好工作招人啦,你们可以直接申请去上班了!”。
也就是说,一个委托实例不仅可以指向一个方法,还可以指向多个方法。如:

Action act = new Action(DoNothing);//“+=” 用来添加,同理“-=”用来移除。
                act += DoNothing;//act -= DoNothing 
                act.Invoke();

public static void  DoNothing()
        {
            Console.WriteLine("This is DoNothing");
        }

调用时,按照方法被添加的顺序依次执行。注意,对于委托,+= 和 -= 对null是不会报错的,如:

	Action dd=null;
    dd += DoNothing;// 相当于Action d = DoNothing;

为了更好的理解多播在实际开发中的应用,我用模拟瞬聘网的职位匹配小工具来做示例。在职位匹配过程中会有一段处理时间,所以在执行匹配的时候要能看到执行的进度,而且还要把执行的进度和执行情况写到日志文件中。在处理完一个步骤时,将分别执行两个方法来显示和记录执行进度。
我们先定义一个委托(ProgressReporter),然后定义一个匹配方法(Match)来执行该委托中的所有方法。如下:

	public delegate void ProgressReporter(int percentComplete);
	public class UtiLity {
    public static void Match(ProgressReporter p)
        {
            if (p != null)
            {
                for (int i = 0; i <= 10; i++)
                {
                    p(i * 10);
                    System.Threading.Thread.Sleep(100);
                }
            }
        }

然后我们需要两个监视进度的方法,一个把进度写到Console,另一个把进度写到文件。如下:

	class Program {
    static void Main(string[] args) {
        ProgressReporter p = WriteProgressToConsole;
                p += WriteProgressToFile;
                UtiLity.Match(p);
                Console.WriteLine("Done.");
                Console.ReadKey();
    }

    static void WriteProgressToConsole(int percentComplete) {
        Console.WriteLine(percentComplete+"%");
    }
    static void WriteProgressToFile(int percentComplete) {
        System.IO.File.AppendAllText("progress.txt", percentComplete + "%");
    }
}

运行结果:
在这里插入图片描述

静态方法和实例方法对于委托的区别

当一个类的实例的方法被赋给一个委托对象时,在上下文中不仅要维护这个方法,还要维护这个方法所在的实例。System.Delegate 类的Target属性指向的就是这个实例。举个例子:

	class Program {
    static void Main(string[] args) {
        X x = new X();
        ProgressReporter p = x.InstanceProgress;
        p(1);
        Console.WriteLine(p.Target == x); // True
        Console.WriteLine(p.Method); // Void InstanceProgress(Int32)    }

    static void WriteProgressToConsole(int percentComplete) {
        Console.WriteLine(percentComplete+"%");
    }
    static void WriteProgressToFile(int percentComplete) {
        System.IO.File.AppendAllText("progress.txt", percentComplete + "%");
    }
	}
	class X {
    	public void InstanceProgress(int percentComplete) {
        // do something    }
	}

运行结果:
在这里插入图片描述

但对于静态方法,System.Delegate 类的Target属性是Null,所以将静态方法赋值给委托时性能更优。

泛型委托

如果你知道泛型,那么就很容易理解泛型委托,说白了就是含有泛型参数的委托,例如:

	public delegate T Calculator<T> (T arg);

我们可以把前面的例子改成泛型的例子,如下:

public delegate T Calculator<T>(T arg);
class Program {

    static int Double(int x) { return x * 2; }
    static void Main(string[] args) {
        int[] values = { 1, 2, 3, 4 };
        UtiLity.Calculates(values, Double);

        foreach (int i in values)
            Console.Write(i + " "); // 2 4 6 8
        Console.ReadKey();
         }
}
class Utility {
    public static void Calculates<T>(T[] values, Calculator<T> c) {
        for (int i = 0; i < values.Length; i++)
            values[i] = c(values[i]);
    }
}

Func 和 Action 委托

有了泛型委托,就有了一能适用于任何返回类型和任意参数(类型和合理的个数)的通用委托,Func 和 Action。
我们上面的Calculator泛型委托就可以删掉了,示例就可以更简洁了:

public static void Calculate<T>(T[] values, Func<T,T> c) {
    for (int i = 0; i < values.Length; i++)
        values[i] = c(values[i]);
}

Func 和 Action 委托,除了ref参数和out参数,基本上能适用于任何泛型委托的场景,非常好用。

委托的兼容

  1. 委托的类型兼容
delegate void D1();delegate void D2();
...
D1 d1 = Method1;
D2 d2 = d1;

下面是被允许的:

D2 d2 = newD2 (d1);

对于具体相同的目标方法的委托是被视为相等的:

delegate void D();
...
D d1 = Method1;
D d2 = Method1;
Console.WriteLine (d1 == d2); // True

同理,对于多播委托,如果含有相同的方法和相同的顺序,也被视为相等。

  1. 参数类型兼容
    在OOP中,任何使用父类的地方均可以用子类代替,这个OOP思想对委托的参数同样有效。如:
delegate void StringAction(string s);class Program {
    static void Main() {
        StringAction sa = new StringAction(ActOnObject);
        sa("hello");
    }
    static void ActOnObject(object o) {
        Console.WriteLine(o); // hello
    }
}
  1. 返回值类型兼容
    道理和参数类型兼容一样:
delegate object ObjectRetriever();class Program {
    static void Main() {
        ObjectRetriever o = new ObjectRetriever(RetriveString);
        object result = o();
        Console.WriteLine(result); // hello    }
    static string RetriveString() { return "hello"; }
}

事件

当我们使用委托场景时,我们很希望有这样两个角色出现:广播者和订阅者。我们需要这两个角色来实现订阅和广播这种很常见的场景。
广播者这个角色应该有这样的功能:包括一个委托字段,通过调用委托来发出广播。而订阅者应该有这样的功能:可以通过调用 += 和 -= 来决定何时开始或停止订阅。
事件就是描述这种场景模式的一个词。事件是委托的一个子集,为了满足“广播/订阅”模式的需求而生。

事件的基本使用

声明一个事件很简单,只需在声明一个委托对象时加上event关键字就行。如下:

public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
public class MyEvent
    {
        public event PriceChangedHandler PriceChanged;
    }

事件的使用和委托完全一样,只是多了些约束。下面是一个简单的事件使用例子:

public delegate void PriceChangedHandler (decimal oldPrice, decimal newPrice);
public class MyEvent {
    decimal price;
    public event PriceChangedHandler PriceChanged;
    public decimal Price {
        get { return price; }
        set {
            if (price == value) return;
            decimal oldPrice = price;
            price = value;
            // 如果调用列表不为空,则触发。
            if (PriceChanged != null)
                PriceChanged(oldPrice, price);
        }
    }
}
class Program {
    static void Main() {
        MyEvent myevent = new MyEvent() { Price = 5288 };
                // 订阅事件
                myevent.PriceChanged += MyEvent_PriceChanged;

                // 调整价格(事件发生)
                myevent.Price = 3999;

                Console.ReadKey();
    }

    static void MyEvent_PriceChanged(decimal oldPrice, decimal price)
        {
            Console.WriteLine("年终大促销,iPhone 6 只卖 " + price + " 元, 原价 " + oldPrice + " 元,快来抢!");

        }
}

运行结果:
在这里插入图片描述
有人可能会问,如果把上面的event关键字拿掉,结果不是一样的吗,到底有何不同?
没错可以用事件的地方就一定可以用委托代替。

但事件有一系列规则和约束用以保证程序的安全可控,事件只有 += 和 -= 操作,这样订阅者只能有订阅或取消订阅操作,没有权限执行其它操作。如果是委托,那么订阅者就可以使用 = 来对委托对象重新赋值(其它订阅者全部被取消订阅),甚至将其设置为null,甚至订阅者还可以直接调用委托,这些都是很危险的操作,广播者就失去了独享控制权。
事件保证了程序的安全性和健壮性。

事件的标准模式

.NET 框架为事件编程定义了一个标准模式。设定这个标准是为了让.NET框架和用户代码保持一致。System.EventArgs是标准模式的核心,它是一个没有任何成员,用于传递事件参数的基类。
按照标准模式,我们对于上面的iPhone6示例进行重写。首先定义EventArgs:

public class PriceChangedEventArgs : EventArgs {
    public readonly decimal OldPrice;
    public readonly decimal NewPrice;
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}

然后为事件定义委托,必须满足以下条件:

  • 必须是 void 返回类型
  • 必须有两个参数,且第一个是object类型,第二个是EventArgs类型(的子类)
  • 它的名称必须以EventHandler结尾。

由于考虑到每个事件都要定义自己的委托很麻烦,.NET 框架为我们预定义好一个通用委托System.EventHandler:

public delegate void EventHandler<TEventArgs> (object source, TEventArgs e) where TEventArgs : EventArgs;

如果不使用框架的EventHandler,我们需要自己定义一个:

public delegate void PriceChangedEventHandler (object sender, PriceChangedEventArgs e);

如果不需要参数,可以直接使用EventHandler(不需要)。有了EventHandler,我们就可以这样定义示例中的事件:

public class MyEvent {
    ...
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    ...
}

最后,事件标准模式还需要写一个受保护的虚方法来触发事件,这个方法必须以On为前缀,加上事件名(PriceChanged),还要接受一个EventArgs参数,如下:

public class MyEvent {
    ...
    public event EventHandler<PriceChangedEventArgs> PriceChanged;
    protected virtual void OnPriceChanged(PriceChangedEventArgs e) {
        if (PriceChanged != null) PriceChanged(this, e);
    }
    ...
}

下面给出完整示例:

public class PriceChangedEventArgs : System.EventArgs {
    public readonly decimal OldPrice;
    public readonly decimal NewPrice;
    public PriceChangedEventArgs(decimal oldPrice, decimal newPrice) {
        OldPrice = oldPrice;
        NewPrice = newPrice;
    }
}
public class MyEvent {
    decimal price;
    public event EventHandler<PriceChangedEventArgs> PriceChanged;

    protected virtual void OnPriceChanged(PriceChangedEventArgs e) {
        if (PriceChanged != null) PriceChanged(this, e);
    }

    public decimal Price {
        get { return price; }
        set {
            if (price == value) return;
            decimal oldPrice = price;
            price = value;
            // 如果调用列表不为空,则触发。
            if (PriceChanged != null)
                OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
        }
    }
}
class Program {
    static void Main() {
        MyEvent myEvent = new MyEvent() { Price = 5288M };
                // 订阅事件
                myEvent.PriceChanged += MyEvent_PriceChanged;

                // 调整价格(事件发生)
                myEvent.Price = 3999;
                Console.ReadKey();
    }

    static void MyEvent_PriceChanged(object sender, PriceChangedEventArgs e) {
        Console.WriteLine("年终大促销,iPhone 6 只卖 " + e.NewPrice + " 元, 原价 " + e.OldPrice + " 元,快来抢!");
    }
}

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值