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

您需要了解的关于 Lambda 表达式的一切

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (62投票s)

2015年1月15日

CPOL

9分钟阅读

viewsIcon

58559

本文将带您了解彻底理解 lambda 表达式所需的所有步骤。

引言

Lambda 表达式只是编写匿名方法的另一种方式。要理解 lambda 表达式,首先应该理解委托和匿名方法的概念。如果您精通这些概念,那么您可以直接跳到 lambda 表达式部分。

我们将按以下顺序阐述概念

  • 委托(Delegates)
  • 匿名方法
  • Lambda 表达式
  • Func<> 委托
  • 委托实战
  • Lambda 表达式实战
  • Lambda 表达式 - 真实示例
  • 闭包

委托(Delegates)

委托是对方法的类型安全引用。换句话说,您可以在 delegate 对象中保存方法的地址。

例如,如果您有一个方法 Addition()

public static int Addition(int no1, int no2)
{
      return no1 + no2;
}

此方法的 Delegate 声明将如下所示

public delegate int MyDelegate(int no1,int no2);

请注意,delegate 声明必须与方法的签名匹配。(因为 delegate 是对方法的类型安全引用)。

现在您可以创建上述委托类型的对象。在创建 delegate 对象时,我们需要在构造函数中指定要引用的方法的名称。

MyDelegate delObj = new MyDelegate(Addition); 

(请注意,这类似于类和对象。我们首先编写类,然后可以创建该类的多个对象。同样,我们首先声明 delegate 类型,然后从中创建 delegate 对象。在委托的情况下,这有点令人困惑,因为 delegate 声明和 delegate 对象都称为 delegate。)

一旦您在 delObj 中有了“Addition”方法的引用,您可以像这样调用

int r = delObj.Invoke(2,5);

int r = delObj(2,5);

在第二种情况下,编译器会将其转换为 delObj.Invoke(2,5);

完整代码

    class Program
    {
        public delegate int MyDelegate(int no1,int no2);
        
        static void Main(string[] args)
        {
            MyDelegate delObj = new MyDelegate(Addition);
            int r = delObj(2,5);
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }

        public static int Addition(int no1, int no2)
        {
            return no1 + no2;
        }
    }

匿名方法

匿名方法是没有名称的方法。我们可以编写没有名称的小方法块并将其地址分配给 delegate 对象,然后使用此 delegate 对象来调用它。

上面的示例可以使用匿名方法完成,如下所示

    class Program
    {
        public delegate int MyDelegate(int no1,int no2);
        
        static void Main(string[] args)
        {
            MyDelegate delObj = delegate(int no1, int no2)
                                {
                                    return no1 + no2;
                                };

            int r = delObj(2,5); 
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }    
    }

在这里,我们使用 delegate 关键字来创建匿名方法。此方法与第一个示例中的 Addition() 方法相同,但它没有任何名称。在创建此方法时,我们将其地址分配给 delegate 对象 delObj。因此,我们可以使用 delObj delegate 对象调用此匿名方法(调用与上一个示例相同)。

Lambda 表达式

Lambda 表达式只是编写匿名方法的另一种方式。上面的代码可以使用 lambda 表达式重写为

    class Program
    {
        public delegate int MyDelegate(int no1,int no2);
        
        static void Main(string[] args)
        {
            MyDelegate delObj = (no1, no2) => no1 + no2;
            int r = delObj(2,5); 
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }    
    }

注意事项

  • Lambda 表达式包含参数列表和方法体,与匿名方法一样。=> 运算符之前的是括号中的参数列表,=> 运算符之后的是方法体。
  • 无需使用 return 关键字,编译器会为您添加。
  • 无需 delegate 关键字
  • 参数不需要数据类型。参数将从 delegate 声明 (MyDelegate) 获取类型信息。

如果 lambda 表达式只有一个参数,则甚至不需要参数周围的括号。

例如,以下是一个计算给定数字平方的 lambda 表达式。

    class Program
    {
        public delegate int MyDelegate(int no);
        
        static void Main(string[] args)
        {
            MyDelegate delObj = no => no * no;
            int r = delObj(4);           
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }    
    } 

Func<> 委托

Func<> 是框架中预定义的泛型 delegate,我们可以用它来保存方法的引用。它有多个重载,我们可以根据我们要引用的方法的签名使用其中一个。

在此示例中,我们将使用 Func<T, TResult> 重载。它可以封装带有一个参数和返回值的方法。

