65.9K
CodeProject 正在变化。 阅读更多。
Home

Lambda 的方式

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (189投票s)

2012年12月12日

CPOL

42分钟阅读

viewsIcon

278171

downloadIcon

1625

对 lambda 表达式的介绍以及关于如何以及在哪里使用它们的深入讨论。本文还将展示一些已知的设计模式,并介绍一些可能有所帮助的新设计模式。

A simple lambda expression

目录

引言

Lambda 表达式是使代码更具动态性、更易于扩展且更快速的强大方式(如果您想知道原因,请参阅本文!)。它们还可以用于减少潜在错误,并利用静态类型和 IntelliSense 以及 Visual Studio 的优秀 IDE。

Lambda 表达式随 .NET Framework 3.5 和 C# 3 一起引入,并且与 LINQ 等技术或 ASP.NET MVC 背后的许多技术一起发挥了重要作用。如果您考虑 ASP.NET MVC 中各种控件的实现,您会发现大部分魔力实际上是通过使用 lambda 表达式来实现的。将 HTML 扩展方法与 lambda 表达式结合使用将利用您在后台实际创建的模型。

在本文中,我将尝试涵盖以下内容:

  • 简要介绍 - 什么是 lambda 表达式,为什么它们与匿名方法(我们之前就有的!)不同?
  • 仔细研究 lambda 表达式的性能 - 是否存在我们与标准方法相比性能有所提高或降低的场景?
  • 非常仔细地查看 - lambda 表达式在 MSIL 代码中是如何处理的?
  • 一些从 JavaScript 世界移植到 C# 的模式。
  • lambda 表达式表现出色的场景 - 无论是性能方面还是纯粹的便利性。
  • 我提出的一些新模式(也许其他人也提出过 - 但我不知道)。

因此,如果您期望这里有一个初学者教程,我可能会让您失望,除非您是一个非常高级且聪明的初学者。无需多说,我不是那样的人,所以我想警告您:对于本文,您需要一些高级 C# 知识,并且应该熟悉该语言。

您可以期待的是一篇试图解释一些事情的文章。文章还将探讨一些(至少对我来说)有趣的问题。最后,我将展示一些可以在某些场合使用的实际示例和模式。我发现 lambda 表达式可以简化许多场景,以至于编写明确的模式可能会很有用。

背景 - 什么是 lambda 表达式?

在 C# 的第一个版本中,引入了委托的概念。此概念的集成是为了能够传递函数。在某种意义上,委托是指向强类型(和托管)函数的指针。当然,委托可以做更多的事情,但本质上就是这样。问题在于,传递函数需要相当多的步骤(通常):

  1. 编写委托(如类),包括指定返回类型和参数类型。
  2. 在应接收签名由委托描述的函数的_方法_中使用委托作为类型。
  3. 创建委托的实例,其中包含要通过该委托类型传递的特定函数。

如果这听起来很复杂,那应该是的,因为本质上它就是(嗯,这并非高科技,但比您预期的要多得多)。因此,步骤 3 通常不是必需的,C# 编译器会为您创建委托。仍然需要步骤 1 和 2!

幸运的是,C# 2 带来了泛型。现在我们可以编写泛型类、方法,更重要的是:泛型委托!然而,直到 .NET Framework 3.5,才有人在 Microsoft 意识到,实际上只需要两个(带有一些“重载”)泛型委托来覆盖 99% 的委托用例。

  • 不带任何输入参数的 Action(无输入,无输出)以及泛型重载。
  • Action<T1, ..., T16>,它接受 1 到 16 个类型作为参数(无输出),以及。
  • Func<T1, ..., T16, Tout>,它接受 0 到 16 个类型作为输入参数和 1 个输出参数。

Action(及其相应的泛型)返回 void(即,这真的只是一个执行某事的_动作_),Func 实际上返回最后一个指定类型的值。有了这两个委托(及其重载),我们确实可以在大多数情况下跳过第一步。第二步仍然是必需的,但只使用 ActionFunc

那么,如果我只想运行一些代码怎么办?C# 2 已经解决了这个问题。在此版本中,您可以创建委托函数,即匿名函数。然而,该语法从未流行起来。一个非常简单的匿名方法示例如下:

Func<double, double> square = delegate (double x) {
	return x * x;
}

那么,让我们改进这种语法并扩展可能性。欢迎来到 lambda 表达式的世界!首先,这个名字来自哪里?这个名字实际上源自数学中的 lambda 演算,它基本上说明了表达函数真正需要什么。更准确地说,它是一种数学逻辑形式系统,用于通过变量绑定和替换来表达计算。所以基本上,我们有 0 到 N 个输入参数和一个返回值。在我们的编程语言中,我们也可以没有返回值(void)。

让我们看一些 lambda 表达式示例:

//The compiler cannot resolve this, which makes the usage of var impossible! Therefore we need to specify the type.
Action dummyLambda = () => { Console.WriteLine("Hello World from a Lambda expression!"); };

//Can be used as with double y = square(25);
Func<double, double> square = x => x * x;

//Can be used as with double z = product(9, 5);
Func<double, double, double> product = (x, y) => x * y;

//Can be used as with printProduct(9, 5);
Action<double, double> printProduct = (x, y) => { Console.WriteLine(x * y); };

//Can be used as with var sum = dotProduct(new double[] { 1, 2, 3 }, new double[] { 4, 5, 6 });
Func<double[], double[], double> dotProduct = (x, y) => {
	var dim = Math.Min(x.Length, y.Length);
	var sum = 0.0;
	for(var i = 0; i != dim; i++)	
		sum += x[i] + y[i];
	return sum;
};

//Can be used as with var result = matrixVectorProductAsync(...);
Func<double[,], double[], Task<double[]>> matrixVectorProductAsync = async (x, y) => {
	var sum = 0.0;
	/* do some stuff using await ... */
	return sum;
};

我们直接从这些陈述中学到的东西:

  • 如果我们只有 **一个** 参数,那么我们可以省略圆括号 ()
  • 如果我们只有一个语句并且想返回它,那么我们可以省略大括号 {} 并跳过 return 关键字。
  • 我们可以声明我们的 lambda 表达式可以异步执行 - 只需像普通方法一样添加 async 关键字。
  • 在大多数情况下不能使用 var 语句 - 只有在非常特殊的情况下。

无需多说,如果我们实际上指定了参数类型,我们就可以(像总是那样)更多地使用 var。这是可选的,通常不做(因为类型可以从分配中使用的委托类型中解析),但这是可能的。请看以下示例:

var square = (double x) => x * x;

var stringLengthSquare = (string s) => s.Length * s.Length;

var squareAndOutput = (decimal x, string s) => {
	var sqz = x * x;
	Console.WriteLine("Information by {0}: the square of {1} is {2}.", s, x, sqz);	
};

现在我们知道了大部分基本内容,但关于 lambda 表达式还有一些很酷的东西(这使它们在许多情况下如此有用!)。首先,请看这段代码片段:

var a = 5;
var multiplyWith = x => x * a;
var result1 = multiplyWith(10); //50
a = 10;
var result2 = multiplyWith(10); //100

啊,好吧!所以您可以使用上层作用域中的其他变量。您可能会说这并不特别。但我说这比您想象的要特别得多,因为这些是真正的 **捕获变量**,这使我们的 lambda 表达式成为所谓的 **闭包**。请考虑以下情况:

void DoSomeStuff()
{
	var coeff = 10;
	var compute = (int x) => coeff * x;
	var modifier = () => {
		coeff = 5;
	};

	var result1 = DoMoreStuff(compute);

	ModifyStuff(modifier);
	s
	var result2 = DoMoreStuff(compute);
}

int DoMoreStuff(Action<int> computer)
{
	return computer(5);
}

void ModifyStuff(Action modifier)
{
	modifier();
}

这里发生了什么?首先,我们在该作用域中创建了一个局部变量和两个 lambda。第一个 lambda 应该显示它也可以访问其他局部作用域中的局部变量。这实际上已经相当令人印象深刻了。这意味着我们正在保护一个变量,但仍然可以在另一个方法中访问它。另一个方法是在此类的内部还是在另一个类中定义并不重要。

第二个 lambda 应该演示 lambda 表达式也可以修改上层作用域变量。这意味着我们可以通过仅传递在相应作用域中创建的 lambda 来修改其他方法中的局部变量。因此,我认为 **闭包** 是一个非常强大的概念,它(像并行编程一样)可能导致意外结果(与并行编程中的竞态条件类似,但如果我们按照代码执行,则不会像意外)。为了展示一个有意外结果的场景,我们可以这样做:

var buttons = new Button[10];

for(var i = 0; i < buttons.Length; i++)
{
	var button = new Button();
	button.Text = (i + 1) + ". Button - Click for Index!";
	button.OnClick += (s, e) => { Messagebox.Show(i.ToString()); };
	buttons[i] = button;
}

//What happens if we click ANY button?!

这是我在 JavaScript 课上通常会问学生的一个棘手问题。大约 95% 的学生会立即回答“按钮 0 显示 0,按钮 1 显示 1,...”。但有些学生已经发现了窍门,并且由于讲座的整个部分都围绕闭包和函数,所以很明显存在一个窍门。结果是:每个按钮都显示 10!

名为 i 的局部作用域变量已更改其值,并且必须具有 buttons.Length 的值,因为我们显然已经离开了 for 循环。有一个简单的办法可以解决这个混乱(在这种情况下)。只需对 for 循环的正文执行以下操作:

var button = new Button();
var index = i;
button.Text = (i + 1) + ". Button - Click for Index!";
button.OnClick += (s, e) => { Messagebox.Show(index.ToString()); };
buttons[i] = button;

这解决了所有问题,但这个变量 index 是一个值类型,因此它会复制到更“全局”(上层作用域)变量 i

这次高级介绍的最后一个主题是所谓的表达式树的可能性。这仅通过 lambda 表达式才可能实现,并且负责 ASP.NET MVC 中 HTML 扩展方法中发生的魔力。关键问题是:目标方法如何知道

  1. 我正在传递的变量的名称是什么?
  2. 我使用的正文的结构是什么?
  3. 我正在使用的正文中的类型是什么?

现在,Expression 实际上可以解决这个问题。它允许我们深入挖掘编译器生成的表达式树。此外,我们可以像使用普通的 FuncAction 委托一样执行给定的函数。它还允许我们在运行时(稍后)解释 lambda 表达式。

让我们看一个关于如何使用 Expression 类型对象的示例:

Expression<Func<MyModel, int>> expr = model => model.MyProperty;
var member = expr.Body as MemberExpression;
var propertyName = memberExpression.Member.Name; //only execute if member != null  ...

这是关于此类表达式用法最简单的示例。原理相当直接:通过形成 Expression 类型的对象,编译器会生成有关生成的解析树的元信息。此解析树包含所有相关信息,如参数(名称、类型……)和方法正文。

方法正文包含整个解析树。在那里,我们可以访问运算符、操作数以及完整的语句,(最重要的是)返回名称和类型。返回变量的名称可能为 null。然而,大多数时候人们会对上述表达式感兴趣。这也类似于 ASP.NET MVC 处理 Expression 类型的方式——以获取参数的名称来使用。对程序员的好处显然是他不会拼错属性的名称,因为任何拼写错误都会导致编译错误。

备注 在程序员只对调用属性的名称感兴趣的情况下,有一个更简单(也更优雅)的解决方案。特殊的参数属性 CallerMemberName 可用于获取调用方法或属性的名称。该字段由编译器自动填充。因此,如果我们只是想知道名称(而无需更多类型信息等),我们只需编写如下示例方法(它返回刚刚调用 WhatsMyName() 方法的名称):

string WhatsMyName([CallerMemberName] string callingName = null)
{
    return callingName;
}

lambda 表达式的性能

一个大问题是:lambda 表达式的速度有多快?嗯,首先我们期望它们的性能与常规函数一样快,因为它们也是由编译器生成的。在下一节中,我们将看到为 lambda 表达式生成的 MSIL 与常规函数没有太大区别。

最有趣的讨论之一将是 lambda 表达式和闭包的性能是否会与全局变量方法一样快。真正有趣的区域是局部作用域中可用变量的数量是否会影响性能。

让我们看一下用于执行一些基准测试的代码。总而言之,我们正在查看 4 个不同的基准测试,这些基准测试应该为我们提供足够的证据来看到正常函数和 lambda 表达式之间的差异。

