C#学习笔记(三)—–C#高级特性中的委托与事件(上)

C#高级特性中的委托与事件(上)


委托

  • 委托是一种知道如何调用方法的对象。委托类型定义了委托对象可以调用的方法。具体的,它定义了方法的签名。包括方法的返回类型和方法的参数类型和数量。
  • 委托的实例实际上是调用者的代表,调用者先调用委托,委托再调用方法。这种间接的方式可以将调用这和委托分开。提示:技术上,我们把没有括号和参数的方法叫做方法组,如果一个方法被重载,那我们可以根据方法的参数类型和数量来决定使用哪一个方法。
  • 从C#2.0开始,把一个方法组赋值给一个委托的变量可以创建一个委托实例:
public delegate DoSomething();
public void Do()
{
Console.WriteLine(.......)
}
DoSomething=Do;
  • 委托实际是特殊的类。虽然C#标准没有确切规定类的层次结构应该是怎样的,但委托必须直接或间接地派生自System.Delegate。事实上,.NET中的委托类型总是派生自System.MulticastDelegate,后者又从System.Delegate派生:

委托
第一个属性属于(System.Reflection.MethodInfo)类型,MethodInfo描述了特定方法的签名,包括方法名称、参数和返回类型。除了MethodInfo,委托还需要一个对象实例,其中包含了要调用的方法。这正是第二个属性Target的作用。在静态方法的情况下,Target对应于类型自身。

  • 实例方法和静态方法的Target属性:当委托对象指向一个实例方法时,委托对象不仅要维护这个方法的引用,而且还会维护到这个实例方法所属类的引用。System.Delegate的Target属性可以表示这个实例方法所属的类。

  • 用委托写插入式的方法:一个委托变量会在运行时被指定一个方法。这个特性对于编写插件式的方法特别有用,本例中有一个名为Transform的公共方法,他对整型数组的每个元素进行变换,为了指定变换的方式,定义了一个名为TransFormer的委托参数:

public delegate int Transformer (int x);//定义的委托会在运行时与Util类中的Transform进行绑定
class Util
{
public static void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
class Test
{
static void Main()
{
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Hook in the Square method
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
}
static int Square (int x) { return x * x; }
}
  • 多播委托:所有的委托实例都有多播的能力,意思是一个委托可以”关联“多个目标方法。委托的+=操作符表示为向委托注册订阅者,通过委托向订阅者发布消息(委托为事件发布者)。用+和+=联合多个委托实例,例如:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;

现在d不仅可以调用Some Method1,还可以调用SomeMethod2。委托按照添加的顺序依次被触发。运算符-和-=可以从左边的委托操作数中一出右边的委托操作数。

d-=SomeMethod1;

现在d只能触发SomeMethod2。
在值为null的delegate上面执行+=相当于为委托赋新值。

SomeDelegate d = null;
d += SomeMethod1;//当SomeDelegate为null时等价于d = SomeMethod1;

同样的,为只有一个目标方法的委托上面使用-或者-=相当于给委托赋予一个null值:

  • 提示:委托是不可变的,因此调用-=或+=操作符实质上是新创建一个委托实例,并把他赋值给已有变量。
  • 提示:所有的委托类型都派生自System.MulticastDelegate,后者派生自System.Delegate。C#编译器使用+, -,+=, 和 -=构成System.Delegate类的静态Combine和Remove方法。
  • 多播委托的实例
    假如有一个需要运行很长时间的例程,里面的System.Threading.Thread.Sleep (100);用来模拟运行很长时间的程序。
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // 调用委托
System.Threading.Thread.Sleep (100); // 模拟长时间的运行
}
}
}

为了监视执行进度,在Main方法中建立一个多播委托实例p,分别用两个方法来对程序的进度进行监视。

class Test
{
static void Main()
{
X x = new X();
ProgressReporter p = x.InstanceProgress;
p(99); // 99
Console.WriteLine (p.Target == x); // True
Console.WriteLine (p.Method); // Void InstanceProgress(Int32)
}
}
class X
{
public void InstanceProgress (int percentComplete)
{
Console.WriteLine (percentComplete);
}
}
  • 委托中的泛型:委托可以引入类型参数:public delegate T Transformer<T> (T arg);
  • 根据上面的定义,可以定义一个泛化的方法来对任何类型都生效:
