[c#]关于c#中委托,匿名函数,事件的浅要解析

委托

委托是一种引用类型,标识对具有特定参数列表和返回类型的方法的引用。在实例化委托时,可以将其实例与任何具有兼容签名和返回类型的方法相关联。通过委托实例调用方法。
委托用于将方法作为参数传递给其它方法。可将任何可访问类或结构中与委托类型匹配的任何方法分配给委托。该方法可以是静态方法,也可以是实例方法。
将方法作为参数进行引用的能力使委托成为定义回调方法的理想选择。
委托有以下属性:

  • 委托类似于c++的函数指针,但委托完全面向对象,不像c++指针会记住函数,委托会同时封装对象实例和方法。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起
  • 方法不必与委托类型完全匹配
  • 可使用匿名方法或lambda表达式内联代码块
  • 可以使用?.Invoke()来调用委托方法
    匿名函数和函数实例的问题
    在研究逆变和协变的时候发现一段官方代码

    public delegate R SampleGenericDelegate<A, R>(A a);
    public static Second AFristRSecond(First first)
    {
    return new Second();
    }
    SampleGenericDelegate<Second, First> fse = AFirstRSecond;

    这段代码里,返回值是定义委托的子类,参数是定义委托的父类,所以这个逆变,又是协变,但是我用匿名函数写的时候却没办法成功
    SampleGenericDelegate<Second, First> fse3 = delegate (First a)
    {
    return new Second();
    };
    没有搞明白这个原因,只能归结于是匿名函数和函数实例的不一致。

    .NET包含的委托类型

  • Func<> 通常用于现有转换的请款,也就是说需要将委托参数转换为其他结果时。必须且只有一个返回值的时候用这个,返回值有协变,如果传入参数的话是带有逆变。
  • Action<> 用于需要使用委托参数执行操作的情况。简而言之,无返回值的用这个,如果有传入参数的定义,这个是有逆变out关键词的。
  • Predicate<> 用于需要确定参数是否满足委托条件的情况,必须且只有一个bool值的返回值,必须且只有一个传入参数,传入参数带有逆变。

    委托中的变体

    
    public class First { }
    public class Second : First { }
    public delegate First SampleDelegate(Second a);
    public delegate Second SampleDelegateSecond(Second a);
    public First ASecondRFirst(Second first)
    { return new First(); }
    public Second AsecondRsecond(Second second)
    {return new Second();}
    public First AFirstRFirst(First first)
    {return new First();}
    public Second AFirstRSecond(First first)
    {return new Second();}
- 协变
定义:将返回派生程度较大的派生类型的方法分配给委托(就是把子类返回出来)
在上述代码中体现为:SampleDelegate sa = AsecondRsecond 这个就表现为协变
- 逆变
定义:方法所接受参数的派生类型所具有的派生程度小于委托类型指定的程度(就是传参传父类)
在上述代码中表现为:SampleDelegate sa = AFirstRFirst 这个就表现为逆变

SampleDelegate sa = AFirstRSecond 这个应该是既满足协变,又满足逆变。
- 泛型委托
public delegate R SampleGenericDelegate(A a);
SampleGenericDelegatedGeneric = ASecondRFirst
上述代码为正常的泛型委托
dGeneric = AFirstRSecond;
上述即是协变也是逆变的隐式转换。
- 泛型类型参数中的变体
泛型委托之间的隐式转换,要求类型继承自对方,才可以将泛型委托分配给对方。
泛型类型参数的变体仅支持引用类型
合并委托的时候要求类型完全相同,所以变体委托不应该合并。
泛型委托的协变需要使用关键词out,协变类型只能用作方法返回类型,不能用作方法参数类型;逆变使用关键词in,逆变类型只能用作方法参数类型,不能用作方法返回类型。
可以在同一个委托中支持变体和协变,单这只适用于不同类型的参数。
在做协变和逆变的时候,写不写关键词,协变是可以支持函数实例和匿名函数的委托,逆变可以支持函数实例的的委托,匿名函数的委托就不能支持;这块的文档官方只说了可能会发现可以使用,所以就当一种奇巧淫技使用了。
当不是用关键字来进行逆变和协变的时候,就不能将一个委托分配给另外一个委托了。
```csharp
public delegate R SampleGenericDelegate<out R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;
//错误
public delegate R SampleGenericDelegate<R>();
SampleGenericDelegate1<String> dobject = () => "";
SampleGenericDelegate1<Object> dobject1 = dobject;</code></pre>
<p>上述代码就体现了这个问题。</p>
<ul>
<li>合并委托(多播委托)
可以通过使用+运算符将多个对象分配到一个委托实例上。多播委托包含已分配委托列表。此多播委托被调用时会依次调用列表中的委托。仅可合并类型相同的委托。对象必须是已经初始化过。当使用匿名函数合并委托的时候,取消是很麻烦的。
<pre><code class="language-csharp">delegate void CustomDel(string s);
void Hello(string s){Console.WriteLine("Hello" + s);}
//错误1
CustomDel c += Hello;
//错误2
CustomDel c = Hello;
CustomDel d;
c += d;</code></pre>
<p>这两个委托都是没有初始化的。所以在合并的时候就不对。</p>
<pre><code class="language-csharp">CustomDel c = Hello;
c += (string s)=> { Console.WriteLine("niming" + s); };
c -= Hello;
c("aaaaa");</code></pre>
<p>上述代码你如果想在多播委托中删除掉匿名函数的委托,你只能再定义一个委托实例,把匿名函数赋值给给委托实例,然后再添加到多播委托上。</p>
<h3>闭包</h3>
<p>委托还能增加一种闭包的概念,这个概念在前面有分析过。</p>
<h2>匿名函数</h2>
<p>在说委托的时候,已经用到了匿名函数,现在具体说下匿名函数的内容
匿名函数是一个“内联”语句或表达式,可在需要委托类型的任何地方使用。可以使用匿名函数来初始化命名委托,或传递命名委托作为方法参数。
可以使用lambda表达式或匿名方法来创建匿名函数。某些类型的lambda表达式可以转换为表达式树类型</p>
<h3>匿名方法</h3>
<p>delegate 运算符创建一个可以转换为委托类型的匿名方法。</p>
<h3>lambda表达式</h3>
<p>只是使用委托的更方便的语法,将声明签名和方法正文,但是在分配到委托之前没有自己的正式标识。</p></li>
<li>表达式lambda,表达式为其主体:
<pre><code class="language-csharp">(input-parameters)=>expression</code></pre>
<p>使用=>从其主体分离lambda参数列表。在左侧指定输入参数,在另一侧输入表达式或语句块。
lambda广泛用于表达式树的构造。
只有当lambda只有一个输入参数的时候,小括号才是可选的;否则括号是必须的。
输入参数类型必须全部为显式或者全部为隐式;否者会编译出错。
表达式lambda的主体可以包含方法调用。不过若要创建在.NET公共语言运行时的上下文之外计算的表达式树,不得在lambda表达式中使用方法调用。在.NET公共语言运行时上下文之外,方法将没有任何意义。</p></li>
<li>语句lambda,语句块作为其主体:
<pre><code class="language-csharp">(inpput-parameters) => {<sequence-of-statements>}</code></pre>
<p>语句lambda与表达式lambda表达式类似,只是语句括在大括号中。
语句lambda的主体可以包含任意数量的语句;但是实际上通常不会多于两个或者三个。
语句 lambda 也不能用于创建表达式目录树。</p>
<h3>异步lambda</h3>
<p>使用关键词async和await。</p>
<h3>lambda 表达式和元组</h3>
<p>c# 7.0起,c#语言提供了对元组的内置支持。可以提供一个元组作为Lambda表达式的参数,同时Lambda表达式也可以返回元组。可通过用括号括住用逗号分隔的组件列表来定义元组。</p>
<pre><code class="language-csharp">Func<Tuple<int, int, int>, Tuple<int, int, int> > doubleThem = ns => (new Tuple<int, int, int> (2 * ns.Item1, 2 * ns.Item2, 2 * ns.Item3));
var numbers = new Tuple<int, int, int>(2, 3, 4);
var doublednumbers = doubleThem(numbers);</code></pre>
<p>元组类型关键词是Tuple,通常是item1,item2这样的字段命名。但是也可以使用命名组件定义元组。</p>
<pre><code class="language-csharp">Func<(int n1, int n2, int n3), (int, int, int)> doublethem = ns => (2 * ns.n1, 2 * ns.n2, 3 * ns.n3);
var numbers = (2, 3, 4);
var doubledNumbers = doublethem(numbers);</code></pre>
<p>上述是使用命名组件定义元组的示例代码。</p>
<h3>捕获lambda表达式中的外部变量和变量范围</h3>
<p>lambda可以引用外部变量。这些变量在定义lambda表达式的方法中或包含lambda表达式的类型中的范围内变量。以这种方式捕获的变量将进行存储以备在lambda表达式中使用,即使在其他情况下,这些变量将超出范围并进行垃圾回收。必须明确地分配外部变量,然后才能将lambda表达式中使用该变量。</p></li>
<li>捕获的变量将不会被作为垃圾回收,直至引用变量的委托符合垃圾回收的条件。</li>
<li>在封闭方法中看不到lambda表达式内引入的变量。</li>
<li>lambda表达式无法从封闭方法中直接捕获in,ref或out参数。</li>
<li>lambda表达式中的return语句不会导致封闭方法返回。</li>
<li>如果相应跳转语句的目标位于lambda表达式块之外,lambda表达式不得包含goto,break或continue语句。同样,如果目标在块内部,在lambda表达式块外部使用跳转语句也是错误的。
<h2>事件</h2>
<p>要定义一个事件,可以在事件类的签名中使用event关键字,并制定事件的委托类型。
还能显式创建包含添加或删除处理程序的属性,创建方法和属性的语法类似,只是替换关键词为add,remove。</p>
<h3>使用委托添加事件</h3>
<pre><code class="language-csharp">
delegate void CustomDel(string s);
event CustomDel cusevnet;
void funcust(string s){</code></pre></li>
</ul>
<p>}
cusevnet += funcust;</p>
<pre><code>### .NET支持事件的委托
```csharp
private EventHandler<T> directoryChanged;
event EventHandler<T> DirectoryChanged
{
    add{ directoryChanged += value;}
    remove{directoryChanged -= value;}
}

EventHandler

是一个预定义的委托,当不生成数据的时候用EventHandler,但是当有数据产生的时候用泛型EventHandler&lt TEventArgs &gtTEventArgs是继承EventArgs的类型。

事件委托的签名

void OnEventRaised(object sender,EventArgs args)

上述代码为.NET事件委托的标准签名。返回类型为void,参数列表包含两个参数:发件人和事件参数。sender的编译类型为system.object,第二个参数通常派生自system.EventArgs的类型,即使事件类型不需要任何参数,你仍要提供者两个参数。应使用特殊值EventArgs.Empty来表示事件不包含任何附加信息。在.NET Core版本中对TEventArgs的定义不再要求必须派生自System.EventArgs的类。

两种事件对比

EventHandler事件是不带返回值的,传入参数只能为固定的两个,而委托事件是可以自定义参数和返回值的。