当前位置: 首页 > article >正文

C#核心笔记——(四)C#高级特性

4.1 委托

委托(delegate)是一种知道如何调用方法的对象。
委托类型(delegete type)定义了一种委托实例(delegates instance)可以调用的方法。具体来说,它定义了方法的返回类型(return type)和参数类型(parameter type)。以下语句定义了一个委托类型Transformer:

delegate int Transformer(int x);

Transformer兼容如何返回类型为int并有一个int类型的参数方法,例如:

static int Square(int x) {
    return x * x; }

或者可以简洁的写为

static int Square(int x) => x * x;

将一个方法赋值给一个委托变量就能创建一个委托实例:

Transformer t = Square;

就可以像调用方法一样进行调用:

int answer = t(3);

以下是一个完整的例子:

delegate int Transformer(int x);  
internal class Program
{
   
    static void Main(string[] args)
    {
   
        Transformer t = Square;// Create delegate instance
        int result = t(3);// Invoke delegate
        Console.WriteLine(result);// 9
    }
    static int Square(int x) => x * x;
}

委托实例字面上是调用者代理:调用者调用委托,委托调用目标方法。这种间接调用方式可以将调用者和目标方法解耦。

以下语句:

 Transformer t = Square;

是下面语句的简写:

Transformer t = new Transformer(Square);

技术上,当引用没有括号和参数的Square方法时,我们指定的是一组方法。如果该方法被重载,C#会根据赋值委托的签名选择正确的重载方法。
语句

t(3)

下面语句的简写:

t.Invoke(3))

委托和回调(callback)类似。一般指捕获类似C函数指针的结构。

4.1.1 用委托书写插件方法

委托变量可以在运行时指定一个目标方法,这个特性可用于编写插件方法。在本例中有一个名为Transform的公共方法,它对整数数组的每一个元素进行变换。Transform方法接受一个委托参数并以此为插件方法执行变换操作:

public delegate int Transformer(int x);
class Util
{
   
    public static void Transform(int[] values, Transformer t)
    {
   
        for (int i = 0; i < values.Length; i++)
            values[i] = t(values[i]);
    }
}

调用

internal class Program
{
   
    static void Main(string[] args)
    {
   
        int[] values = {
   1, 2, 3};
        Util.Transform(values, Square);
        foreach (int i in values)
            Console.Write(i + " ");//1 4 9
    }
    static int Square(int x) => x * x;
}

Transform方法是一个高阶函数,因为它是一个以函数作为参数的函数。(返回委托的方法也称为高阶函数)。

4.1.2 多播委托

所有的委托实例都拥有多播能力。这意味着一个委托实例可以引用一个目标方法,也可以引用一组目标方法。委托可以使用+和+=运算符联结多个委托实例。例如:
声明一个委托

public delegate void SomeDelegate();

方法

public static void SomeMethod1()
{
   
    Console.WriteLine("SomeMethod1");
}
 
public static void SomeMethod2()
{
   
    Console.WriteLine("SomeMethod2");
}

调用

SomeDelegate d = SomeMethod1;
d += SomeMethod2;
d();

现在调用d不仅会调用SomeMethod1而且会调用SomeMethod2。委托会按照添加的顺序依次触发。

-和-=运算符会从左侧委托操作数中将右侧委托操作数删除。例如:

d -= SomeMethod1;

现在调用d只会触发SomeMethod2调用。
对值为null的委托变量进行+或者+=操作,等价于为变量指定一个新的值:

SomeDelegate d = null;
d += SomeMethod1;

同样,在只有唯一目标方法的委托上调用-=等价于为该变量指定null值。

委托是不可变的,因此调用+=和-=的实质是创建一个新的委托实例,并把它赋值给已有变量。

如果一个多播委托拥有非void的返回类型,则调用者将从最后一个触发的方法接收返回值。前面的方法仍然调用,但是返回值都会被丢弃。大部分调用多播委托的情况都会返回void类型,因此这个细小的差异就没有了。

所有的委托类型都是从System.MulticastDelegate类隐式派生的。而System.MulticastDelegate继承自System.Delegate。C#将委托中的+、-、+=、-=运算符都编译成了System.Delegate的静态Combine和Remove方法。

多播委托的示例
若方法的执行时间很长,且该方法定期调用一个委托向调用者报告进程的执行情况。例如,在以下代码中,HardWork方法通过调用ProgressReporter委托参数报告执行进度:

 public delegate void ProgressReporter(int percentComplete);
 class Util
 {
   
     public static void HardWork(ProgressReporter p)
     {
   
         for (int i = 1; i <= 10; i++)
         {
   
             p(i * 10);//Invoke delegate
             System.Threading.Thread.Sleep(100);//Simulate hard work
         }
     }
 }