T:参数类型。
TResult:返回类型。

同样的例子可以使用 Func<T, TResult> 编写为

    class Program
    {      
        static void Main(string[] args)
        {
            Func<int, int> delObj = no => no * no;
            int r = delObj(4);          
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }    
    }

在上面的代码中,我们不需要显式声明 delegate(像 MyDelegate)。相反,我们使用 Func<T, TReturn> delegate。此 delegate 简化了代码并消除了声明 delegate 的需要。

委托实战

当我们想将方法作为参数传递给另一个方法时,使用 Delegate。例如,将方法 fun1() 作为参数传递给方法 fun2()

为了更好地理解它,让我们回顾一下我们的第一个例子。

我们有一个 Addition 方法

   public static int Addition(int no1, int no2)
   {
       return no1 + no2;
   }

委托声明

   public delegate int MyDelegate(int no1, int no2);

Main 方法:这次,我们不直接从 Main 调用 Addition() 方法。仔细检查代码并阅读“注意事项”。

        static void Main(string[] args)
        {
            MyDelegate delObj = new MyDelegate(Addition);
            int r = Calculate(delObj);
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }
        public static int Calculate(MyDelegate delObj)
        {
            int r = delObj(2, 5);
            return r;
        }

注意事项

  • 首先,我们将 Addition 方法的引用/地址保存在 delObj
  • Main,我们调用 Calculate() 方法。但在调用它时,我们将 Addition() 方法的地址(即 delObj)作为参数传递给 Calculate() 方法。(Addition(): 回调方法)
  • 现在在 Calculate 方法中,我们调用 delObj 委托。由于 delObj 持有 Addition() 方法的地址,它将调用相同的方法。
  • 您可以将 Main 中的代码替换为以下代码。(无需创建 delegate 对象。您可以直接在 Calculate() 方法中指定方法名称。)
     static void Main(string[] args)
     {            
        int r = Calculate(Addition);
        Console.WriteLine("Result = " + r);

        Console.ReadKey();
     }

您心中一定有一个大问题,如果我们要从 Calculate() 方法调用 Addition 方法,为什么不这样写呢?

    class Program
    {
        public delegate int MyDelegate(int no1, int no2);

        static void Main(string[] args)
        {
            int r = Calculate();
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }

        public static int Calculate()
        {
            int r = Addition(2,5);
            return r;
        }

        public static int Addition(int no1, int no2)
        {
            return no1 + no2;
        }
    }

但这样,您将在 Calculate() 方法中硬编码对 Addition() 的调用。如果我们想要进行减法并且这次我们想要调用 Subtraction() 方法而不是 Addition() 怎么办。为了实现这一点,我们必须使用 delegate 方法。

以下示例将使其非常清楚

    class Program
    {
        public delegate int MyDelegate(int no1, int no2);

        static void Main(string[] args)
        {
            int r = Calculate(Addition);
            Console.WriteLine("Result = " + r);

            r = Calculate(Substraction);
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }

        public static int Calculate(MyDelegate delObj)
        {
            int r = delObj(2, 5);
            return r;
        }

        public static int Addition(int no1, int no2)
        {
            return no1 + no2;
        }

        public static int Substraction(int no1, int no2)
        {
            return no1 - no2;
        }
    }
  • 这里,我们添加了一个方法 Substraction()
  • Calculate 方法不硬编码对 Addition() 方法的调用。它使用 delegate 调用。
  • 我们可以将任何方法的地址传递给 Calculate()。如第 3 行所示,我们将其传递了 Subtraction() 方法的地址。
  • 这些方法通常被称为回调方法。

Lambda 表达式实战

在上述场景中,每当我们要为 Calculate() 方法添加新功能时,我们都需要先编写一个新方法,然后将它的引用传递给 Calculate() 方法。

例如,为了支持乘法,我们首先必须编写 Multiplication() 方法,然后将其引用传递给 Calculate() 方法。如果您的方法足够复杂,包含多行代码,则此方法很好。但对于像 Addition()Substaction() 这样的一行方法,我们能否不创建单独的方法,而是将其作为匿名方法编写?而且我们知道,lambda 表达式提供了一种优雅的方式来编写匿名方法。因此,在上面的代码中,我们可以使用 lambda 表达式重构如下

    class Program
    {
        public delegate int MyDelegate(int no1, int no2);

        static void Main(string[] args)
        {
            int r = Calculate((no1,no2) => no1 + no2);
            Console.WriteLine("Result = " + r);

            r = Calculate((no1, no2) => no1 - no2);
            Console.WriteLine("Result = " + r);

            Console.ReadKey();
        }

        public static int Calculate(MyDelegate delObj)
        {
            int r = delObj(2, 5);
            return r;
        }
    }