using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace LambdaTests
{
	class StandardBenchmark : Benchmark
    {
		const int LENGTH = 100000;
        static double[] A;
		static double[] B;

        static void Init()
        {
            var r = new Random();
            A = new double[LENGTH];
            B = new double[LENGTH];

            for (var i = 0; i < LENGTH; i++)
            {
                A[i] = r.NextDouble();
                B[i] = r.NextDouble();
            }
        }

        static long LambdaBenchmark()
        {
            Func<double> Perform = () =>
            {
                var sum = 0.0;

                for (var i = 0; i < LENGTH; i++)
                    sum += A[i] * B[i];

                return sum;
            };
            var iterations = new double[100];
            var timing = new Stopwatch();
            timing.Start();

            for (var j = 0; j < iterations.Length; j++)
                iterations[j] = Perform();

            timing.Stop();
            Console.WriteLine("Time for Lambda-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
            return timing.ElapsedMilliseconds;
        }

        static long NormalBenchmark()
        {
            var iterations = new double[100];
            var timing = new Stopwatch();
            timing.Start();

            for (var j = 0; j < iterations.Length; j++)
                iterations[j] = NormalPerform();

            timing.Stop();
            Console.WriteLine("Time for Normal-Benchmark: \t {0}ms", timing.ElapsedMilliseconds);
            return timing.ElapsedMilliseconds;
        }

        static double NormalPerform()
        {
            var sum = 0.0;

            for (var i = 0; i < LENGTH; i++)
                sum += A[i] * B[i];

            return sum;
        }
    }
}

我们可以使用 lambda 表达式更好地编写此代码(然后它将通过回调模式获取任意传递的方法的度量,正如我们将发现的那样)。不这样做是因为不想泄露最终结果。所以我们基本上有三种方法。一种用于 lambda 测试,一种用于普通测试。第三种方法将在普通测试中调用。缺少的第四种方法是我们的 lambda 表达式,它将在第一种方法中创建。计算无关紧要,我们只选择随机数以避免该区域的任何编译器优化。最后,我们只对正常方法和 lambda 表达式之间的差异感兴趣。

如果我们运行这些基准测试,我们将看到 lambda 表达式通常不会比普通方法差。一个惊喜可能是 lambda 表达式实际上比常规函数性能稍好。然而,在存在闭包(即捕获变量)的情况下,这肯定是不正确的。这仅仅意味着我们不应该犹豫定期使用 lambda 表达式。但我们应该仔细考虑使用闭包时可能获得的性能损失。在这种情况下,我们通常会损失一点性能,但这仍然是可以接受的。损失是由于多种原因造成的,正如我们在下一节中将探讨的那样。

我们的基准测试的原始数据如下表所示:

测试 Lambda [毫秒] 正常 [毫秒]
045+-146+-1
144+-146+-2
249+-345+-2
348+-245+-2

相应于这些数据的图表如下所示。我们可以看到,常规函数和 lambda 表达式的性能在同一范围内,即使用 lambda 表达式没有性能损失。

Performance oflambda expression

幕后 - MSIL

使用著名的工具 LINQPad,我们可以毫无负担地仔细查看 MSIL。下面显示了使用 LINQPad 检查 IL 的屏幕截图。

LINQPad in action

我们将查看三个示例。让我们从第一个开始。lambda 表达式如下所示:

Action<string> DoSomethingLambda = (s) =>
{
	Console.WriteLine(s);// + local
};

相应的方法具有以下代码:

void DoSomethingNormal(string s)
{
	Console.WriteLine(s);
}

这两种代码产生了以下两个 MSIL 代码片段:

DoSomethingNormal:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  call        System.Console.WriteLine
IL_0007:  nop         
IL_0008:  ret         
<Main>b__0:
IL_0000:  nop         
IL_0001:  ldarg.0     
IL_0002:  call        System.Console.WriteLine
IL_0007:  nop         
IL_0008:  ret       

这里的主要区别在于方法的命名和用法,而不是声明。声明实际上是相同的。编译器在局部类中创建一个新方法并推断该方法的用法。这没什么新颖之处——我们只是使用 lambda 表达式可以方便而已。从 MSIL 的角度来看,我们在这两种情况下都做了同样的事情;即调用当前对象中的方法。

我们可以将此观察结果放入一个小图表中来说明编译器所做的修改。在下面的图片中,我们看到编译器实际上将 lambda 表达式移动成为一个固定的方法。

Lambda expressions into the MSIL

第二个示例展示了 lambda 表达式的真正魔力。在此示例中,我们使用的是(普通的)带全局变量的方法,或者是一个带有捕获变量的 lambda 表达式。代码如下:

void Main()
{
	int local = 5;

	Action<string> DoSomethingLambda = (s) => {
		Console.WriteLine(s + local);
	};
	
	global = local;
	
	DoSomethingLambda("Test 1");
	DoSomethingNormal("Test 2");
}

int global;

void DoSomethingNormal(string s)
{
	Console.WriteLine(s + global);
}

现在这里没有什么不寻常的。关键问题是:编译器如何解析 lambda 表达式?

IL_0000:  newobj      UserQuery+<>c__DisplayClass1..ctor
IL_0005:  stloc.1     
IL_0006:  nop         
IL_0007:  ldloc.1     
IL_0008:  ldc.i4.5    
IL_0009:  stfld       UserQuery+<>c__DisplayClass1.local
IL_000E:  ldloc.1     
IL_000F:  ldftn       UserQuery+<>c__DisplayClass1.<Main>b__0
IL_0015:  newobj      System.Action<System.String>..ctor
IL_001A:  stloc.0     
IL_001B:  ldarg.0     
IL_001C:  ldloc.1     
IL_001D:  ldfld       UserQuery+<>c__DisplayClass1.local
IL_0022:  stfld       UserQuery.global
IL_0027:  ldloc.0     
IL_0028:  ldstr       "Test 1"
IL_002D:  callvirt    System.Action<System.String>.Invoke
IL_0032:  nop         
IL_0033:  ldarg.0     
IL_0034:  ldstr       "Test 2"
IL_0039:  call        UserQuery.DoSomethingNormal
IL_003E:  nop         

DoSomethingNormal:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery.global
IL_0008:  box         System.Int32
IL_000D:  call        System.String.Concat
IL_0012:  call        System.Console.WriteLine
IL_0017:  nop         
IL_0018:  ret         

<>c__DisplayClass1.<Main>b__0:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  ldarg.0     
IL_0003:  ldfld       UserQuery+<>c__DisplayClass1.local
IL_0008:  box         System.Int32
IL_000D:  call        System.String.Concat
IL_0012:  call        System.Console.WriteLine
IL_0017:  nop         
IL_0018:  ret         

<>c__DisplayClass1..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret      

再次,从它们调用的语句来看,这两个函数是相等的。再次应用了相同的机制,即编译器为函数生成了一个名称并将其放在代码的某个位置。现在的主要区别是,编译器还 **生成了一个类**,其中放置了编译器生成的函数(我们的 lambda 表达式)。该类的实例在创建 lambda 表达式的函数中生成。这个类的目的是什么?它为用作捕获变量的变量提供了全局作用域。通过这种技巧,lambda 表达式就可以访问局部作用域变量(因为从 MSIL 的角度来看,它们只是类实例中的全局变量)。

因此,所有变量都从新生成的类的实例中分配和读取。这解决了变量之间存在引用的问题(只需要对类有一个额外的引用——但仅此而已!)。编译器还足够智能,只将那些用作捕获变量的变量放在类中。因此,我们本来可以期望在使用 lambda 表达式时没有性能问题。然而,需要警告的是,这种行为可能会由于仍然引用的 lambda 表达式而导致内存泄漏。只要函数存在,作用域就仍然存在(这应该之前就很明显了——但现在我们看到了原因!)。

与之前一样,我们也将其放入一些漂亮的小图表中。在这里,我们看到在闭包的情况下,不仅方法被移动,捕获的变量也被移动。所有移动的对象都将放置在编译器生成的类中。因此,我们最终会实例化一个来自未知类的对象。

Closures into the MSIL

移植一些流行的 JavaScript 模式

使用(或了解)JavaScript 的优势之一是其卓越的函数使用方式。在 JavaScript 中,函数只是对象,可以为它们分配属性。在 C# 中,我们不能做 JavaScript 中可以做的所有事情,但我们可以做一些事情。原因之一是 JavaScript 为函数内的变量提供了作用域。因此,必须(通常是匿名)创建函数来本地化变量。在 C# 中,我们通过使用块(即使用大括号)来创建作用域。

当然,在某种程度上,函数在 C# 中也提供了作用域。通过使用 lambda 表达式,我们需要使用大括号(即创建新作用域)来在 lambda 表达式中创建变量。但是,我们也可以在本地创建作用域。

让我们看一下现在可以通过 lambda 表达式在 C# 中实现的一些最有用的 JavaScript 模式。

回调模式

这种模式很老了。实际上,回调模式自 .NET Framework 的第一个版本以来一直被使用,但方式略有不同。现在的情况是,lambda 表达式可以用作闭包,即捕获局部变量,这是一项有趣的功能,它允许我们编写如下代码:

void CreateTextBox()
{
	var tb = new TextBox();
	tb.IsReadOnly = true;
	tb.Text = "Please wait ...";
	DoSomeStuff(() => {
		tb.Text = string.Empty;
		tb.IsReadOnly = false;
	});
}

void DoSomeStuff(Action callback)
{
	// Do some stuff - asynchronous would be helpful ...
	callback();
}

对于来自 JavaScript 的人来说,整个模式并不新鲜。在这里,我们通常倾向于大量使用这种模式,因为它非常有用,而且我们可以将参数用作与 AJAX 相关的事件(oncompletedonsuccess 等)以及其他帮助程序的事件处理程序。如果您使用 LINQ,那么您也在使用回调模式的一部分,因为例如 LINQ 的 where 会在每次迭代中回调您的查询。这只是回调函数有用的一个例子。在 .NET 世界中,事件通常是处理事件的首选方式(顾名思义),这有点像带状的“回调”。原因有两方面:拥有一个特殊的关键字和类型模式(2 个参数:sender 和 arguments,其中 sender 通常是 object 类型(最通用的类型),arguments 继承自 EventArgs),以及有机会使用 +=(add)和 -=(remove)运算符调用多个方法。

返回函数

与普通函数一样,lambda 表达式也可以返回函数指针(委托实例)。这意味着我们可以使用 lambda 表达式来创建并返回一个 lambda 表达式(或仅仅是指向已定义方法的委托实例)。在许多场景中,这种行为可能会有所帮助。首先,让我们看一些示例代码:

Func<string, string> SayMyName(string language)
{
	switch(language.ToLower())
	{
		case "fr":
			return name => {
				return "Je m'appelle " + name + ".";
			};
		case "de":
			return name => {
				return "Mein Name ist " + name + ".";
			};
		default:
			return name => {
				return "My name is " + name + ".";
			};
	}
}

void Main()
{
	var lang = "de";
	//Get language - e.g. by current OS settings
	var smn = SayMyName(lang);
	var name = Console.ReadLine();
	var sentence = smn(name);
	Console.WriteLine(sentence);
}

在这种情况下,代码可以更短。如果请求的语言未找到,我们也可以通过抛出异常来避免默认返回值。但是,为了说明起见,这个示例应该显示这是一种函数工厂。另一种方法是包含一个 Hashtable 或更好的(由于静态类型)Dictionary<K, V> 类型。

static class Translations
{
	static readonly Dictionary<string, Func<string, string>> smnFunctions = new Dictionary<string, Func<string, string>>();

	static Translations()
	{
		smnFunctions.Add("fr", name => "Je m'appelle " + name + ".");
		smnFunctions.Add("de", name => "Mein Name ist " + name + ".");
		smnFunctions.Add("en", name => "My name is " + name + ".");
	}

	public static Func<string, string> GetSayMyName(string language)
	{
		//Check if the language is available has been omitted on purpose
		return smnFunctions[language];
	}
}

//Now it is sufficient to call Translations.GetSayMyName("de") to get the function with the German translation.

尽管这看起来有点过度设计,但这可能是创建此类函数工厂的最佳方式。毕竟,这种方式非常易于扩展,并且可以在许多场景中使用。这种模式与反射结合使用,可以使大多数编程代码更加灵活,更易于维护,并且更健壮地扩展。下图展示了这种模式的工作原理。

A typical function factory

自定义函数

自适应函数模式是 JavaScript 中的一种常见技巧,可以用来提高代码的性能(和可靠性)。该模式的主要思想是,一个被设置为属性的函数(即,我们只在变量上有一个函数指针)可以非常轻松地与其他函数交换。让我们看看这具体意味着什么:

class SomeClass
{
	public Func<int> NextPrime
	{
		get;
		private set;
	}

	int prime;

	public SomeClass
	{
		NextPrime = () => {
			prime = 2;

			NextPrime = () => {
				//Algorithm to determine next - starting at prime
				//Set prime
				return prime;
			};

			return prime;
		}
	}
}

这里做了什么?嗯,在第一种情况下,我们只得到第一个素数,即 2。由于这很简单,我们可以调整算法以默认排除所有偶数。这肯定会加快我们的算法,但我们仍然会得到 2 作为起始素数。我们无需查看是否已经执行了对 NextPrime() 函数的查询,因为一旦返回了简单情况(2),该函数就会自我定义。这样,我们就可以节省资源,并在更有趣的区域(所有大于 2 的数字)中优化我们的算法。

我们已经看到这也可以用于提高性能。让我们考虑以下示例:

Action<int> loopBody = i => {
	if(i == 1000)
		loopBody = /* set to the body for the rest of the operations */;

	/* body for the first 1000 iterations */
};

for(int j = 0; j < 10000000; j++)
	loopBody(j);

这里我们基本上只有两个不同的区域——一个用于前 1000 次迭代,另一个用于剩余的 9999000 次迭代。通常我们需要一个条件来区分两者。在大多数情况下,这将是不必要的开销,这就是为什么我们使用自适应函数来更改它,一旦较小的区域执行完毕。

立即调用的函数表达式

在 JavaScript 中,立即调用的函数表达式(所谓的 IIFE)非常常见。原因是与 C# 不同,大括号不提供作用域来形成新的局部变量。因此,人们会用变量污染全局(主要是 window 对象)。由于许多原因,这是不希望的。

解决方案很简单:虽然大括号不提供作用域,但函数提供。因此,在任何函数内定义的变量都限制在该函数(及其子项)中。由于 JavaScript 用户通常希望这些函数直接执行,因此首先为它们分配名称然后执行它们将浪费变量和语句行。另一个原因是这种执行只需要一次。

在 C# 中,我们也可以轻松编写此类函数。这里我们也获得了一个新作用域,但这不应该是我们的主要关注点,因为我们可以轻松地在任何地方创建新作用域。让我们看一些示例代码:

(() => {
	// Do Something here!
})();

这段代码可以轻松解析。但是,如果我们想对参数做一些事情,那么我们需要指定它们的类型。让我们看一个传递参数给 IIFE 的示例。

((string s, int no) => {
	// Do Something here!
})("Example", 8);

这似乎占用了太多行但却什么都没获得。然而,我们可以将这种模式结合起来使用 async 关键字。让我们看一个例子:

await (async (string s, int no) => {
	// Do Something here async using Tasks!
})("Example", 8);

//Continue here after the task has been finished

现在,可能会有一个或另一个用途作为 async 包装器或其他类似用途。

立即对象初始化

与此密切相关的是立即对象初始化。我将此模式包含在关于 lambda 表达式的文章中的原因是匿名对象非常强大,因为它们可以包含比简单类型更多的内容。其中一项可以包含的内容是 lambda 表达式。这就是为什么有一些东西可以在 lambda 表达式的领域进行讨论。

//Create anonymous object
var person = new {
	Name = "Florian",
	Age = 28,
	Ask = (string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42!");
	}
};

//Execute function
person.Ask("Why are you doing this?");

如果您想运行此模式,那么您很可能会看到一个异常(至少我看到了一个)。神秘的原因是 lambda 表达式不能分配给匿名对象。如果这听起来没有意义,那么我们就处境相同。幸运的是,编译器想告诉我们的一切是:“哥们,我不知道应该为这个 lambda 表达式创建哪种委托!”在这种情况下,很容易帮助编译器。只需使用以下代码代替:

var person = new {
	Name = "Florian",
	Age = 28,
	Ask = (Action<string>)((string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42!");
	})
};

肯定会出现的一个问题是:函数(在这种情况下是 Ask)生活在哪个作用域?答案是它生活在创建匿名对象的类的作用域中,或者如果它使用捕获变量,则生活在其自己的作用域中。因此,编译器仍然会创建一个匿名对象(这涉及到为编译器生成的类布局元信息,实例化一个具有幕后类信息的对象并使用它),但只是将 Ask 属性设置为指向我们创建的 lambda 表达式位置的委托对象。

警告 当您实际上想在直接设置给匿名对象的任何 lambda 表达式中访问匿名对象的任何属性时,您应该避免使用此模式。原因是:C# 编译器要求在实际使用任何对象之前声明它们。在这种情况下,用法肯定是在声明之后;但编译器怎么知道呢?从他的角度来看,访问与声明同时发生,因此变量 person 尚未声明。

有一个方法可以摆脱这种地狱(实际上有更多方法,但在我看来这是最优雅的……)。考虑以下代码:

dynamic person = null;
person = new {
	Name = "Florian",
	Age = 28,
	Ask = (Action<string>)((string question) => {
		Console.WriteLine("The answer to `" + question + "` is certainly 42! My age is " + person.Age + ".");
	})
};

//Execute function
person.Ask("Why are you doing this?");

现在我们在之前声明了它。我们也可以通过声明 person 的类型为 object 来实现相同的功能,但在这种情况下,我们将需要反射(或一些好的包装器)来访问匿名对象的属性。在这种情况下,我们依赖 DLR,这会产生此类最漂亮的包装器。现在代码非常像 JavaScript,我不知道这是好事还是坏事……(这就是为什么对此评论有警告!)。

初始化时分支

此模式实际上与自适应函数密切相关。唯一的区别是,在这种情况下,函数不是自我定义,而是定义其他函数。这显然只有在其他函数不是以经典方式定义,而是通过属性(即成员变量)定义时才可能。

该模式也称为加载时分支,本质上是一种优化模式。创建此模式是为了避免永久使用 switch-caseif-else 等控制结构。因此,在某种程度上,可以说该模式创建了永久连接代码的某些分支的路径。

让我们考虑以下示例:

public Action AutoSave { get; private set; }

public void ReadSettings(Settings settings)
{
	/* Read some settings of the user */

	if(settings.EnableAutoSave)
		AutoSave = () => { /* Perform Auto Save */ };
	else
		AutoSave = () => { }; //Just do nothing!
}

在这里,我们做了两件事。首先,我们有一个读取用户设置的方法(处理任意 Settings 类)。如果发现用户启用了自动保存,那么我们将完整的代码设置为属性。否则,我们只将一个虚拟方法放在该位置。因此,我们可以始终只调用 AutoSave() 属性并调用它——我们总是会执行已设置的内容。无需再次检查设置或其他类似操作。我们也不需要将此特定设置保存在布尔变量中,因为相应的函数是动态设置的。

有人可能会认为这并不是巨大的性能提升,但这只是一个小例子。在非常复杂的代码中,这实际上可以节省一些时间——尤其是在场景变得更复杂并且动态设置的方法将在(巨大的)循环中调用时。

而且(我将其视为主要原因)代码可能更容易维护(如果了解此模式),也更容易阅读。而不是不必要的控制序列,您可以专注于重要的事情:例如调用自动保存例程。

在 JavaScript 中,这种加载时分支模式通常与功能(或浏览器)检测结合使用。更不用说浏览器检测实际上很糟糕,不应该在任何网站上进行,但功能检测确实很有用,并且与此模式结合使用效果最佳。这也是 jQuery 检测 AJAX 请求的正确对象的_方式_(例如)。一旦它在浏览器中检测到 XMLHttpRequest 对象,那么在我们的脚本执行过程中,底层浏览器就不太可能更改,从而导致需要处理 ActiveX 对象。

Lambda 非常有用的场景

有些模式比其他模式更适用。一个非常有用的模式是用于初始化对象部分的自适应函数表达式。让我们考虑以下示例:

我们想创建一个能够执行某种延迟加载的对象。这意味着即使对象已正确实例化,我们也未加载所有必需的资源。避免这种情况的原因之一是巨大的 IO 操作(例如通过 Internet 进行网络传输)来获取所需数据。我们希望确保在开始处理数据时数据尽可能新鲜。现在有一些方法可以做到这一点,最有效的方法肯定是 Entity Framework 解决此延迟加载场景与 LINQ 的方式。这里 IQueryable<T> 只存储查询而没有底层数据。一旦我们需要结果,不仅会执行构造的查询,而且查询会以最有效形式执行,例如在远程数据库服务器上执行 SQL 查询。

在我们的场景中,我们只想区分两种状态。首先,我们查询,然后一切都应该准备好,并且查询将在加载的数据上执行。

class LazyLoad
{
	public LazyLoad()
	{
		Search = query => {
			var source = Database.SearchQuery(query);

			Search = subquery => {
				var filtered = source.Filter(subquery);

				foreach(var result in filtered)
					yield return result;
			};

			foreach(var result in source)
				yield return result;
		};
	}

	public Func<string, IEnumerable<ResultObject>> Search { get; private set; }
}

所以我们基本上需要在这里设置两种不同的方法。第一种方法将从 Database(或该静态类所做的任何事情)中提取数据,而第二种方法将过滤从数据库中提取的数据。一旦我们有了结果,我们基本上只会处理第一次查询的结果集。当然,您也可以想象内置另一个方法来重置此类行为或其他对生产代码有用的方法。

另一个例子是初始化时分支。假设我们有一个名为 Perform() 的方法。该方法将用于调用某些代码。包含此方法Thus object 可以在三种不同的方式下进行初始化(即构造):

  1. 通过传递要调用的函数(直接)。
  2. 通过传递包含要调用的函数的对象(间接)。
  3. 或通过以序列化形式传递第一个案例的信息。

现在我们可以将所有这三种状态(以及给定的完整信息)保存为全局变量。Perform() 方法的调用现在必须查看当前状态(保存在枚举变量中,或由于与 null 比较),然后确定调用方式。最后,可以开始调用。

一个更好的方法是让 Perform() 方法成为一个属性。此属性只能在对象内部设置,并且是委托类型。现在我们可以直接在相应的构造函数中设置属性。因此,我们可以省略全局变量,而不必担心对象是如何构造的。这性能更好,并且具有一旦构造(应如此)就固定的优点。

关于此场景的一些示例代码:

class Example
{
	public Action<object> Perform { get; private set; }

	public Example(Action<object> methodToBeInvoked)
	{
		Perform = methodToBeInvoked;
	}

	//The interface is arbitrary as well
	public Example(IHaveThatFunction mother)
	{
		//The passed object must have the method we are interested in
		Perform = mother.TheCorrespondingFunction;
	}

	public Example(string methodSource)
	{
		//The Compile method is arbitrary and not part of .NET or C#
		Perform = Compile(methodSource);
	}
}

尽管此示例似乎经过精心设计(双关语),但它可以经常应用,尽管大多数情况下仅限于前两种可能的调用。有趣的情景出现在领域特定语言 (DSL)、编译器、日志框架、数据访问层等等主题中。通常有许多方法可以完成任务,但仔细且经过深思熟虑的 lambda 表达式可能是最优雅的解决方案。

考虑到一个肯定会从立即调用的函数表达式中受益的场景是在函数式编程领域。然而,在不深入探讨这个主题的情况下,我将展示另一种在 C# 中使用 IIFE 的方法。我展示的场景也很常见,但肯定不会那么频繁地使用(我相信这确实没关系,它没有在这种场景中使用)。

Func<double, double> myfunc;
var firstValue = (myfunc = (x) => {
	return 2.0 * x * x - 0.5 * x;
})(1);
var secondValue = myfunc(2);
//...

您还可以使用立即调用的函数来防止某些(非静态)方法被调用一次以上。这结合了自适应函数和初始化时分支以及 IIFE。

一些新的 lambda 焦点设计模式

本节将介绍我提出的一些模式,这些模式的核心是 lambda 表达式。我不认为它们都是全新的,但至少我没有看到有人给它们贴上标签。所以我决定尝试提出一些可能好也可能不好的名称(这将是一个品味问题)。至少我选择的名称力求具有描述性。我还将判断此模式是否有用、强大或危险。提前说一句:大多数模式都非常强大,但可能会在您的代码中引入潜在的错误。所以请小心处理!

多态性完全掌握在您手中

lambda 表达式可用于创建类似多态性(override)的东西,而无需使用 abstractvirtual(这并不意味着您不能使用这些关键字)。请看以下代码片段:

class MyBaseClass
{
	public Action SomeAction { get; protected set; }

	public MyBaseClass()
	{
		SomeAction = () => {
			//Do something!
		};
	}
}

现在这里没有什么新东西。我们创建了一个类,它通过属性发布一个函数(一个 lambda 表达式)。这又非常像 JavaScript。有趣的部分是,不仅这个类能够更改属性公开的函数,而且这个类的子类也能做到。看看这段代码片段:

class MyInheritedClass : MyBaseClass
{
	public MyInheritedClass
	{
		SomeAction = () => {
			//Do something different!
		};
	}
}

啊哈!所以我们实际上可以通过滥用 protected 访问修饰符来更改方法(或者更确切地说,设置给属性的方法)。这种方法的缺点是,我们无法直接访问父类的实现。这里我们缺少 base 的功能,因为基类的属性具有相同的值。如果一个人真的需要这样的东西,那么我建议以下*模式*:

class MyBaseClass
{
	public Action SomeAction { get; private set; }

	Stack<Action> previousActions;

	protected void AddSomeAction(Action newMethod)
	{
		previousActions.Push(SomeAction);
		SomeAction = newMethod;
	}

	protected void RemoveSomeAction()
	{
		if(previousActions.Count == 0)
			return;

		SomeAction = previousActions.Pop();
	}

	public MyBaseClass()
	{
		previousActions = new Stack<Action>();

		SomeAction = () => {
			//Do something!
		};
	}
}

在这种情况下,子类必须通过 AddSomeAction() 方法来覆盖当前设置的方法。这个方法将只是将当前设置的方法推送到先前方法的堆栈中,使我们能够恢复任何先前状态。

我为这种模式起的名字是*Lambda 属性多态模式*(或简称 LP3)。它基本上描述了将任何函数封装在属性中的可能性,然后该属性可以由基类的派生类设置。堆栈只是对该模式的补充,它不改变该模式使用属性作为交互点的目标。

为什么使用这种模式?嗯,有几个原因。首先:因为我们可以!但是,这种模式在你开始使用各种不同的属性时确实会派上用场。“多态性”一词突然具有了全新的含义。但这将是另一种模式……现在我只想指出,这种模式在现实中可以做一些以前认为不可能的事情。

例如:您想(不推荐,但这是*您的*问题的最优雅解决方案)覆盖一个静态方法。好吧,静态方法不支持继承。原因很简单:继承只适用于实例,而静态成员不绑定到实例。它们对所有实例都是相同的。这也意味着警告。以下模式可能不会产生您想要的结果,因此只有在您知道自己在做什么时才使用它!

以下是一些示例代码:

void Main()
{
	var mother = HotDaughter.Activator().Message;
	//mother = "I am the mother"
	var create = new HotDaughter();
	var daughter = HotDaughter.Activator().Message;
	//daughter = "I am the daughter"
}

class CoolMother
{
	public static Func<CoolMother> Activator { get; protected set; }
	
	//We are only doing this to avoid NULL references!
	static CoolMother()
	{
		Activator = () => new CoolMother();
	}

	public CoolMother()
	{
		//Message of every mother
		Message = "I am the mother";
	}
	
	public string Message { get; protected set; }
}

class HotDaughter : CoolMother
{
	public HotDaughter()
	{
		//Once this constructor has been "touched" we set the Activator ...
		Activator = () => new HotDaughter();
		//Message of every daughter
		Message = "I am the daughter";
	}
}

这只是一个非常简单但希望不完全误导人的例子。在这种模式下,事情会变得非常复杂,因此我总是想避免它。尽管如此,这是可能的(并且也可能以这种方式构造所有这些静态属性和函数,以至于您仍然总是得到您感兴趣的那个)。静态多态性(是的,这是可能的!)的一个好解决方案并不容易,需要一些编码,并且只有在它确实解决了您的问题而没有带来任何额外的麻烦时才应该这样做。

只需请求一个函数

我已经介绍了一种模式(但没有指定名称),那就是_函数字典模式_。此模式的基本组成部分是:一个哈希表或字典,其中包含某种类型的键(通常是字符串,但取决于情况,它也可以是一个更专业的对象,例如在 YAMP 中我使用的是正则表达式),其值是某种类型的函数。该模式还指定了一种构建字典的特定样式。这实际上是模式所必需的,否则函数中的简单 switch-case 可以完成同样的工作。考虑这个例子:

public Action GetFinalizer(string input)
{
	switch
	{
		case "random":
			return () => { /* ... */ };
		case "dynamic":
			return () => { /* ... */ };
		default:
			return () => { /* ... */ };
	}
}

我们需要一个字典在哪里?无处。当然,我们也可以这样做:

Dictionary<string, Action> finalizers;

public void BuildFinalizers()
{
	finalizers = new Dictionary<string, Action>();
	finalizers.Add("random", () => { /* ... */ });
	finalizers.Add("dynamic", () => { /* ... */ });
} 

public Action GetFinalizer(string input)
{
	if(finalizers.ContainsKey(input))
		return finalizers[input];

	return () => { /* ... */ };
}

但是等等——现在这种模式没有任何优势。实际上,这种模式效率低得多,并且需要额外的行。但是我们可以使用反射来“自动化”字典的构建过程。这样,我们可能不像使用 switch-case 语句那样高效,但我们的代码是健壮的,并且需要更少的维护。这实际上可能很方便,如果您考虑一个非常大的代码,其中需要手动将每个函数添加到 switch-case 块中。

让我们来看一个可能的实现。我通常喜欢添加某种约定来选择字典键的名称。然而,您也可以考虑选择所选类的属性值,或者满足特定签名或要求的_方法_的名称。在此示例中,我们遵循约定。

static Dictionary<string, Action> finalizers;

//The method should be called by a static constructor or something similar
//The only requirement is that we built
public static void BuildFinalizers()
{
	finalizers = new Dictionary<string, Action>();

	//Get all types of the current (= where the code is contained) assembly
	var types = Assembly.GetExecutingAssembly().GetTypes();

	foreach(var type in types)
	{
		//We check if the class is of a certain type
		if(type.IsSubclassOf(typeof(MyMotherClass)))
		{
			//Get the constructor
			var m = type.GetConstructor(Type.EmptyTypes);

			//If there is an empty constructor invoke it
			if(m != null)
			{
				var instance = m.Invoke(null) as MyMotherClass;
				//Apply the convention to get the name - in this case just we pretend it is as simple as
				var name = type.Name.Remove("Mother");
				//Name could be different, but let's just pretend the method is named MyMethod
				var method = instance.MyMethod;

				finalizers.Add(name, method);
			}
		}
	}
} 

public Action GetFinalizer(string input)
{
	if(finalizers.ContainsKey(input))
		return finalizers[input];

	return () => { /* ... */ };
}

现在这看起来好一点了!实际上,这种模式为我节省了很多工作。但是,这种模式最好的地方在于:它使您能够编写如此好的插件,并实现跨各种库的功能。为什么会这样?您可以使用此代码扫描 NEW(未知)库以查找特定模式并将其包含在您的代码中。来自其他库的函数将无缝集成到您的代码中。您只需要做以下工作:

//The start is the same

internal static void BuildInitialFinalizers()
{
	finalizers = new Dictionary<string, Action>();
	LoadPlugin(Assembly.GetExecutingAssembly());
}

public static void LoadPlugin(Assembly assembly)
{
	//This line has changed
	var types = assembly.GetTypes();

	//The rest is identical! Perfectly refactored and obtained a new useful method
	foreach(var type in types)
	{
		if(type.IsSubclassOf(typeof(MyMotherClass)))
		{
			var m = type.GetConstructor(Type.EmptyTypes);

			if(m != null)
			{
				var instance = m.Invoke(null) as MyMotherClass;
				var name = type.Name.Remove("Mother");
				var method = instance.MyMethod;
				finalizers.Add(name, method);
			}
		}
	}
} 

//The call is the same

现在(在我们的应用程序中),我们只需要一个指定插件的点和一个处理这些插件的函数。最终,它将归结为读取路径,尝试从给定路径创建程序集对象,并使用获得的 Assembly 实例调用 LoadPlugin() 方法。这只是该模式的一种应用。我经常使用这种模式,我也尝试在 JavaScript 中使用它(没有内置反射,但如果您查看我的 Mario5 文章,您可能会看到我在那里做了什么来创建类似的东西)。

为您的属性赋能

属性是 C# 语言最伟大的特性之一。许多在 C/C++ 中不容易做到的事情,只需使用属性,只需几行 C# 代码即可完成。此模式将属性的功能与 lambda 表达式结合起来。最终,这种_函数属性模式_将进一步提高属性的可能性,从而提高其生产力。

lambda 表达式与属性结合使用可能非常有帮助,因为我们不必为特定情况编写特定的类。我尝试用一个简单的例子来解释我的意思。让我们考虑一个具有以下属性的类:

class MyClass
{
	public bool MyProperty
	{
		get;
		set;
	}
}

现在我们想对该类的任何实例执行以下操作:我们希望能够通过某种领域特定语言或脚本语言修改该属性。因此,我们希望能够修改属性值而不必显式编写代码。当然,我们将需要一些反射。我们还将需要一些属性,因为我们需要一种方法来指定此属性的值是否可以由用户更改。

class MyClass
{
	[NumberToBooleanConverter]
	[StringToBooleanConverter]
	public bool MyProperty
	{
		get;
		set;
	}
}

因此,我们在那里指定了两种转换器。一种足以标记该属性可由任何用户修改。我们使用两个这样的属性来给用户更多可能性。在这种情况下,用户实际上可以使用字符串设置值(将被转换为布尔类型)以及数字(如 0 或 1)。

转换器是如何实现的?让我们来看一个 StringToBooleanConverterAttribute 类的示例实现。

public class StringToBooleanConverterAttribute : ValueConverterAttribute
{
	public StringToBooleanConverterAttribute()
		: base(typeof(string), v => {
			var str = (v as string ?? string.Empty).ToLower();

			if (str == "on")
				return true;
			else if (str == "off")
				return false;

			throw new Exception("The only valid input arguments are [ on, off ]. You entered " + str + ".");
		})
	{
		/* Nothing here on purpose */
	}
}

public abstract class ValueConverterAttribute : Attribute
{
	public ValueConverterAttribute(Type expected, Func>object, object> converter)
	{
		Converter = converter;
		Expected = expected;
	}

	public ValueConverterAttribute(Type expected)
	{
		Expected = expected;
	}

	public Func<Value, object> Converter { get; set; }

	public object Convert(object argument)
	{
		return Converter.Invoke(argument);
	}

	public bool CanConvertFrom(object argument)
	{
		return Expected.IsInstanceOfType(argument);
	}

	public Type Expected
	{
		get;
		set;
	}

	public string Type
	{
		get { return Expected.Name; }
	}
}

此模式有什么优点?嗯,如果属性可以接受非常量表达式作为参数(如委托,即 lambda 表达式是可能的),那么我们肯定会从该模式中获益更多。这样,我们只需用 lambda 表达式替换抽象方法,这些 lambda 表达式将被传递到基类构造函数。

有人可能会争辩说,这与 abstract 函数(需要实现)没有什么不同,但有趣的部分不是像这样的函数那样使用它,而是作为可以从外部 set 的属性。这可以在非常动态的代码中使用,以覆盖某些转换器,当它们已经被实例化时。

避免循环引用

本节的功劳归 Charles Young,他在他的博客[^]上对此进行了发布。

循环引用在使用 C# 时不是一个大问题。实际上,出现有问题的循环引用只有一种方式,那就是在 struct 中。由于类是引用类型,循环引用不会造成任何伤害。我们有一个从源到目标对象的指针,以及另一个从目标到源对象的指针。这里一点问题都没有!

但在 struct 的情况下,我们不是放置指针(或留在 C# 词汇中的引用),而是创建新对象(在堆栈上)。因此,在这种情况下,源对象将包含目标对象,目标对象包含源对象的副本(不是原始源对象),它包含目标对象的副本(不是原始目标对象),依此类推。

在大多数情况下,编译器会检测到这种循环引用并抛出编译错误。这是一个非常好的特性。让我们看一个创建此类错误的示例实现:

struct FirstStruct
{
    public SecondStruct Target;
}

struct SecondStruct
{
    public FirstStruct Source;
}

在此代码中,结构用作变量。这里我们有一个与类的一个大区别:即使我们不实例化变量,默认实例也已经创建。

然而,编程是复杂的,编译器并非无所不能。所以有一些方法可以欺骗编译器(我们甚至可能在不知情的情况下使用这样的技巧!)。如果我们欺骗编译器,那么我们将得到一个运行时错误,告诉我们对象无法创建。欺骗编译器的一种方法是使用自动属性:

struct FirstStruct
{
    public SecondStruct Target { get; set; }
}

struct SecondStruct
{
    public FirstStruct Source { get; set; }
}

这并没有解决问题,它只是将问题从编译错误转移到运行时错误。一个我们可能立即想到的解决方案(除了只是使用类——但这可能不是我们真正想要的)是使用可空结构或 struct?

struct FirstStruct
{
    public SecondStruct? Target { get; set; }
}

struct SecondStruct
{
    public FirstStruct Source { get; set; }
}

现在最大的问题是,这些可空结构也是结构。它们继承自 System.Nullable<T>,它确实是一个 struct,因此这些对象也放在堆栈上。哎哟!

所以现在 lambda 表达式来拯救了。考虑以下代码:

struct FirstStruct
{
    readonly Func<SecondStruct> f;

    public FirstStruct(SecondStruct target)
    {
        f = () => target;
    }

    public SecondStruct Target
    { 
        get
        {
            return f();
        }
    }
}

struct SecondStruct
{
    public FirstStruct Source { get; set; }
}

这里做了什么?使用了指向函数的引用。该函数为我们提供了结构。该结构作为全局变量包含在一个由编译器生成的类中(我们上面已经看到了——编译器就是这样处理闭包的)。由于结构总是包含一个标准构造函数,它会使函数 f 未被引用,因此我们生成了另一个带有目标结构作为参数的构造函数。

最后,我们创建了一个闭包,它将为我们返回捕获的结构实例。重要的是要注意,还有其他可能性。在任何情况下,技巧在于使用引用类型作为值类型的容器,或者循环引用会杀死我们。lambda 表达式是处理此类情况的一种方式,在某些情况下,它们是最具表现力和最直接的处理方式。

递归 lambda 表达式

本节摘自 Mads Torgersen 的博客文章[^]。Mads Torgersen 是 Microsoft C# 产品组的成员之一。

由于 lambda 表达式是匿名方法,我们无法直接调用它们,因此递归似乎不可能。让我们以阶乘函数为例。用普通方法表示,我们可以写出如下代码:

int factorial(int n)
{
	if(n == 0)
		return 1;
	else
		return n * factorial(n - 1);
}

这段小程序比简单的循环效率低,但它将向我们展示如何用 lambda 表达式来表示。让我们首先将其重写为 lambda 表达式:

n => n == 0 ? 1 : n * factorial(n - 1)

然而,问题仍然存在——factorial 是什么?它仍然是我们普通的(命名)函数吗?如果是这样,那么整个练习就没有意义了。如果不是,那么 factorial 显然被定义为 lambda 表达式。无需多说,这行不通,因为我们将变量用作闭包的参数,但变量尚未定义。

我们也可以写得更复杂一些,如下所示:

factorial => (n => n == 0 ? 1 : n * factorial(n - 1))

现在我们创建了两个 lambda 表达式。第一个将接收一个 lambda 表达式作为参数,第二个将递归调用第一个。乍一看,这似乎毫无意义,但对此会有一些理由。

让我们现在显式地表达给定的 lambda 表达式,以便我们知道它实际上意味着什么:

Func<Func<int, int>, Func<int, int>> F = 
	factorial => (n => n == 0 ? 1 : n * factorial(n - 1));

所以变量 F 包含一个将一个输入函数映射到输出函数的_方法_。因此,在语句_y=F(x)_中,_x_和_y_都将是函数。现在的任务是找到一个合适的_x_,使得_x_和_y_相同。这样的_x_可以称为 factorial。这被称为不动点,通常很难找到。然而,我们寻找的不是数值,而是函数。

乍一看似乎极具挑战性,但实际上并不那么难,因为很多工作已经完成了。我们现在只需要两到三个要素:

  1. 描述我们的函数的委托(通用的 Func 类型在这里不起作用)。
  2. 一个通过递归应用表达式来搜索不动点的函数。
  3. 定义一个所谓的 Y 组合子,它将两个函数连接成另一个函数。

将所有内容放在一起,我们将得到以下通用类:

public class RF<T>
{
	delegate T SelfApplicable<T>(SelfApplicable<T> self);

	public static SelfApplicable<Func<Func<Func<int, int>,Func<int, int>>, Func<int, int>> Y = y => (f => (x => f(y(y)(f))(x)));

	public static Func<Func<Func<int, int>, Func<int, int>>, Func<int, int>> Fix = Y(Y);
}

我们也可以将所有内容放入一个命名方法中。这个更容易,但需要命名:

Func<T, T> Fix<T>(Func<Func>T, T>, Func<T, T>> F)
{
	return t => F(Fix(F))(t);
}

现在这解决了我们的问题,我们可以通过使用 RF<T>.Fix 轻松生成递归 lambda 表达式,如下所示:

//Higher-order function to describe the factorial
Func<Func<int, int>,Func<int, int>> F = factorial => n => n == 0 ? 1 : n * factorial(n - 1);
//The factorial function itself
Func<int, int> f = RF.Fix(F);
//usage:
Console.WriteLine(f(4)); //4 * 3 * 2 * 1 = 24.

现在,通过我们的递归辅助类或递归辅助方法,可以使用递归 lambda 表达式。

使用代码

我编译了一些样本集,并列出了基准测试。我将所有内容都收集在一个控制台项目中——所以它基本上应该可以在支持 C# 3 及以下版本的任何平台(我指的是 Mono、.NET、Silverlight……)上运行。我的建议是,人们应该首先尝试使用 LINQPad。这里的绝大多数示例代码都可以直接在 LINQPad 中编译。有些示例非常抽象,如果没有创建合适的场景,则无法编译。

尽管如此,我希望代码能够演示本文中提到的一些特性。我也希望 lambda 表达式能像接口现在被使用一样被广泛使用。回想几年前,接口似乎完全过度设计,用途不大。如今,每个人都在谈论接口——“实现在哪里?”有人可能会问…… lambda 表达式如此有用,以至于最棒的扩展使它们能够按预期工作。你能想象在没有 LINQ、ASP.NET MVC、Reactive Extensions、Tasks……(你最喜欢的框架?)的情况下,像你现在喜欢的那样用 C# 编程吗?

兴趣点

当我第一次看到 lambda 表达式的语法时,我感到有点害怕。语法看起来很复杂,而且不太有用。现在我的看法完全改变了。我认为语法实际上非常棒(尤其是与 C++11 中存在的语法相比,但这只是品味问题)。我还认为 lambda 表达式是整个 C# 语言的关键部分。

如果没有这个语言特性,我怀疑 C# 是否会创造出像 ASP.NET MVC、许多 MVVM 框架……以及更不用说 LINQ 那样好的可能性!当然,所有这些技术也都可能实现,但不是以如此清晰、易用的方式。

最后写点个人感受。我积极为 CodeProject 贡献已经一年了!这是我的第 16 篇文章(这很棒,因为我喜欢 2 的整数幂),很高兴有这么多人发现我的一些文章很有帮助。我希望大家都能欣赏 2013 年即将到来的内容,届时我可能会专注于在 C# 和 JavaScript 之间建立桥梁(我把想象的含义留给你们——不:它不是那些已经见过的 C# 到 JavaScript 或 MSIL 到 JavaScript 的转译器之一)。

话虽如此:**祝大家圣诞快乐,新年快乐 2013!**

历史

  • v1.0.0 | 初始发布 | 2012 年 12 月 12 日
  • v1.1.0 | 添加了 LP3 模式 | 2012 年 12 月 14 日
  • v1.2.0 | 添加了 FDP 模式 | 2012 年 12 月 19 日
  • v1.3.0 | 添加了 FA 模式 | 2013 年 1 月 1 日
  • v1.4.0 | 添加了关于避免循环引用和递归 lambda 表达式的章节 | 2013 年 1 月 28 日
  • v1.4.1 | 修正了一些拼写错误 | 2013 年 1 月 29 日
© . All rights reserved.