为了监视进度,我们在Main方法中创建了一个多播委托实例p。这样就可以通过两个独立方法监视执行进度:

static void WriteProgressToConsole(int percentComplete)
    => Console.WriteLine(percentComplete);

static void WriteProgressToFile(int percentComplete)
    => System.IO.File.WriteAllText("progress.txt",percentComplete.ToString());

调用

ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile;
Util.HardWork(p);

4.1.3 实例目标方法和静态目标方法

将一个实例方法赋值给委托对象时,后者不但要维护方法的引用,还需要维护方法所属的实例的引用。System.Delegate类的Target属性代表这个实例(如果委托引用的是一个静态方法,则该属性值为null)。例如:
委托

public delegate void ProgressReporter(int percentComplete);

class X
{
   
    public void InstanceProgress(int percentComplete)
        => Console.WriteLine(percentComplete);
}

调用

X x = new X();
ProgressReporter p = x.InstanceProgress;
p(99);
Console.WriteLine(p.Target == x);
Console.WriteLine(p.Method);

运行结果

99
True
Void InstanceProgress(Int32)

4.1.4 泛型委托类型

委托类型可以包含泛型类型参数。例如:

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

根据上面的定义,可以写一个通用的Transform方法,让它对任何类型都有效:

委托

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

class X
{
   
    public void InstanceProgress(int percentComplete)
        => Console.WriteLine(percentComplete);
}

调用:

int[] values = {
    1, 2, 3 };
Util1.Transform(values, Square);
foreach (int i in values)
    Console.WriteLine(i + " ");

运行结果:

1
4
9

4.1.5 Func和Action委托

有了委托,我们就可以定义出一些非常通用的小型委托类型,它们可以具有任意的返回类型和合理任意数目的参数。它们就是定义在System命名空间下的Func和Action委托(in和out是标记可变性的修饰符)

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 16

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 16

这些委托都是非常通用的委托。前面例子中的Transform委托就可以用一个带有T类型参数并返回T类型的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]);
}

调用

int[] values = {
    1, 2, 3 };
Transform<int>(values, x => x * x);
foreach (int i in values)
    Console.WriteLine(i + " ");

运行结果

1
4
9

这些委托中没有涉及的场景只有ref/out和指针参数了。

4.1.6 委托和接口

能用委托解决的问题,都可以用接口解决。例如,下面的ITransformer接口可以代替解决前面例子中的问题:

class Squarer : ITransformer
{
   
    public int Transform(int x) => x * x;
}
public class Util2
{
   
    public static void TransformAll(int[] values, ITransformer t)
    {
   
        for (int i = 0; i < values.Length; i++)
            values[i] = t.Transform(values[i]);
    }
}

调用

int[] values = {
    1, 2, 3 };
Util2.TransformAll(values, new Squarer());
foreach (int i in values)
    Console.WriteLine(i);

如果以下一个或着多个条件成立,委托可能是比接口更好的选择:

  • 接口内仅定义了一个方法
  • 需要多播能力
  • 订阅者需要多次实现接口

虽然在ITransformer的例子中不需要多播,但接口仅仅定义了一个方法,而且订阅者有可能为了支持不同的变换(例如平方或立方变换)需要多次实现ITransformer接口。如果使用接口,由于Test只能实现一次ITransformer,因此我们就必须对每一种变换编写一个新的类型。

public interface ITransformer
{
   
    int Transform(int x);
}
class Cuber : ITransformer
{
   
    public int Transform(int x) => x * x * x;
}

调用

int[] values = {
    1, 2, 3 };
Util2.TransformAll(values, new Cuber());
foreach (int i in values)
    Console.WriteLine(i);

4.1.7 委托的兼容性

4.1.7 .1 类型的兼容性

即使签名相似,委托类型也互不兼容:

delegate void D1();
delegate void D2();
static void Method1(){
   }

调用

D1 d1 = Method1;
//D2 d2 = d1; //Compile-time error
D2 d2 = new D2(d1);//yes

如果委托实例指向相同的目标方法,则认为它们是等价的:

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

如果多播委托按照相同的顺序引用相同的方法,则认为它们是等价的

4.1.7 .2 参数的兼容性

当调用方法时,可以给方法的参数提供特定的变量类型这是正常的多态行为。基于同样的原因,委托也可以有比它的目标方法参数类型更具体的参数类型,这称为逆变。
下面举例说明:

委托