注意事项

  • 在这里,在调用 calculate 方法时,我们直接以 lambda 表达式语法编写 Addition()Substraction() 的主体。
  • 匿名方法将在内存中创建,其地址将传递给 Calculate() 方法。Calculate() 方法将把该地址保存在 delObj delegate 对象中。并将使用它调用相同的方法。
  • 现在,如果您想支持乘法功能,一行代码就足够了
    r = Calculate((no1, no2) => no1 * no2);

Lambda 表达式 - 真实示例

上面使用的例子在实际场景中可能不实用,但我只是用它来以最简单的形式解释概念。在实际生活中,lambda 表达式在编写 LINQ 查询时被广泛使用。

以下示例将为您提供一个 lambda 表达式实际用法的简短测试

public class Employee
    {
        public int EmpId { get; set; }
        public string EmpName { get; set; }
        public double Salary { get; set; }
        public string City { get; set; }

        public override string ToString()
        {
            return String.Format("{0} {1} {2} {3}",EmpId,EmpName,Salary,City);
        }
    }
    
    public class Program
    {
        static void Main(string[] args)
        {
            List<Employee> employees = new List<Employee>();
            employees.Add(new Employee 
            { EmpId = 1, EmpName = "Mikyway", Salary = 10000, City = "Mumbai" });
            employees.Add(new Employee 
            { EmpId = 2, EmpName = "Andromeda", Salary = 20000, City = "Newyork" });
            employees.Add(new Employee 
            { EmpId = 3, EmpName = "Sculptor", Salary = 30000, City = "Mumbai" });

            foreach(Employee emp in FilterByCity(employees, "Mumbai"))
            {
                Console.WriteLine(emp.ToString());
            }

            Console.ReadKey();
        }

        public static IEnumerable<Employee> FilterByCity
            (IEnumerable<Employee> employees, string filterStr)
        {
            foreach(Employee emp in employees)
            {
                if (emp.City == filterStr)
                    yield return emp;
            }
        }
    }

在上面的代码中:

  • 我们有一个带有几个属性的 Employee
  • 员工列表是 Employee 的集合
  • Filter 是一个根据 City 筛选员工列表的方法

如果我们想根据任何其他字段(即 NameSalary)筛选列表怎么办?那么,我们不应该编写特定的 FilterByCity() 方法,而应该编写通用的 Filter() 方法,它应该能够使用任何字段进行筛选。

如果我们能将硬编码条件 if (emp.City == filterStr)FiterByCity() 方法中取出,这是可能的。我们可以使用 lambda 表达式如下所示

使用 lambda 表达式的通用 Filter()

    public delegate bool EmpDelegate(Employee emp);

    public class Program
    {
        static void Main(string[] args)
        {
            ...

            Console.WriteLine("Filter by City");
            foreach(Employee emp in Filter(employees, emp => emp.City == "Mumbai"))
            {
                Console.WriteLine(emp.ToString());
            }

            Console.WriteLine("\nFilter by Salary");
            foreach (Employee emp in Filter(employees, emp => emp.Salary >= 20000))
            {
                Console.WriteLine(emp.ToString());
            }

            Console.ReadKey();
        }

        public static IEnumerable<Employee> Filter(IEnumerable<Employee> employees, EmpDelegate delObj)
        {
            foreach(Employee emp in employees)
            {
                if (delObj(emp) == true)
                    yield return emp;
            }
        }
    }

同样的事情可以使用内置的 Func<> 委托来完成。在这种情况下,不需要显式 delegate 声明 (EmpDelegate)。

	public static IEnumerable<Employee> Filter(IEnumerable<Employee> employees, Func<Employee,bool> delObj)
	{
		foreach(Employee emp in employees)
		{
			if (delObj(emp) == true)
				yield return emp;
		}
	}

完整代码

