使C#代码现代化——第二部分:方法

目录

介绍

背景

什么是方法?

现代方式

扩展方法

委托

Lambda表达式

LINQ表达式

方法表达式

局部函数

结论


介绍

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

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

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

背景

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

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

什么是方法?

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

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

使用隐式this参数,可以派生出一种特殊的语法。我们发现c.f(a, b, ...) 而不是调用一个函数ff(a, b, ...)那样,其中c是某个类的实例。所有其他情况可以被认为是完全限定的名称,请参阅

// file a.cs

namespace A
{
    public class Foo
    {
        public static void Hello()
        {
            // some code
        }
    }
}

// file b.cs
using A;

public class Bar
{
    public static void Test()
    {
        Foo.Hello();
    }
}

// file c.cs
using 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     // test
IL_0003:  ldloc.0     // test
IL_0004:  callvirt    Test.FooInstance
IL_0009:  nop         
IL_000A:  call        Test.FooStatic
IL_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     // test
IL_0003:  ldloc.0     // test
IL_0004:  call        Test.Foo
IL_0009:  nop         
IL_000A:  ldloc.0     // test
IL_000B:  call        Test.Foo
IL_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_0
IL_0006:  dup         
IL_0007:  brtrue.s    IL_0020
IL_0009:  pop         
IL_000A:  ldsfld      <>c.<>9
IL_000F:  ldftn       <>c.b__0_0
IL_0015:  newobj      Foo..ctor
IL_001A:  dup         
IL_001B:  stsfld      <>c.<>9__0_0
IL_0020:  stloc.0     // foo
IL_0021:  ldloc.0     // foo
IL_0022:  callvirt    Foo.Invoke
IL_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..ctor
IL_0005:  stsfld      c.<>9
IL_000A:  ret         

<>c..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_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> foo = (a, b) => { /* body */ };

生成的MSIL至少可以说是丑陋的(鉴于它是一个非常简短的例子,我们可以猜出现实生活中的代码可能是什么样子):

IL_0000:  nop         
IL_0001:  ldtoken     System.Int32
IL_0006:  call        System.Type.GetTypeFromHandle
IL_000B:  ldstr       "a"
IL_0010:  call        System.Linq.Expressions.Expression.Parameter
IL_0015:  stloc.1     
IL_0016:  ldtoken     System.Int32
IL_001B:  call        System.Type.GetTypeFromHandle
IL_0020:  ldstr       "b"
IL_0025:  call        System.Linq.Expressions.Expression.Parameter
IL_002A:  stloc.2     
IL_002B:  ldnull      
IL_002C:  ldtoken     Nothing
IL_0031:  call        System.Reflection.MethodBase.GetMethodFromHandle
IL_0036:  castclass   System.Reflection.MethodInfo
IL_003B:  call        System.Array.Empty<Expression>
IL_0040:  call        System.Linq.Expressions.Expression.Call
IL_0045:  ldc.i4.2    
IL_0046:  newarr      System.Linq.Expressions.ParameterExpression
IL_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<Foo>
IL_0058:  stloc.0     // foo
IL_0059:  ret

从本质上讲,此调用的整个生成的AST现在以对象格式提供——因此包含在MSIL中。

ORM可以检查此信息以创建优化的查询,从而安全地传输变量和特殊字段,而不会以任何形式劫持。由于委托仍然是强类型的,因此结果可以强类型化(并由ORM声明)。但是,即使不编写ORM,我们也可以使用LINQ表达式吗?

在许多情况下,LINQ表达式可以派上用场。一个例子是它们在ASP.NET MVC / Razor视图中的使用方式。在这里,我们需要从给定模型中选择一个属性。现在,由于C#的类型系统相当有限,因此没有办法减少(并帮助)开发人员缩小潜在字符串(对所有属性名称)。相反,使用LINQ表达式选择该属性。

Expression<Func<TModel, TProperty>> selectedProperty = model => model.PropertyName;

现在我们仍然需要一些魔术来评估它,但是,一般来说,从上面给定的表达式获取属性名称或信息是非常简单的:

static PropertyInfo GetPropertyInfo<T, TProperty>
     (this T model, Expression<Func<T, TProperty>> 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_0007
IL_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<string>(m => (s + m).Dump());
call("world");

生成的MSIL代码如下所示:

IL_0000:  newobj      <>c__DisplayClass0_0..ctor
IL_0005:  stloc.0     // CS$<>8__locals0
IL_0006:  nop         
IL_0007:  ldloc.0     // CS$<>8__locals0
IL_0008:  ldstr       "Hello, "
IL_000D:  stfld       <>c__DisplayClass0_0.s
IL_0012:  ldloc.0     // CS$<>8__locals0
IL_0013:  ldftn       <>c__DisplayClass0_0.<Main>b__0
IL_0019:  newobj      System.Action<System.String>..ctor
IL_001E:  stloc.1     // call
IL_001F:  ldloc.1     // call
IL_0020:  ldstr       "world"
IL_0025:  callvirt    System.Action<System.String>.Invoke
IL_002A:  nop         
IL_002B:  ret         

<>c__DisplayClass0_0.<Main>b__0:
IL_0000:  ldarg.0     
IL_0001:  ldfld       <>c__DisplayClass0_0.s
IL_0006:  ldarg.1     
IL_0007:  call        System.String.Concat
IL_000C:  call        Dump
IL_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__locals0
IL_0003:  ldstr       "Hello, "
IL_0008:  stfld       <>c__DisplayClass0_0.s
IL_000D:  nop         
IL_000E:  ldstr       "world"
IL_0013:  ldloca.s    00 // CS$<>8__locals0
IL_0015:  call        <Main>g__call|0_0
IL_001A:  nop         
IL_001B:  ret         

<Main>g__call|0_0:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldfld       <>c__DisplayClass0_0.s
IL_0007:  ldarg.0     
IL_0008:  call        System.String.Concat
IL_000D:  call        Dump
IL_0012:  pop         
IL_0013:  ret

代码要短得多!如果我们仔细观察,我们会发现很多节省来自于不必去处理委托(即没有其实例,没有callvirt......)。

但等一下——还有什么?以前,我们有更多的使用c__DisplayClass0_0的调用,比如调用它的构造函数。所有这一切都消失了,为什么?原因很简单——生成c__DisplayClass0_0的不再是类,而是结构!作为结构,我们不需要任何构造函数调用,因为(实际)默认构造函数存在。

我们可以拥有结构而不是类的原因是局部函数仍然是局部的。没有必要担心它在代码块结束时被销毁。是的,局部函数可以自己捕获,但是,在本例中,我们有不同的结构,我们不会失去一致性。

非常有用

应避免的

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

结论

C#的演变并没有停止在函数上。我们从简单的方法到完整的函数,增加了可扩展性,AST生成,匿名函数的简单语法和局部可重用代码块。我们已经看到C#从最初版本开始如何发展。

下一篇:使C#代码现代化——第三部分:值

 

原文地址:https://www.codeproject.com/Articles/1342509/Modernize-Your-Csharp-Code-Part-II-Methods

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值