delegate void StringAction(string s);

方法

static void ActOnObject(object o) => Console.WriteLine(o);

调用

 StringAction sa = new StringAction(ActOnObject);
 sa("hello");//hello

委托仅仅替其他人调用方法。在本例中,在调用StringAction时,参数类型是string。当这个参数传递给目标方法,参数隐式向上转换为object。

标准事件模式的设计宗旨是通过使用公共的EventArgs基类来利用逆变的特性。例如,可以用两个不同的委托调用同一个方法,一个传递MouseEventArgs而另一个则传递KeyEventArgs。

4.1.7 .3 返回类型的兼容性

调用一个方法时可能得到比请求类型更特定的返回值类型,这也是正常的多态行为。基于同样的原因,委托的目标方法可能返回比委托声明的返回值类型更加特定的返回值类型,这称为协变。例如:

 delegate object ObjectRetriever();
 static string RetrieveString() => "hello";

调用

 ObjectRetriever o = new ObjectRetriever(RetrieveString);
 object result = o();
 Console.WriteLine(result);

ObjectRetriever期望返回一个object。但若返回object子类也是可以的,这是因为委托的返回类型是协变的。

4.1.7.4 泛型委托类型的参数协变

在第三章中,介绍了接口时如何支持协变和逆变参数类型的。而委托也具有相同的功能。
如果我们定义了一个泛型委托类型,那么最好参考如下的准则:

  • 将只用于返回值类型的类型参数标记为协变(out)。
  • 将只用于参数的任意类型参数标记为逆变(in)。

这样可以依照类型的继承关系自然地进行类型转换。

以下(在System命名空间中的定义的)委托拥有协变类型参数Tresult:

delegate TResult Func<out TResult>();

它允许如下操作:

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

在下面(在System命名空间中定义)的委托拥有逆变类型参数T:

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

因而可以执行如下的操作:

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

4.2 事件

当使用委托时,一般会出现两种角色:广播者(broadcaster)和订阅者(subscriber)。
广播者是包含委托字段的类型,它通过调用委托决定何时进行广播。
而订阅者是方法的目标接收者。订阅者通过在广播者的委托上调用 += 和 -= 类决定何时开始监听而何时监听结束,订阅者不知道也不会干涉其他的订阅者。

而事件就是正式定义这一模式的语言功能。事件是一种使用有限的委托功能实现广播者/订阅者模型的结构。使用事件的主要目的在于保证订阅者之间互相不影响。

声明事件最简单的方法就是在委托成员的前面加上event关键字:

public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
public class Broadcaster
{
   
    public event PriceChangedHandler PriveChanged;
}

Broadcaster类型中的代码对PriceChanger有完全的访问权限,并可以将其视为委托。而Broadcaster类型之外的代码则仅可以在PriceChanged事件上执行 += 和 -= 运算。

事件的工作机制是怎样的
当声明如下委托是,在内部发生了三件事:

public delegate void PriceChangedHandler(decimal oldPrice, decimal newPrice);
public class Broadcaster
{
   
    public event PriceChangedHandler PriveChanged = (a,b)=> {
    };
}

首先,编译器将事件的声明翻译为如下形式:

PriceChangerdHandler priceChanged;
public event PriceChangerdHandler Pricechanged
{
   
    add {
    priceChanged += value; }
    remove {
    priceChanged -= value}
}

add和remove关键字明确了事件的访问器,就像属性的访问器那样。我们将在后续继续讲解如何编写访问器。

而后,编译器在Broadcast类里面找到除调用 += 和 -=之外的priveChanged引用点,并将它们重定向到内部的priceChanged委托字段。

最后,编译器对事件上的+=和-=运算符操作相应地调用事件的add或remove访问器。有意思当应用于事件时,+=和-=的行为是唯一的,而不像其他的情况是+和-运算符于赋值运算符的简写。

观察下面的例子。在Stock类中,每当Stock的Price发生变化时,PriceChanged事件就会触发。

public delegate void PriceChangedHandler(decimal oldPrice,decimal newPrice);
internal class Stock
{
   
    string symbol;
    decimal price ;
    public event PriceChangedHandler PriceChanged;
    public Stock(string symbol) 
    {
   
        price = 10;
        this.symbol = symbol;
        PriceChanged = PriceChange;
    }

    public decimal Price
    {
   
        get {
    return price; }
        set
        {
   
            if (price == value) return;
            decimal oldPrice = price;
            price = value;
            PriceChanged?.Invoke(oldPrice,price);
            //等价于
            //if (PriceChanged != null)
            //    PriceChanged(oldPrice, price);
        }
    }

