撰写了文章 发布于 2020-04-12 12:18:09
从C#开始的编程入门——方法的其它存在形式
到目前为止,我们学习了如何在一个类型中定义一个实例方法、静态方法,以及如果把一个方法包装成一个委托,但是实际上方法还有其它的存在形式。
表达式主体
这虽然不是一种方法的存在形式,它只是一种语法糖,但是它引出了后面的一些内容。
很多时候我们可能需要一些方法简单地返回某个表达式的值或者简单地执行另一个方法,这个时候我们或许会按部就班地写上括号和return:
public static int Add(int a, int b) { return a + b; }
这样的方法可以简化为这种形式:
public static int Add(int a, int b) => a + b;
=>表示方法返回的表达式,这样一来这类简单的方法就可以不用起那么多行结果只写了一句话,可以让代码看起来更加简洁。这样的方法定义被称为表达式主体(expression body,body指的就是方法后面大括号内的具体定义部分)。除了返回值,只有一个语句不返回值的方法也可以这样写。要注意表达式主体只允许有一个表达式或者语句,毕竟如果你需要写多个语句的话这个方法就不算是“简单方法”,而需要使用大括号更好地组织了。
本地方法
方法除了在一个类中定义,还可以嵌套定义在另一个方法中。这样的方法本称为本地方法(或局部方法),它只能在包围它的方法中被使用,它同样可以访问类型中的各种成员,但是一旦离开了本地方法被定义的方法,其他地方便无法调用它。对于一些仅在某一个方法中重复的代码片段,可以定义这样的本地方法来减少重复工作。
public void Process() { void PrintOk() => System.Console.WriteLine(“OK”); System.Console.WriteLine(“Booting…”); PrintOk(); System.Console.WriteLine(“Logging…”); PrintOk(); }
标准库中的委托类型
前面我们讲到了自己定义和使用委托的方法。实际上在.NET基础类库中也内置了一些通用的委托类型可以使用,在某些时候我们可以省去自行定义的重复性工作。值得注意的是,这些委托是范型的。
范型委托
方法有范型的,引用方法的委托类型自然也可以是范型的,从而可以引用范型方法。比如说范型的Add方法就需要一个这样的委托:
delegate T Add<T>(T a, T b);
Action
基础类库中提供了一系列Action为名的委托类型。它们用于引用各种不返回值的方法,Action一共有17个版本,范型版本最高可以使用16个类型参数,代表有16个参数、不返回值的方法。 使用时和一般的委托没有区别:
static void ItsRaining(Action DoSomething) { DoSomething?.Invoke(); }
Func
Func即function,用于引用一些函数形式的方法,和Action唯一的不同是Func引用的方法有返回值,因此Func的所有版本都是范型委托,最后一个类型参数为返回值类型,有一个以上的类型参数时前面的类型参数都是参数类型。
int Add(int a, int b) => a + b; Func<int, int, int> operationOnTwoOperands = Add;
这些标准库中的委托类型为我们提供了方便,在不需要进行严格定义的时候可以直接使用这些委托。
方法和函数:前面提到过函数和方法的概念。通常对于在类的实例上执行的函数称为方法,但是实际上很多概念并非只有面向对象语言才支持,很多概念对于方法和函数来说都是一样的。因此后面可能会交替使用函数和方法两个词。
Lambda表达式
到目前为止,我们定义的方法都有各自的名字,无论我们是要调用还是要通过委托引用都要提供一个方法的名字。但实际上很多时候我们可能并不是一直需要某个方法,有可能我们只是为了在调用某个方法时将它作为委托传递进去以满足要求。比如说前面的ItsRaining方法,它接受一个委托类型的参数,我们可能想调用多个方法,或者根据被调用时的情况调用不同的方法。这个时候可以写成一个单独的方法,但它的作用有限,实在是有点多余。
public static void DoWhenRaining() { DoThatThing(); if (IsOk) { DoOtherThing(); } }
面对这种情况,我们需要一种简单的方法来定义这些简单的方法,这就是Lambda表达式的作用之一。
Lambda表达式正如其名,是一种表达式。和其它返回各种类型的表达式一样,它会产生一个值,Lambda表达式的值是一个方法,或者说委托。它和一般的方法不一样,它是匿名的。
Lambda表达式的书写方式和表达式主体有一些类似地地方。首先以参数列表开始,如果没有参数则括号内留空,但不能省去括号。参数的类型可以全部显式注明,也可以全部省略,但是不能部分省略。紧接着是=>运算符,后面是主体部分,如果只有一个语句或表达式则可以省去大括号,否则必须加上大括号。
(int a, int b) => a == b;
Lambda表达式总是可以转换为和其产生的方法签名兼容的委托。我们可以通过一个变量来保存lambda表达式的结果,但是我们不能通过var推断,我们通常需要一个委托类型的变量或者参数来处理:
Func<int> func = () => 1; Action action = () => System.Console.WriteLine(“Hello”); ItsRaining(()=>System.Console.WriteLine(“It’s raining!”));
然后我们可以照常调用这个委托。关于C#中的lambda表达式可以参阅一下这里。
Lambda表达式与闭包
思考下面的代码:
class Factory { private static int i = 0; public Func<int> FuncFactory(int n) { Console.WriteLine($”i is {i} before return from factory.”); return () => { Console.WriteLine($”i is {i} before lambda run”); return i += n; }; } }
FuncFactory会在每次被调用时都返回一个新的委托,这个委托会为i加上n。 随后执行下面的代码:
Factory factory = new Factory(); var add1 = factory.FuncFactory(1); var add2 = factory.FuncFactory(2); System.Console.WriteLine(add2()); System.Console.WriteLine(add1());
试着猜一猜会输出什么:给add1和add2赋值时,FuncFactory都会输出i为0,而调用返回的委托时分别会输出0和2,WriteLine会输出增加后的i的值分别为2和3。
你可能产生了好几个问题。首先,lambda表达式产生的匿名方法在从方法作为委托返回时会被调用吗?答案是不会。委托只是单纯地引用了这个返回的匿名方法,它会在我们主动调用委托时调用,就像之前学习委托和事件时那样。
add1被调用时,我们发现此时i已经变成了2,而不是add1引用的从FuncFactory返回时i的值。也就是说实际上生成的匿名方法在被调用时,i实际上还是代表着当时factory中的i,而不是单纯地记录了一个值,就好像这个匿名方法在factory内部被调用一样。
既然如此,那么为什么这样的代码是合法的呢?为什么即使离开了Factory类的作用域,也可以在外部通过委托引用的匿名方法访问private的i呢?
实际上这些委托是一类特殊的方法,它们被称作闭包。
闭包
闭包(closure)是如今很多编程语言都支持的一种特性。闭包究竟“闭”(close)了什么?闭包将匿名函数内部的一些变量绑定到了其被定义时所在作用域中的某个变量上,而在闭包执行时,这些被闭合到这些变量的名称仍然保留了和原本被定义时所在的作用域的联系。闭包就像一个黑箱子,生成闭包的类型把一些不为人知的东西装到里面,并且密封起来,而最终使用闭包的类型却对其内部一无所知,但依然可以照常使用。
前面多次提到过,委托和我们直接通过对象和方法名调用方法不一样,委托不会立即执行,而是在我们需要执行它时,并且提供了所需参数时才会执行。看下面这个例子:
Func<int,int,int> func = (a, b) => a + b; System.Console.WriteLine(func(3, 4)); // 7
首先这样的代码完全合法。我们拿到了一个委托,这个委托引用了一个lambda表达式,在执行这条语句的一瞬间,a和b实际上没有任何意义(它们没有绑定任何值),因此不会也不可能立即执行。唯一确定的是,根据左侧的类型进行推断,它们是int类型,会执行加法操作并返回结果。
直到func被调用时,给委托传递进了两个值,这时a和b才绑定了各自的值,开始执行lambda中定义的操作,最终得出7。那么再看下面的代码(和上面的代码互不影响是单独的片段):
int a = 1; int b = 2; Func<int, int, int> func = (a, b) => a + b; System.Console.WriteLine(func(3, 4)); // ?
正确答案是:7。我们把lambda就当成一个本地函数看,根据C#的作用域规则,由于在lambda内部使用a和b时,内部已经定义了a和b两个名称,是lambda的两个参数,它不会试图查找外面的a和b,在执行lambda时a和b依然绑定到传递进来的参数上。最后还有一个例子:
int a = 1; int b = 2; Func<int, int, int> func = (lhs, rhs) => a + b; System.Console.WriteLine(func(3, 4)); // ?
这一次,答案变成了:3。这个时候a和b在lambda中没有定义,但是这并不会造成变量未定义的异常,它会向外查找,最终它查找到了外部的a和b(依然把lambda当成本地方法看),然后绑定到了lambda中,这个过程被称为捕获(capture)。随后无论引用这个闭包的委托处在哪个作用域,a和b都会被确确实实地绑定到闭包中。
我多次提到,C#中没有脱离某个类型单独存在的全局函数,说到这里你会发现我们使用lambda表达式产生的方法似乎并没有对应的类型。前面提到了一个闭包会捕获它被定义时作用域(通常是另一个方法,而另一个方法又属于某个类型)中的变量,那么除了闭包,一般来说谁可以访问那个作用域中的变量呢?当然是那个类型的成员,因此实际上编译器在处理lambda时往往都会在它被定义的类中生成又一个方法(这就是一个匿名方法)继而包装成委托返回给其他地方。因此实际上C#的lambda表达式也可以看作是一种语法糖。
世俗骑士 1年前
最近刚好在琢磨骑砍2的Mod制作,C#的语法指南在这里会有帮助
黛黛冬优子 [作者] 1年前
世俗骑士 1年前
发布