public class Employee
    {
        public int EmpId { get; set; }
        public string EmpName { get; set; }
        public double Salary { get; set; }
        public string City { get; set; }

        public override string ToString()
        {
            return String.Format("{0} {1} {2} {3}",EmpId,EmpName,Salary,City);
        }
    }
    
    public class Program
    {
        static void Main(string[] args)
        {
            List<Employee> employees = new List<Employee>();
            employees.Add(new Employee 
            { EmpId = 1, EmpName = "Mikyway", Salary = 10000, City = "Mumbai" });
            employees.Add(new Employee 
            { EmpId = 2, EmpName = "Andromeda", Salary = 20000, City = "Newyork" });
            employees.Add(new Employee 
            { EmpId = 3, EmpName = "Sculptor", Salary = 30000, City = "Mumbai" });

            Console.WriteLine("Filter by City");
            foreach(Employee emp in Filter(employees, emp => emp.City == "Mumbai"))
            {
                Console.WriteLine(emp.ToString());
            }

            Console.WriteLine("\nFilter by Salary");
            foreach (Employee emp in Filter(employees, emp => emp.Salary >= 20000))
            {
                Console.WriteLine(emp.ToString());
            }

            Console.ReadKey();
        }

        public static IEnumerable<Employee> 
        Filter(IEnumerable<Employee> employees, Func<Employee,bool> delObj)
        {
            foreach(Employee emp in employees)
            {
                if (delObj(emp) == true)
                    yield return emp;
            }
        }
    }  

我希望这个例子能帮助您更好地理解 LINQ 查询。为了使这个例子尽可能简单,并只关注 lambda 表达式,避免了一些行业标准,如包含、扩展方法等。

闭包

我将本节包含在内是为了回应评论中的建议。我希望这会使其更有用。

首先检查以下代码

	static void Main(string[] args)
	{
		int i = 5;
		Func<int, int> add = x => x + i;

		Console.WriteLine(add(10));   
		Console.ReadKey();
	}

这里我们有一个带一个参数的 lambda 表达式“add”。我们将 10 传递给参数 x。变量 i 已经初始化为 5。如果你运行这段代码,输出将是 15。这很明显。

好的,在阅读完代码块后,如果你有灾难的直觉!!!......那么恭喜你!你很好地理解了 lambda 表达式。如果上面的代码对你来说很明显,你找不出任何奇怪的地方,那么很抱歉地说,你需要再读一遍文章......

好的,别担心,只需阅读以下几点,然后阅读上面的代码块

 

  1. Lambda 表达式是匿名方法,即编译器将为 lambda 表达式创建新方法(也在单独的线程上)。
  2. 在上面的例子中,i 是函数 Main() 的局部变量。如果你还记得你最初的编程课程,我们学到“函数局部变量的作用域和生命周期只在函数内部”,即我们不能在函数外部访问函数的局部变量。
  3. 在上面的代码中,lambda 表达式正在访问 Main() 的变量 i(没有任何错误!!!)。

所以我们可以说,使用 lambda 表达式,我们可以访问 lambda 表达式块外部的变量。这个概念被称为闭包。

百万美元的问题:闭包的概念是否违反了编程的基本定律?实际上不是。为了理解这个概念,让我们看看幕后发生了什么。每当我们在 lambda 表达式中使用外部变量时,编译器都会创建

  1. 用于 lambda 表达式的匿名类。(闭包类)
  2. 类变量,用于保存 lambda 表达式中使用的外部变量的值。(闭包变量)
  3. 构造函数,用于传递我们在 lambda 表达式中使用的外部变量值。即,如果我们在 lambda 表达式中使用两个外部变量,编译器将创建带有两个参数的构造函数。
  4. 用于 lambda 表达式本身的匿名方法。

在上述情况下,编译器将创建匿名闭包类,如下所示

	public class AnonymousClosureClass
    {
        private int i;
        public AnonymousClosureClass(int i)
        {
            this.i = i;
        }
        public int AnonymousMethod(int x)
        {
            return x + i;
        }
    }

实际上,Main() 函数的变量 i 对 lambda 表达式不可访问。但它的值被传递给构造函数,然后构造函数将该值赋给类变量 i(闭包变量)。还要注意,匿名方法使用的变量 i 是类变量,而不是 Main() 函数的局部变量 i。简而言之,无需扔掉你的旧编程书 :)

参考文献

  • Apress Pro ASP.NET MVC 5
  • Wrox Professional C# 2012 and .NET 4.5
  • http://msdn.microsoft.com/zh-cn/magazine/cc163362.aspx
  • http://msdn.microsoft.com/zh-cn/library/orm-9780596516109-03-09.aspx
© . All rights reserved.