    void PriceChange(decimal oldPrice, decimal newPrice)
    {
   
        Console.WriteLine(string.Format("价格变化了,原价为{0},现价为{1}", oldPrice, newPrice));
    }
}

调用

Stock stock = new Stock("南极人");
stock.Price = 15;
stock.Price = 9.9M;

运行结果:

价格变化了,原价为10,现价为15
价格变化了,原价为15,现价为9.9

本例中,如果将event关键字去掉,PriceChanged就变成了普通的委托字段,虽然运行结果时不变的,但是没有原来健壮了,因为订阅者可以通过以下方式互相影响:

  • 通过重新指派PriceChanged替换其他的订阅者(不用+=运算符)
  • 清除所有的订阅者(将PriceChanged设置为null)
  • 通过调用其委托广播到其他的订阅者
    WinRT的事件具有稍微不同的语义:附加到一个事件时会返回一个令牌以便从事件分离。编译器透明地弥补了这个差异(内部保存了令牌字典),因此可以将WinRT事件当作普通的CLR事件来使用。

4.2.1 标准事件模式

.NET FrameWork 为事件编程定义了一个标准模式,它的目的就是保持框架和用户代码的一致性。标准事件模式的核心是System.EventArgs类,一个预定的没有成员(但是有一个静态的Empty属性)的类。EventArgs类是为事件传递信息的基类。在Stock实例中,我们可以继承EventArgs以便在PriceChanged事件触发时传递新的和旧的Price值:

internal class PriceChangedEventArgs : System.EventArgs
{
   
    public readonly decimal LastPrice;
    public readonly decimal NewPrice;

    public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
    {
   
        LastPrice = lastPrice;
        NewPrice = newPrice;
    }
}

考虑到复用性,EventArgs子类应当根据它包含的信息来命名(而非根据它的事件命名)。它一般将数据以属性或只读字段的方式暴露给外界。

EventArgs子类就位后,下一步就是选择或者定义事件的委托。这一步需要遵循三条原则:

  • 委托必须以void作为返回值
  • 委托必须接受两个参数,第一个参数是object类型,第二个参数则是EventArgs的子类。第一个参数表面了事件的广播者,第二个参数则包含了需要传递的额外信息。
  • 委托的名称必须以EventHandler结尾。

框架定义了一个名为System.EventHandler<>的泛型委托,该委托满足以上提到的三种条件规则:

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

