C#中的神奇方法

2020-07-10 07:03:50

在C#中有一组特殊的方法签名,它们在语言级别上有特别的支持,具有这些签名的方法允许使用特殊的语法,这有几个好处。例如,我们可以使用它们来简化我们的代码,或者创建DSL来以更清晰的方式表达特定于域的问题的解决方案。我在不同的地方遇到了这些方法,所以我决定创建一个博客帖子来总结我在这个主题上的所有发现。

集合初始值设定项是一个相当老的特性,因为它从版本3(2007年底发布)起就存在于该语言中。提醒一下,集合初始值设定项允许通过在BLOCK语句中提供元素来预先填充列表:

集合初始值设定项不仅适用于BCL中的数组和集合类型,还可以与满足以下条件的任何类型一起使用:

公共类CustomList<;T>;:IEnumerable{public IEnumerator GetEnumerator()=>;抛出新的NotImplementedException();public void add(T Item)=>;抛出新的NotImplementedException();}。

通过将Add方法定义为扩展方法,我们可以向现有类型添加对集合初始值设定项的支持:

public static class ExistingTypeExtensions{public static void add<;T>;(ExistingType@this,T Item)=>;抛出新的NotImplementedException();}。

此语法还可用于将元素插入到初始化块中没有可访问的setter的集合字段中:

class CustomType{public list<;string>;CollectionField{get;private set;}=new list<;string>;();}class Program{static void main(string[]args){var obj=new CustomType{CollectionField={";item1";,";item2";}};}

公共类CustomList<;T>;:IEnumerable{public IEnumerator GetEnumerator()=>;抛出新的NotImplementedException();public void add(T Item,String ExtraParam1,int ExtraParam1)=>;抛出新的NotImplementedException();}。

要在初始化块内使用此重载,我们需要用额外的一对大括号将所有参数括起来:

var obj=new CustomType{CollectionField={{";item1";,";ExtraParamVal1";,2},{";item2";,";ExtraParamVal2";,3}};

集合初始值设定项经常用于初始化具有已知数量的项目的集合,但是我们可以利用它来设置具有动态数量的元素的集合。在这两种情况下,语法是相同的:

公共类CustomList<;T>;:IEnumerable{public IEnumerator GetEnumerator()=>;抛出新的NotImplementedException();public void add(IEnumerable<;T>;Items)=>;抛出新的NotImplementedException();}。

遗憾的是,来自BCL的数组类型和集合没有实现void add(IEnumerable<;T>;Items)方法,但是我们可以通过为现有集合类型定义一个扩展方法轻松地改变这一点:

多亏了这个扩展方法,现在可以编写如下所示的代码:

var obj=new CustomType{CollectionField={ExistingItems.Where(x=>;/*筛选项*/).Select(x=>;/*映射项*/)}};

或者甚至由单个元素和多个可枚举的结果的混合组成结果集合:

var obj=new CustomType{CollectionField={个别元素1,个别元素2,列表1,其中(x=&>;/*筛选项*/).Select(x=>;/*映射项*/),list2.其中(x=>;/*筛选项*/).Select(x=>;/*映射项*/),}};

如果没有此语法,则很难在初始化块内实现类似的结果。

我在处理具有从协议约定生成的集合字段的类型的映射时意外地发现了这个语言特性。对于那些不熟悉Protobuf的人来说,如果您使用grpctools从proto文件生成DotNet类型,则所有集合字段将按如下方式生成:

正如您所看到的,生成的代码中的集合字段没有setter,而是一个变相的福音,RepeatableField实现了void add(IEnumerable Items),因此我们仍然可以在初始化块中初始化它们:

/<;Summary>;/将所有指定值添加到此集合中。此方法用于/允许根据集合初始值设定项内的查询构造重复字段。/在非集合初始值设定项代码中,请考虑使用等效的<;请参阅CREF=";AddRange";/>;/方法。/<;/Summary<;/<;param name=";value&34;<;要添加到此集合的值。<;

C#6中引入的一个很酷的特性是索引初始化器,它简化了字典初始化的语法。多亏了这一点,我们可以以更易读的方式编写字典初始化代码:

var errorCodes=new Dictionary<;int,string>;{[404]=";,[302]=";页面已移动,但留下了转发地址。";,[500]=";Web服务器今天无法出来播放。";};

var errorCodes=new Dictionary<;int,string>;();errorCodes[404]=";;找不到页面;errorCodes[302]=";页面已移动,但留下了转发地址。";;errorCodes[500]=";Web服务器今天无法出来播放。";;

这不是很多,但它肯定会带来更好的编写和阅读代码的体验。

索引初始化器最大的优点是它不仅限于Dictionary<;>;类,它可以与任何定义索引器的类型一起使用:

class HttpHeaders{public string this[字符串键]{get=>;抛出新的NotImplementedException();set=>;抛出新的NotImplementedException();}}class Program{static void main(string[]args){var Headers=new HttpHeaders{{[";access-control-Allow-Origin";]=";*";,[";cache-。

在C#7.0中,与元组一起引入了解构函数机制。解构函数允许将一个元组“分解”成一组单独的变量,如下所示:

此语法还允许切换两个变量的值,而无需显式声明第三个变量:

类点{public int X{get;}public int Y{get;}public Point(int x,int y)=>;(X,Y)=(x,y);}。

反构造函数不仅可以与元组一起使用,还可以与自定义类型一起使用。为了允许解构自定义类型,它需要实现一个符合以下规则的方法:

类点{public int X{get;}public int Y{get;}public Point(int x,int y)=>;(X,Y)=(x,y)=(x,y);public void deconstruct(out int x,out int y)=>;(x,y)=(X,Y);}。

通过将反构造函数定义为扩展方法,可以将其添加到在源代码外部声明的类型:

public static class PointExtensions{public static void deconstruct(this Point@this,out int x,out int y)=>;(x,y)=(@this.x,@this.y);}。

最有用的解构函数示例之一是KeyValuePair<;TKey,TValue>;,它允许在迭代字典时轻松访问键和值:

foreach(new Dictionary<;int,string>;{[1]=";val1";,[2]=";val2";}中的var(key,value)){//TODO:做点什么}。

KeyValuePair<;TKey,TValue>;.Deconstruct(TKey,TValue)仅从netStandard2.1提供。对于以前的netStandard版本,我们需要使用带有扩展方法的方法手动添加它。

C#5(与Visual Studio2012一起发布)引入了异步/等待机制,它真正改变了异步编程领域的游戏规则。在此之前,处理异步方法调用通常会导致相当混乱的代码,特别是当有多个异步调用时:

void DoSomething(){DoSomethingAsync().ContinueWith((Task1)=>;{if(task1.IsCompletedSuccessful){DoSomethingElse1Async(task1.Result).ContinueWith((task2)=>;{if(task2.IsCompletedSuccessful){DoSomethingElse2Async(task2.Result).ContinueWith((task3)=>;{//TODO:做某事};}});}});}私有任务<;int>;DoSomethingAsync()。DoSomethingElse1Async(Int I)=>;抛出新的NotImplementedException();私有任务<;int>;DoSomethingElse2Async(Int I)=>;抛出新的NotImplementedException();

异步任务单据Something(){var res1=等待DoSomethingAsync();var res2=等待DoSomethingElse1Async(Res1);等待DoSomethingElse2Async(Res2);}。

这可能会让人感到惊讶,但是等待关键字并不是仅保留用于Task类型。它可以与包含名为GetAwaiter的方法并返回满足以下要求的类型的任何类型一起使用:

要向自定义类型添加对AWAIT关键字的支持,我们需要定义GetAwaiter方法,该方法返回TaskAwaiter<;TResult>;的实例或满足上述条件的自定义类型:

类CustomAwaable{public CustomAwaiter GetAwaiter()=>;抛出新的NotImplementedException();}类CustomAwaiter:INotifyCompletion{public void OnCompleted(Action Continue)=>;抛出新的NotImplementedException();public bool IsCompleted=>;=>;抛出新的NotImplementedException();public void GetResult()=。

您可能想知道将AWAIT语法与自定义可等待类型一起使用的可能场景是什么。如果是这样的话,我强烈推荐阅读斯蒂芬·图布(Stephen Toub)的一篇题为“等待一切”的文章,其中提供了大量有趣的例子。

C#3.0最好的发明无疑是语言集成查询(LINQ),它允许使用类似SQL的语法操作集合。LINQ有两种变体:类SQL语法和扩展方法语法。我更喜欢第二种,因为在我看来它更易读,但可能是因为我习惯了它。关于类SQL语法的一个有趣事实是,它在编译期间被转换为扩展方法语法,因为它是一个C#,而不是CLR特性。LINQ最初是为了处理IEnumerable、IEnumerable&t;T>;和IQueryable<;T>;类型而发明的,但它并不局限于这些类型,我们可以将它与满足查询表达式模式要求的任何类型一起使用。LINQ使用的完整方法签名集如下所示:

class C{public C<;T>;cast<;T>;();}class C<;T>;:C{public C<;T&>where(Func<;T,bool&>;谓词);public C<;U>;Select<;U>;(Func<;T,U>;选择器);public C<;选择器,Func<;T,U,V&>;result Selector);public C<;V&>;JOIN<;U,K,V&>;(C<;U&U,V&>;(C<;U;U,V;T,K&>;OUterKeySelector,Func<;U,K&>;innerKeySelector,Func<;T,U,V。outterKeySelector,Func<;U,K&>;innerKeySelector,Func<;T,C<;U>;,V&>;ResultSelector);public O<;T&>;OrderBy<;K&>;(Func<;T,K&>;keySelector);public O<;T>;Order。T,K&>;keySelector);public C<;G<;K,E&>;>;Group By<;K,E&>;(Func<;T,K&>;keySelector,Func<;T,E&>;elementSelector);}class O<;T>;:C<;T>;{public O<;{public O<;(函数<;T,K&>;keySelector);}class G<;K,T&>;:C<;T>;{public K key{get;}}。

当然,我们不需要实现所有这些方法来将LINQ语法与我们的自定义类型一起使用。LINQ运算符及其所需方法的列表可以在此处找到。带有自定义类型的LINQ语法经常用于实现Monad。如何做到这一点可以在MiłOsz Piechocki的文章《用LINQ理解单子》中找到一个非常好的解释

本文的目的不是鼓励您滥用这些语法技巧,而是要揭开它们的神秘面纱。另一方面,它们不应该完全避免。它们是为使用而发明的,有时它们可以使您的代码更加干净。如果您担心生成的代码对您的团队成员来说可能不是那么明显,那么您应该找到一种方法来分享您的知识,或者至少链接到本文。)我不确定它是否是那些“神奇方法”的完整集合-如果您知道其他方法,我将非常感谢您在下面的评论区中分享它。

如果你觉得这篇博客很有用,并且认为值得与他人分享这个想法,请毫不犹豫地使用下面的这些按钮: