博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
使C#代码现代化——第二部分:方法
阅读量:3531 次
发布时间:2019-05-20

本文共 12538 字,大约阅读时间需要 41 分钟。

目录


介绍

近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单个问题的许多潜在(语言)解决方案。这既好又坏。很好是因为它给了我们成为开发人员的自由和权力(不会影响向后兼容性),并且由于与决策相关的认知负荷而导致不好。

在本系列中,我们希望了解存在哪些选项以及这些选项的不同之处。当然,在某些条件下,某些人可能有优点和缺点。我们将探索这些场景,并提出一个指南,以便在翻新现有项目时让我们的生活更轻松。

这是该系列的第二部分。您也可以查看

背景

我觉得重要的是要讨论新语言功能的发展方向以及旧语言的位置——让我们称之为已建立 ——这些仍然是首选。我可能并不总是对的(特别是,因为我的一些观点肯定会更主观/是品味问题)。像往常一样,留下评论讨论将不胜感激!

让我们从一些历史背景开始。

什么是方法?

我们常常看到人们对术语——方法功能感到困惑。实际上,每个方法都是一个函数,但并不是每个函数都是一个方法。方法是一种特殊的函数,它具有隐含的第一个参数,称为上下文。上下文被赋予this并且与指定方法的类的实例相关联。因此,(真实)方法只能存在于类中,而static方法应该被称为函数。

虽然人们通常认为this保证非null的,但实际上是人为的限制。实际上,运行时对第一个参数执行一些隐式(通过使方法的调用成为callvirt指令)检查,但是,理论上这可以很容易地避开。

使用隐式this参数,可以派生出一种特殊的语法。我们发现c.f(a, b, ...) 而不是调用一个函数ff(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();    }}

正如我们所看到的,自C6起类也可以被认为是名称空间——至少对于它的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

方法和函数之间有两个主要区别:

  1. 方法需要加载它所属的实例。这将是隐含的第一个参数。
  2. 总是使用callvirt(如果我们使用sealed或者明确地使用virtual)调用方法。

那么,为什么声明的方法为sealedvirtual呢?原因很简单:工程!这是告诉开发人员如何使用代码的一种方式。正如类似privatereadonly的东西,隐式的(至少在开始时)仅在编译器的处理中。运行时可以稍后使用此信息进行进一步优化,但由于某些情况,不会产生直接影响。

让我们回顾一下函数应该优于方法。

非常有用

应避免的

  • 小的帮助类
  • 独立于特定(类)实例
  • 没有与virtual 调用相关的成本/检查
  • 如果你寻找继承

现代方式

标准方法和功能在C#中仍然取得了进步,即使它们的目的保持不变(为什么要改变?)。我们得到了一些有用的语法糖来编写一些(冗长的)代码。我们还有新的语言功能,可以帮助在C#中编写更多可重用的函数。

让我们一起来看看扩展方法。

扩展方法

扩展方法是一种相当古老但又简单的机制,可以在广泛的范围内重用功能。编写扩展方法的语法非常简单:

  • 我们需要一个static类(不能实例化,不能继承,不允许实例成员)
  • 我们需要一个static方法(即功能)
  • 需要至少一个参数(称为扩展目标)
  • 必须使用this关键字修饰第一个参数

以下作为扩展方法的示例。

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

我们总是加载第一个参数然后调用该函数。两者之间没有魔力!但是,扩展方法看起来更好,还有一个额外的好处......

考虑到已有的泛型函数,如WhereSelectOrderBy,等,这些函数在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只需要处理单一方法并免费获得我们的便利方法。

非常有用

应避免的

  • 通用接口方法
  • 具有至少1个参数的辅助方法
  • 替换普通的类实例方法

委托

在上一节中,我们触及了扩展方法的重要性及其对可读代码的含义(从左到右而不是由内向外)。该示例使用类似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

我们看到生成的类实际上包含了一些功能(也是静态成员)。更重要的是,还有另外两种方法——BeginInvokeEndInvoke。最后,创建委托不是自由的——它实际上是生成的类的对象创建。调用委托实际上与调用Invoke类上的方法相同。因此,这是virtual 调用并且比调用(例如,函数)更昂贵。

到目前为止,我们只看到了委托是什么以及它是如何声明的。实际上,大多数时候我们不需要自己声明委托。我们可以使用内置的通用声明:

  • Action<T...>为所有代表返回void(什么也没有)
  • Func<T..., TReturn>为所有委托返回一些东西TReturn

对于诸如事件委托,谓词(例如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#让我们得到了保障。

Lambda表达式

正如我们已经看到的那样,委托通过将它们很好地打包到类中来传输函数非常方便。但是,现在正在编写一些逻辑,即将匿名函数打包到委托中,看起来非常麻烦和丑陋。

幸运的是,使用C3不仅引入了LINQ(与扩展方法一起),而且还使用新的胖箭头(或lambda)运算符=>编写匿名函数的新语法。

如果我们更改前面的示例以使用lambda表达式,它可能如下所示:

Foo foo = (a, b) => { /* body */ };foo(2, 3);

生成的MSIL与(匿名)委托完全相同。因此,这实际上只是语法糖,但正是我们要求的甜蜜!

非常有用

应避免的

  • 匿名函数
  • 可重复使用的代码块

LINQ表达式

还有一件事与LINQ一起引入(C3非常好,对吧?):它是LINQ表达式!这不是关于查询语法与直接使用扩展方法或类似方法,而是ORM如何使用LINQ

问题如下:在将LINQ引入C#之前,我们主要是在C#中直接编写SQL查询。虽然这肯定有一些优点(完全访问我们的数据库中提供的所有功能),但缺点是非常真实的:

  • 没有编译器的帮助
  • 潜在的安全问题
  • 结果没有是静态类型的

使用LINQ,这已经通过引入LINQ表达式来解决,LINQ表达式是一种不向MSIL编译匿名函数的方法,而是将生成的AST转换为对象。

尽管这是一个编译器功能,但这一切都归结为使用正确的类型。早些时候,我们已经看到了泛型委托,例如FuncAction允许我们避免再次编写它们。如果我们将这样的委托打包到Expression类型中,我们最终会得到一个AST持有者。

关于这样一个简单示例(实际上,下面的并没有真正编译,因为我们需要右侧的表达式,但这个想法应该是可见的):

Expression
foo = (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.Empty
IL_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;}

问题解决了——仍然是强类型的,没有使用魔术字符串。

非常有用

应避免的

  • ORM映射
  • 与外部系统通信
  • 规避型系统限制
  • 实际调用的函数

方法表达式

C7开始,应该在语言中引入更多功能元素。这也意味着有更多的表达式(而不仅仅是语句)和更简单/简洁的语法。这种清理并没有停留在标准功能上。

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();}

虽然这可能看起来像坏样式或函数实现出错,但是函数可能有很多原因看起来像这样。然而,在过去,我们不得不回到一些非常特殊的模式来实现这一目标。我们必须要么

  • 最后的特殊部分中使用goto(在前一个例子中我们称之为清理),或者
  • 在正在实现的IDisposable类的Dispose方法中使用using语句和清理代码。

后者可能带来其他问题(例如,传输所有必需的值)。

所以,这已经是一个巨大的胜利,我们可以在可重用代码中定义一个可重用代码块。但就像匿名函数一样,这样的局部函数能够从外部作用域捕获值。

让我们看一下使用匿名函数捕获:

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:  call        
g__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/

你可能感兴趣的文章
记第一次面试经历
查看>>
网站实现qq登录(springboot后台)
查看>>
简单的用户头像修改功能(springboot后台)
查看>>
springboot+mybatis实现分页
查看>>
leetcode332. 重新安排行程
查看>>
为什么局域网网段不同不能通信?
查看>>
itchat微信助手,kaggle 电影数据集分析,基于内容的电影推荐
查看>>
认识和使用JWT
查看>>
通过springboot框架,自己动手实现oauth2.0授权码模式认证
查看>>
条件表达式于运算符的点点滴滴的积累
查看>>
最短路径最基本的三种算法【此后无良辰】
查看>>
class的点点滴滴的总结
查看>>
vector 的点点滴滴的总结
查看>>
测试用例
查看>>
自动化测试学习步骤
查看>>
自动化测试需要掌握的知识
查看>>
HTTP协议
查看>>
Python问题总结01
查看>>
Python小程序——冒泡排序
查看>>
cmd中输入net start mysql 提示:服务名无效或者MySQL正在启动 MySQL无法启动
查看>>