在泛型出现之(C#2.0之前),我们只能以 如下的方式书写自定义委托:

public delegate void EventHanler<TEventArgs>(object source, TEventArgs e);

接下来就是定义选定委托类型的事件了。这里使用泛型的EventHandler委托:

public class Stock
{
   
	public event EventHandle<PriceChangedEventArgs> PriceChanged;
}

最后该模式需要写一个protected的虚方法来触发事件。方法名必须和事件名称一致,以On为前缀,并接收唯一的EventArags参数:

public event EventHandler<PriceChangedEventArgs> PriceChanged;

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

在多线程情形下,为了保证线程安全,在测试和调用委托之前需要将它保存在一个临时变量中:

var temp = PriceChanged;
if(temp != null) temp(this, e);

我们可以使用C# 6的null条件运算符来避免临时变量的声明:

decimal e = 1.22M;
PriceChanged?.Invoke(this, e);

这种方式线程安全又书写简明,是现阶段最好的事件触发方式了,

总参考

public delegate void PriceChangedHandler1(Broadcaster oldPrice, decimal newPrice);
public class Broadcaster
{
   
    public event PriceChangedHandler1 PriceChanged = (a,b)=> {
    };
    public void Chance()
    {
   
        decimal e = 1.22M;
        PriceChanged?.Invoke(this, e);
    }
}

这样就提供了一个子类可以调用或重写事件的关键点(假如不是密封类的话)。以下是完整的例子:
PriceChangedEventArgs

internal class PriceChangedEventArgs : System.EventArgs
{
   
   public readonly decimal LastPrice;
   public readonly decimal NewPrice;

   public PriceChangedEventArgs(decimal lastPrice, decimal newPrice)
   {
   
       LastPrice = lastPrice;
       NewPrice = newPrice;
   }
}

Stock

public delegate void PriceChangedHandler(decimal oldPrice,decimal newPrice);
internal class Stock
{
   
    string symbol;
    decimal price;

    public Stock(string symbol)
    {
   
        this.symbol = symbol;
    }

    public event EventHandler<PriceChangedEventArgs> PriceChanged;

    protected virtual void OnPriceChanged(PriceChangedEventArgs e)
    {
   
        PriceChanged?.Invoke(this, e);
    }

    public decimal Price
    {
   
        get {
    return price; }
        set 
        {
   
            if(price == value) return;
            decimal oldPrice = price;
            price = value;
            OnPriceChanged(new PriceChangedEventArgs(oldPrice, price));
        }
    }
}

stock_PriceChanged

static void stock_PriceChanged(object sender, PriceChangedEventArgs e)
{
   
    if((e.NewPrice - e.LastPrice) / e.LastPrice > 0.1M)
        Console.WriteLine("Alert,10% stock price increase!");
}

调用

Stock stock = new Stock("THEH");
stock.Price = 27.01M;
//Register with the PriceChanged event
stock.PriceChanged += stock_PriceChanged;
stock.Price = 31.59M;

如果事件不需要传递额外的信息,则可以使用预定义的非泛型委托EventHandler。本例中,我们重写Stock类,当Price属性发生变换时,触发PriceChanged事件,事件除了传达已发生的消息之外没有必须包含信息。为了避免创建非必要的EventArgs实例,我们使用了EventArgs.Emtpy属性:

internal class Stock
{
   
    string symbol;
    decimal price;

    public Stock(string symbol)
    {
   
        this.symbol = symbol;
    }

    public event EventHandler PriceChanged;

    protected virtual void OnPriceChanged(EventArgs e)
    {
   
        PriceChanged?.Invoke(this, e);
    }

    public decimal Price
    {
   
        get {
    return price; }
        set
        {
   
            if (price == value) return;
            price = value;
            OnPriceChanged(EventArgs.Empty);
        }
    }
}

4.2.2 事件访问器

事件访问器是对事件的 += 和 -= 功能的实现。默认情况下,访问器由编译器隐式实现。

public event EventHandler PriceChanged;

编译器将其转化为:

  • 一个私有的委托字段
  • 一对公有的事件访问器函数(add_PriceChanged 和remove_PriceChanged),它们将 += 和 -= 操作转向了私有的委托字段。

我们也可以显式定义事件访问器来替代这个过程。以下是PriceChanged事件的手动实现:

public event EventHandler priceChanged;

public event EventHandler PriceChanged
{
   
    add {
    priceChanged += value; }
    remove {
    priceChanged -= value; }
}

本例从功能上和C#的默认访问器实现是等价的(但是C#还使用了无锁功能的比较并交换算法,保证了在更新委托时的线程安全性)。有了自定义事件访问器,C#就不会生成默认的字段和访问器逻辑。

显式定义事件访问器,可以在委托的存储和访问上进行更复杂的操作。这主要有三种情形:

  • 当前事件访问器仅仅是广播事件的类的中继器
  • 当类了定义大量的事件,而大部分事件有很少的订阅者,例如Windows控件。在这种情况下,最好在一个字典中存储订阅者的委托实例。这是因为字段比大量的空委托字段的引用的存储开销更少。
  • 当显式实现声明事件的接口时。

以下的例子展示了第三种情况:

 public interface IFoo {
    event EventHandler Ev; }
 public class Foo : IFoo
 {
   
     

http://www.kler.cn/a/573319.html

相关文章:

  • C语言高性能交换两个变量的值
  • 【蓝桥杯】每天一题,理解逻辑(2/90)【LeetCode 复写零】
  • Electron桌面应用开发:自定义菜单
  • 谈谈单例模式中通过Htools包的SpringUtil.getBean获取Bean的好处
  • 计算机毕业设计SpringBoot+Vue.js科研工作量管理系统的(源码+文档+PPT+讲解)
  • 在Linux中开发OpenGL——检查开发环境对OpenGL ES的支持
  • 【音视频】封装格式与音视频同步
  • 【Elasticsearch】reindex
  • ArcGIS操作:14 按位置选址
  • 深入解析 Android Activity 生命周期
  • 1、语言的本质
  • vue3中Element-plus table 反选 禁用实战
  • 【Elasticsearch】Elasticsearch 的`path.settings`是用于配置 Elasticsearch 数据和日志存储路径的重要设置
  • JVM常用概念之局部变量可达性
  • 大模型为何无法达到AGI?
  • 利用Python爬虫按图搜索1688商品(拍立淘)
  • 【linux 安装mongodb】在redhat9上安装mongodb8出现下载元数据错误
  • 【大模型安全】大模型的技术风险
  • NModbus 连接到Modbus服务器(Modbus TCP)
  • NFC 碰一碰发视频系统技术开发实战:从硬件触发到智能生成的全流程实现