本文共 12538 字,大约阅读时间需要 41 分钟。
目录
近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单个问题的许多潜在(语言)解决方案。这既好又坏。很好是因为它给了我们成为开发人员的自由和权力(不会影响向后兼容性),并且由于与决策相关的认知负荷而导致不好。
在本系列中,我们希望了解存在哪些选项以及这些选项的不同之处。当然,在某些条件下,某些人可能有优点和缺点。我们将探索这些场景,并提出一个指南,以便在翻新现有项目时让我们的生活更轻松。
这是该系列的第二部分。您也可以查看。
我觉得重要的是要讨论新语言功能的发展方向以及旧语言的位置——让我们称之为已建立 ——这些仍然是首选。我可能并不总是对的(特别是,因为我的一些观点肯定会更主观/是品味问题)。像往常一样,留下评论讨论将不胜感激!
让我们从一些历史背景开始。
我们常常看到人们对术语——“方法”与“功能”感到困惑。实际上,每个方法都是一个函数,但并不是每个函数都是一个方法。方法是一种特殊的函数,它具有隐含的第一个参数,称为“上下文”。上下文被赋予this并且与指定方法的类的实例相关联。因此,(真实)方法只能存在于类中,而static方法应该被称为函数。
虽然人们通常认为this保证非null的,但实际上是人为的限制。实际上,运行时对第一个参数执行一些隐式(通过使方法的调用成为callvirt指令)检查,但是,理论上这可以很容易地避开。
使用隐式this参数,可以派生出一种特殊的语法。我们发现c.f(a, b, ...) 而不是调用一个函数f如f(a, b, ...)那样,其中c是某个类的实例。所有其他情况可以被认为是完全限定的名称,请参阅
// file a.csnamespace A{ public class Foo { public static void Hello() { // some code } }}// file b.csusing A;public class Bar{ public static void Test() { Foo.Hello(); }}// file c.csusing static A.Foo;public class Bar{ public static void Test() { Hello(); }}
正如我们所看到的,自C#6起类也可以被认为是名称空间——至少对于它的static方法。因此,static方法实际上总是像没有任何前缀的函数一样被调用,差异被简化为完全限定与普通名称。
到目前为止,我们经常提到this。在C#中什么是this?
this关键字是指类的当前实例,并且也用作扩展方法的第一个参数的修饰符。
我们稍后将介绍扩展方法。现在让我们通过以下示例概述标准方法与函数(即静态“方法”):
void Main(){ var test = default(Test); test.FooInstance(); Test.FooStatic();}public class Test{ public void FooInstance() { } public static void FooStatic() { }}
结果在以下MSIL代码中:
IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 // testIL_0003: ldloc.0 // testIL_0004: callvirt Test.FooInstanceIL_0009: nop IL_000A: call Test.FooStaticIL_000F: nop IL_0010: ret
方法和函数之间有两个主要区别:
那么,为什么声明的方法为sealed或virtual呢?原因很简单:工程!这是告诉开发人员如何使用代码的一种方式。正如类似private或readonly的东西,隐式的(至少在开始时)仅在编译器的处理中。运行时可以稍后使用此信息进行进一步优化,但由于某些情况,不会产生直接影响。
让我们回顾一下函数应该优于方法。
非常有用 | 应避免的 |
|
|
标准方法和功能在C#中仍然取得了进步,即使它们的目的保持不变(为什么要改变?)。我们得到了一些有用的语法糖来编写一些(冗长的)代码。我们还有新的语言功能,可以帮助在C#中编写更多可重用的函数。
让我们一起来看看扩展方法。
扩展方法是一种相当古老但又简单的机制,可以在广泛的范围内重用功能。编写扩展方法的语法非常简单:
以下作为扩展方法的示例。
public static class Test{ public static void Foo(this object obj) { }}
虽然方法有一个隐式this参数(命名this),但扩展方法有一个明确的“this”参数,它可以有一个我们决定的名字。由于this已经是关键字,我们不能使用它,不幸的是(或者幸运的是,至少乍一看——看起来像标准方法,其绝对不是这样的)。
只有在我们调用它时,才会显示扩展方法(针对普通函数)的优点。
var test = default(object);Test.Foo(test);test.Foo();
虽然第一个调用使用显式语法(如果允许,可以通过在顶部使用using static Test;缩减到仅使用Foo(test)),第二个调用使用扩展方法。
我们可以猜测,从生成的MSIL来看,没有任何区别!
IL_0000: nop IL_0001: ldnull IL_0002: stloc.0 // testIL_0003: ldloc.0 // testIL_0004: call Test.FooIL_0009: nop IL_000A: ldloc.0 // testIL_000B: call Test.FooIL_0010: nop
我们总是加载第一个参数然后调用该函数。两者之间没有魔力!但是,扩展方法看起来更好,还有一个额外的好处......
考虑到已有的泛型函数,如Where,Select,OrderBy,等,这些函数在IEnumerable<T>实例上工作。
如果调用这些函数来引入条件,请选择一个特定属性,并按照某些规则对可枚举进行排序,我们将编写代码,例如:
var result = MyLinq.OrderBy(MyLinq.Select(MyLinq.Where(source, ...), ...), ...);
因此,结果代码需要从内到外(洋葱风格)而不是自然的从左到右的方向读取,如上面给出描述的句子(称为“链接”或“管道”顺序)。这是不幸的,因为它破坏了代码的可读性,并且很难理解发生了什么......对救援的评论?
不是真的,通过使用扩展方法,我们可以滥用第一个参数由“调用实例”隐式给出。因此,代码如下所示:
var result = source.Where(...).Select(...).OrderBy(...);
明确写出MyLinq类的符号也不存在了(没有介绍using static MyLinq)。精彩!
扩展方法也可以用作广义的帮助类。请考虑以下interface:
interface IFoo{ Task FooAsync(); Task FooAsync(CancellationToken cancellationToken);}
在这里,我们已经告诉实现方有两种方法而不是一种方法。我想这个接口的几乎所有实现实际上看起来如下:
class StandardFoo : IFoo{ public Task FooAsync() { return FooAsync(default(CancellationToken)); } public Task FooAsync(CancellationToken cancellationToken) { // real implementation }}
这是不好的。实现者必须做更多不必要的工作。相反,我们可以指定我们的接口和关联的辅助方法,如下所示:
interface IFoo{ Task FooAsync(CancellationToken cancellationToken);}static class IFooExtensions{ public static Task FooAsync(this IFoo foo) { return foo.FooAsync(default(CancellationToken)); }}
太棒了,现在实现的人IFoo只需要处理单一方法并免费获得我们的便利方法。
非常有用 | 应避免的 |
|
|
在上一节中,我们触及了扩展方法的重要性及其对可读代码的含义(从左到右而不是由内向外)。该示例使用类似LINQ的函数集来激励扩展方法。实际上,LINQ(语言集成查询的简称)功能首先引入了(需要)扩展方法。但是,我们在前面的例子中也省略了一个重要的部分......
LINQ只有在我们有一套复杂的选项来定义各种函数的选项时才有效(例如Select)。然而,即使是最复杂的对象结构作为参数也不会给LINQ带来所需的灵活性(并且使其使用真的过于复杂)。因此,我们需要一种特殊的对象作为参数——一个函数。在C#中,传递函数的方式是间接通过所谓的委托。
通过以下语法定义委托:
delegate void Foo(int a, int b);
因此,委托就像函数签名一样编写,其中函数名称由委托的名称替换,并且delegate关键字已用于引入签名。
最终,一个委托被编译成一个带有方法的类Invoke。这种方法的签名等于我们刚刚介绍的签名。
让我们看看MSIL通过一个示例实现(空体)调用我们的委托来揭示更多信息:
IL_0000: nop IL_0001: ldsfld <>c.<>9__0_0IL_0006: dup IL_0007: brtrue.s IL_0020IL_0009: pop IL_000A: ldsfld <>c.<>9IL_000F: ldftn <>c.b__0_0IL_0015: newobj Foo..ctorIL_001A: dup IL_001B: stsfld <>c.<>9__0_0IL_0020: stloc.0 // fooIL_0021: ldloc.0 // fooIL_0022: callvirt Foo.InvokeIL_0027: nop IL_0028: ret Foo.Invoke:Foo.BeginInvoke:Foo.EndInvoke:Foo..ctor:<>c.b__0_0:IL_0000: nop IL_0001: ret <>c..cctor:IL_0000: newobj c..ctorIL_0005: stsfld c.<>9IL_000A: ret <>c..ctor:IL_0000: ldarg.0 IL_0001: call System.Object..ctorIL_0006: nop IL_0007: ret
我们看到生成的类实际上包含了一些功能(也是静态成员)。更重要的是,还有另外两种方法——BeginInvoke和EndInvoke。最后,创建委托不是自由的——它实际上是生成的类的对象创建。调用委托实际上与调用Invoke类上的方法相同。因此,这是virtual 调用并且比调用(例如,函数)更昂贵。
到目前为止,我们只看到了委托是什么以及它是如何声明的。实际上,大多数时候我们不需要自己声明委托。我们可以使用内置的通用声明:
对于诸如事件委托,谓词(例如Func,但固定为返回bool)等事物,还有一些泛型构造。
我们如何实例化一个委托?让我们考虑上面的委托Foo,它接受两个整数参数。
Foo foo = delegate (int a, int b) { /* body */ };foo(2, 3);
或者,我们可能希望将其指向现有函数:
void Sample(int a, int b){ /* body */}Foo foo = new Foo(Sample);foo(2, 3);
生成的MSIL实际上并不完全相同,但此刻不起作用。最后一个实际上也可以简化为Foo foo = Sample,它隐式地处理委托实例的创建。
非常有用 | 应避免的 |
|
|
到现在为止还挺好。我们显然缺少的是更好地编写匿名函数的语法。幸运的是,C#让我们得到了保障。
正如我们已经看到的那样,委托通过将它们很好地打包到类中来传输函数非常方便。但是,现在正在编写一些逻辑,即将匿名函数打包到委托中,看起来非常麻烦和丑陋。
幸运的是,使用C#3不仅引入了LINQ(与扩展方法一起),而且还使用新的“胖箭头”(或lambda)运算符=>编写匿名函数的新语法。
如果我们更改前面的示例以使用lambda表达式,它可能如下所示:
Foo foo = (a, b) => { /* body */ };foo(2, 3);
生成的MSIL与(匿名)委托完全相同。因此,这实际上只是语法糖,但正是我们要求的甜蜜!
非常有用 | 应避免的 |
|
|
还有一件事与LINQ一起引入(C#3非常好,对吧?):它是LINQ表达式!这不是关于查询语法与直接使用扩展方法或类似方法,而是ORM如何使用LINQ。
问题如下:在将LINQ引入C#之前,我们主要是在C#中直接编写SQL查询。虽然这肯定有一些优点(完全访问我们的数据库中提供的所有功能),但缺点是非常真实的:
使用LINQ,这已经通过引入LINQ表达式来解决,LINQ表达式是一种不向MSIL编译匿名函数的方法,而是将生成的AST转换为对象。
尽管这是一个编译器功能,但这一切都归结为使用正确的类型。早些时候,我们已经看到了泛型委托,例如Func或Action允许我们避免再次编写它们。如果我们将这样的委托打包到Expression类型中,我们最终会得到一个AST持有者。
关于这样一个简单示例(实际上,下面的并没有真正编译,因为我们需要右侧的表达式,但这个想法应该是可见的):
Expressionfoo = (a, b) => { /* body */ };
生成的MSIL至少可以说是丑陋的(鉴于它是一个非常简短的例子,我们可以猜出现实生活中的代码可能是什么样子):
IL_0000: nop IL_0001: ldtoken System.Int32IL_0006: call System.Type.GetTypeFromHandleIL_000B: ldstr "a"IL_0010: call System.Linq.Expressions.Expression.ParameterIL_0015: stloc.1 IL_0016: ldtoken System.Int32IL_001B: call System.Type.GetTypeFromHandleIL_0020: ldstr "b"IL_0025: call System.Linq.Expressions.Expression.ParameterIL_002A: stloc.2 IL_002B: ldnull IL_002C: ldtoken NothingIL_0031: call System.Reflection.MethodBase.GetMethodFromHandleIL_0036: castclass System.Reflection.MethodInfoIL_003B: call System.Array.EmptyIL_0040: call System.Linq.Expressions.Expression.CallIL_0045: ldc.i4.2 IL_0046: newarr System.Linq.Expressions.ParameterExpressionIL_004B: dup IL_004C: ldc.i4.0 IL_004D: ldloc.1 IL_004E: stelem.ref IL_004F: dup IL_0050: ldc.i4.1 IL_0051: ldloc.2 IL_0052: stelem.ref IL_0053: call System.Linq.Expressions.Expression.Lambda IL_0058: stloc.0 // fooIL_0059: ret
从本质上讲,此调用的整个生成的AST现在以对象格式提供——因此包含在MSIL中。
ORM可以检查此信息以创建优化的查询,从而安全地传输变量和特殊字段,而不会以任何形式劫持。由于委托仍然是强类型的,因此结果可以强类型化(并由ORM声明)。但是,即使不编写ORM,我们也可以使用LINQ表达式吗?
在许多情况下,LINQ表达式可以派上用场。一个例子是它们在ASP.NET MVC / Razor视图中的使用方式。在这里,我们需要从给定模型中选择一个属性。现在,由于C#的类型系统相当有限,因此没有办法减少(并帮助)开发人员缩小潜在字符串(对所有属性名称)。相反,使用LINQ表达式“选择”该属性。
Expression> selectedProperty = model => model.PropertyName;
现在我们仍然需要一些魔术来评估它,但是,一般来说,从上面给定的表达式获取属性名称或信息是非常简单的:
static PropertyInfo GetPropertyInfo(this T model, Expression > propertyLambda){ var type = typeof(T); var member = propertyLambda.Body as MemberExpression ?? throw new ArgumentException($"Expression '{propertyLambda.ToString()}' refers to a method, not a property."); var propInfo = member.Member as PropertyInfo ?? throw new ArgumentException($"Expression '{propertyLambda.ToString()}' refers to a field, not a property."); if (type != propInfo.ReflectedType && !type.IsSubclassOf(propInfo.ReflectedType)) throw new ArgumentException($"Expression '{propertyLambda.ToString()}' refers to a property that is not from type {type}."); return propInfo;}
问题解决了——仍然是强类型的,没有使用魔术字符串。
非常有用 | 应避免的 |
|
|
从C#7开始,应该在语言中引入更多功能元素。这也意味着有更多的表达式(而不仅仅是语句)和更简单/简洁的语法。这种“清理”并没有停留在标准功能上。
public static int Foo(int a, int b){ return a + b;}
这是一个非常简单的例子,需要4行代码(至少如果我们遵循通用样式指南)。编译的MSIL如下所示:
IL_0000: nop IL_0001: ldarg.0 IL_0002: ldarg.1 IL_0003: add IL_0004: stloc.0 IL_0005: br.s IL_0007IL_0007: ldloc.0 IL_0008: ret
使用方法表达式,我们可以将它减少到C#中的一行(不与任何样式指南冲突):
public static int Foo(int a, int b) => a + b;
生成的MSIL也有点不同:
IL_0000: ldarg.0 IL_0001: ldarg.1 IL_0002: add IL_0003: ret
我们已经看到在前一篇文章中已经使用属性(或getter / setter)表达式进行了类似的缩减。丢失的4条指令都涉及标准语法中引入的范围。
非常有用 | 应避免的 |
|
|
最后!局部函数是函数内的函数。这首先听起来更加微不足道,但让我们等一下才能看到它真正的优势。
一个非常简单的例子:
void LongFunction(){ void Cleanup() { // Define cleanup logic here } // ... many steps if (specialCondition) { // ... Cleanup(); return; } // ... many steps if (specialCondition) { // ... Cleanup(); return; } // ...many steps Cleanup();}
虽然这可能看起来像“坏样式”或函数实现出错,但是函数可能有很多原因看起来像这样。然而,在过去,我们不得不回到一些非常特殊的模式来实现这一目标。我们必须要么
后者可能带来其他问题(例如,传输所有必需的值)。
所以,这已经是一个巨大的胜利,我们可以在可重用代码中定义一个可重用代码块。但就像匿名函数一样,这样的局部函数能够从外部作用域捕获值。
让我们看一下使用匿名函数捕获:
var s = "Hello, ";var call = new Action(m => (s + m).Dump());call("world");
生成的MSIL代码如下所示:
IL_0000: newobj <>c__DisplayClass0_0..ctorIL_0005: stloc.0 // CS$<>8__locals0IL_0006: nop IL_0007: ldloc.0 // CS$<>8__locals0IL_0008: ldstr "Hello, "IL_000D: stfld <>c__DisplayClass0_0.sIL_0012: ldloc.0 // CS$<>8__locals0IL_0013: ldftn <>c__DisplayClass0_0.b__0IL_0019: newobj System.Action ..ctorIL_001E: stloc.1 // callIL_001F: ldloc.1 // callIL_0020: ldstr "world"IL_0025: callvirt System.Action .InvokeIL_002A: nop IL_002B: ret <>c__DisplayClass0_0. b__0:IL_0000: ldarg.0 IL_0001: ldfld <>c__DisplayClass0_0.sIL_0006: ldarg.1 IL_0007: call System.String.ConcatIL_000C: call DumpIL_0011: pop IL_0012: ret
给定代码中没有那么多有趣的部分。我们已经知道的大多数部分,例如委托需要首先实例化。但是,临时(生成)类中有一行是有趣的。
在IL_000D中,我们将常量字符串"Hello, "分配给字段s。这是从外部范围捕获变量s!
让我们重写上面的代码来代替使用局部函数。
var s = "Hello, "; void call(string m){ (s + m).Dump();} call("world");
现在MSIL已改为:
IL_0000: nop IL_0001: ldloca.s 00 // CS$<>8__locals0IL_0003: ldstr "Hello, "IL_0008: stfld <>c__DisplayClass0_0.sIL_000D: nop IL_000E: ldstr "world"IL_0013: ldloca.s 00 // CS$<>8__locals0IL_0015: callg__call|0_0IL_001A: nop IL_001B: ret g__call|0_0:IL_0000: nop IL_0001: ldarg.1 IL_0002: ldfld <>c__DisplayClass0_0.sIL_0007: ldarg.0 IL_0008: call System.String.ConcatIL_000D: call DumpIL_0012: pop IL_0013: ret
代码要短得多!如果我们仔细观察,我们会发现很多节省来自于不必去处理委托(即没有其实例,没有callvirt,......)。
但等一下——还有什么?以前,我们有更多的使用c__DisplayClass0_0的调用,比如调用它的构造函数。所有这一切都消失了,为什么?原因很简单——生成c__DisplayClass0_0的不再是类,而是结构!作为结构,我们不需要任何构造函数调用,因为(实际)默认构造函数存在。
我们可以拥有结构而不是类的原因是局部函数仍然是局部的。没有必要担心它在代码块结束时被销毁。是的,局部函数可以自己捕获,但是,在本例中,我们有不同的结构,我们不会失去一致性。
非常有用 | 应避免的 |
|
|
C#的演变并没有停止在函数上。我们从简单的方法到完整的函数,增加了可扩展性,AST生成,匿名函数的简单语法和局部可重用代码块。我们已经看到C#从最初版本开始如何发展。
下一篇:
原文地址:
转载地址:http://gczhj.baihongyu.com/