public class Util
{
public static void Transform<T> (T[] values, Transformer<T> t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
}
class Test
{
static void Main()
{
int[] values = { 1, 2, 3 };
Util.Transform (values, Square); // Hook in Square
foreach (int i in values)
Console.Write (i + " "); // 1 4 9
}
static int Square (int x) { return x * x; }
}
  • Func和Action委托:Func和Action可以代表绝大多数的方法,它们定义在System中:
delegate TResult Func <out TResult> ();
delegate TResult Func <in T, out TResult> (T arg);
delegate TResult Func <in T1, in T2, out TResult> (T1 arg1, T2 arg2);
... and so on, up to T16
delegate void Action ();
delegate void Action <in T> (T arg);
delegate void Action <in T1, in T2> (T1 arg1, T2 arg2);
... and so on, up to T16

这些委托现在使用的非常普遍,前面举例的Transformer委托,可以用一个Func

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

只有ref/out和指针没有在这些委托中涉及到。
在.net framework2.0之前,大多数代码使用的还是自定义的委托类型,因为那个时候还没有泛型,也不存在Func和Action。
- 委托和接口:能用委托解决的问题,都能用接口解决。例如:

public interface ITransformer
{
int Transform (int x);
}
public class Util
{
public static void TransformAll (int[] values, ITransformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t.Transform (values[i]);
}
class Squarer : ITransformer
{
public int Transform (int x) { return x * x; }
}
...
static void Main()
{
int[] values = { 1, 2, 3 };
Util.TransformAll (values, new Squarer());
foreach (int i in values)
Console.WriteLine (i);
}
  • 在下面的情形中,如果有一条或者多条符合情况,委托可能是比接口更好的选择:
    ①接口内只定义一个方法
    ②需要多播能力
    ③订阅者需要多次实现接口
    订阅者为了实现不同的计算,可能会提供多个类来实现接口(比如平方或者立方转换),如果使用接口,类只能实现一次接口,所以必须写多个类来重写接口方法来实现不同的功能,这样看起来很傻:
class Squarer : ITransformer
{
public int Transform (int x) { return x * x; }
}
class Cuber : ITransformer
{
public int Transform (int x) {return x * x * x; }
}
...
  • 委托的兼容性:即使签名相同,委托也互不兼容:
delegate void D1();
delegate void D2();
...
D1 d1 = Method1;
D2 d2 = d1; // Compile-time error

提示:但是允许下面的写法:

D2 d2 = new D2 (d1);
  • 如果委托实例指向相同的方法,则认为它们是等价的。
  • 如果多播委托按照相同的顺序指向相同的方法,则认为它们是等价的。

委托的逆变和协变

  • 参数的兼容性:当调用一个方法时,可以给参数提供一个大于该参数的类型的参数,这是正常的多态行为:
 class Animal{}   
 class Bird : Animal{}  
 class Sparrow : Bird{}
 static void Do(Bird shit){}
 static void Main(string[] args)
        {
            Animal animal=new Animal();
            Sparrow sparrow=new Sparrow();
            Do(animal);//错误,无法从Animal转换为Bird:Bird=Animal这个向下转换是不安全的
            Do(sparrow);//正确:Bird=Sparrow,向上转换是安全的
        }      
  • 基于同样的原理,可以给委托的类型参数提供大于该类型参数的类型的类型参数,这个叫做委托的逆变(这个问题的讨论将在后续继续展开)。
delegate void StringAction (string s);
class Test
{
static void Main()
{
StringAction sa = new StringAction (ActOnObject);
sa ("hello");
}
static void ActOnObject (object o)
{
Console.WriteLine (o); // hello
}
}

委托只是代替其他类来调用方法。在本例中,当调用StringAction时,使用的是string类型的参数,当这个string类型的参数被转发到目标方法时(在本例中一个object类型参数的方法),string将被向上转换为object(隐式)。

  • 返回类型的兼容性:如果调用一个方法,返回值的类型可能是比你请求的类型的一个更细化的(more specific)类型(子类),也就是说返回了一个更特殊的类型,(一般情况下我们说子类比基类更特殊,更细化)这也是正常的多态行为:
 class Animal{}
 class Bird : Animal{}
 class Sparrow : Bird{}
 static Bird Do()
        {
            return  new Bird();
        }   
  Animal animal=new Animal();
            Sparrow sparrow=new Sparrow();
            animal = Do();
            sparrow = Do(); //转换失败,Bird无法隐式转换成Sparrow        

在这里可以总结下:在输入位置提供一个子类或者在输出位置提供一个基类,都是安全的,都是多态的表现,反之则不然。

  • 基于同样的道理,委托可以返回一个比它自身描述的返回类型更详细的类型,这个称为委托的协变。
delegate object ObjectRetriever();
class Test
{
static void Main()
{
ObjectRetriever o = new ObjectRetriever (RetriveString);
object result = o();
Console.WriteLine (result); // hello
}
static string RetriveString() { return "hello"; }
}

ObjectRetriever期望返回一个object,而它指向的方法返回一个string。

  • 泛型委托类型参数的可变性(协变和逆变):泛型的委托和泛型的接口一样,都支持类型参数的协变和逆变。如果定义了一个泛型的委托,最好按照如下的准则进行:
    ①将只用在返回(输出)位置的类型参数标注为协变的(out)
    ②将只用在参数(输入)位置的类型桉树标注为逆变的(in)
    这样做可以使转换自然的遵循类型的继承关系(其实本质上说都是向上转换是类型安全的,因为不管是协变还是逆变,实现的都是向上转换)。
    下面的委托支持协变:
delegate TResult Func<out TResult>();

允许:

Func<string> x = ...;
Func<object> y = x;

下面的委托支持逆变:

delegate void Action<in T> (T arg);

允许:

Action<object> x = ...;
Action<string> y = x;

事件

  • 在使用委托的环境中一般会出现两种角色:广播这和订阅者。广播者是包含委托字段的类,它决定什么时候去传播消息(意即调用委托)。订阅者是目标方法,通过在广播者的委托上面调用+=和-=操作符,来决定什么时候开始或者结束对广播这的监听(注册或取消注册),一个订阅者不知道也不干涉其他订阅者。
  • 事件是使这一模式正式化的语言形态。事件是只显示委托中广播/订阅需要的子特性的结构。使用事件的主要目的在于:保护订阅者之间的互不干涉。
  • 声明事件最好的方法就是在委托类型前面添加event关键字:
// Delegate definition
public delegate void PriceChangedHandler (decimal oldPrice,decimal newPrice);
public class Broadcaster
{
// Event declaration
public event PriceChangedHandler PriceChanged;
}

Broadcaster类中的代码对PriceChanged有完全的访问权限,并把它当作一个委托,Broadcaster类外的代码只能使用+=和-=在PriceChanged事件上。

  • (这个是我在C#本质论里面的记录)在使用委托时,一个非常容易犯的错误就是在本该使用+=操作符的时候使用了=操作符,结果就是将一个委托变量错误的指向了另一个委托实例。由于这是一个非常容易犯的错误,最好的解决方法就是
    仅为包容类内部的对象提供对赋值操作符的支持。event关键字的作用就是提供额外的封装,避免不小心地取消其他订阅者。
    委托和事件的第二个区别就是,事件确保只有包容类才能触发事件的通知。
    在类中定义事件非常容易,就是在定义委托字段的基础上添加event关键字,event关键字提供了对委托需要的一切封装。这里简单总结一下事件的编码规范:第一个参数sender是object类型的,它包含对调用委托的那个对象的一个引用(静态事件则为null)。第二个参数是System.EventArgs类型的
    (或者从System.EventArgs派生,但包含了事件的附加数据)。调用委托的方式和以前几乎完全一样,只是要提供附加的参数。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值