CS4L15——Lambda表达式

lambda表达式

可以将 lambda 表达式 理解为 匿名函数 的缩写
它除了写法不同外,使用上和匿名函数一模一样
都是和委托或者事件 配合使用的

匿名函数存在闭包这个概念,闭包确保了内层的匿名函数可以引用包含在它外层的函数的变量,即使外层函数的已经执行完毕

注意:通过调用 使用闭包变量的匿名函数 时,闭包变量的值不是变量创建时的值,而是在父函数(严格来说是作用域)范围内的最终值!!!

lambda表达式语法

1
2
3
4
5
6
7
8
9
10
11
//匿名函数
delegate (参数列表)
{

};

//lambda表达式
(参数列表) =>
{
//函数体
};

使用

  • 无参无返回值

    1
    2
    3
    4
    5
    Action a = () =>
    {
    Console.WriteLine("无参无返回值的lambda表达式");
    };
    a();

    输出:

    1
    无参无返回值的lambda表达式
  • 有参

    1
    2
    3
    4
    5
    Action<int> a2 = (int value) =>
    {
    Console.WriteLine("有参数的lambda表达式:{0}", value);
    };
    a2(100);

    输出:

    1
    有参数的lambda表达式:100

    甚至参数类型都可以省略 因为参数类型和委托或事件容器一致

    1
    2
    3
    4
    5
    Action<int, string> a3 = (value, value2) =>
    {
    Console.WriteLine("省略参数类型的写法{0},{1}", value, value2);
    };
    a3(200, "123");

    输出:

    1
    省略参数类型的写法200123
  • 有返回值

    1
    2
    3
    4
    5
    6
    Func<string, int> a4 = (value) =>
    {
    Console.WriteLine("有返回值有参的lambda表达式{0}", value);
    return 1;
    };
    Console.WriteLine(a4("123123"));

    输出:

    1
    2
    3
    4
    5
    6
    Func<string, int> a4 = (value) =>
    {
    Console.WriteLine("有返回值有参的lambda表达式{0}", value);
    return 1;
    };
    Console.WriteLine(a4("123123"));

如果函数逻辑非常简单,则可以进一步缩写,下面是缩写示例:

  • 函数体只有一句表达式,可以省略大括号,如果有返回值,只要表达式返回值的类型是正确的,就可以直接省略 return

    1
    2
    Action<int> printNumber = (int i) => Console.WriteLine(i);
    Func<int, string, bool> isTooLong = (int x, string s) => s.Length > x;
  • 如果lambda表达式的参数列表的类型可以被编译器推断出来(例如可以由接收函数的委托决定各个参数类型)
    则参数列表内的参数可以省略类型(注意,lambda表达式的参数列表要么全部省略类型,要么全部指定类型,不能只省略部分)

    1
    Func<int, int, bool> testForEquality = (x, y) => x == y;    //这里x, y的类型都可以由前面的委托传入的泛型类型推断出来
  • 如果函数只有一个参数,则参数列表的小括号也可以省略 (没有参数时小括号不可省略!)

    1
    Func<int[], int> getFirstNumber = a => a[0];

这种缩写常用于参数传递,有些方法的参数需要执行对应格式的方法(例如:LINQ),而如果我们传入的逻辑非常简单时,这种写法就很方便了

其他传参使用等和匿名函数一样,缺点也是和匿名函数一样的

闭包(非常重要!!!)

闭包确保了内层的匿名函数可以引用包含在它外层的函数的变量,即使外层函数的执行已经终止

注意:通过调用 使用闭包变量的匿名函数 时,闭包变量的值不是变量创建时的值,而是在父函数(严格来说是作用域)范围内的最终值!!!

函数内部声明的局部变量,在函数执行完毕后就会被释放(以值类型变量为例),而无法再被使用

1
2
3
4
5
public void TestFunc() 
{
int value = 10;
Console.WriteLine(value);
} //value 在这里执行完了就被释放,之后再也不能使用了

内层的匿名函数可以调用外层函数的变量,因为匿名函数很有可能会写在另一个函数内部,而匿名函数内经常会使用到外部函数的局部变量

1
2
3
4
5
6
7
8
9
10
11
public event Action action;

public void TestFunc()
{
int value = 10;
//内层的匿名函数可以调用外层函数的变量,比如下面的匿名函数就可以使用函数里定义的value;
action = () =>
{
Console.WriteLine(value);
};
}

当外层函数的某个局部变量,被内层的匿名函数使用,且这个内层的匿名函数,被传递到函数外部或者由函数外部的成员变量装载时
我们就会说这个 被匿名函数使用的局部变量 形成了闭包,因为这个局部变量的生命周期被改变了,它不会在外层函数执行完毕时就被释放

如上面的代码,TestFunc()​ 函数执行完毕时,value​ 不会因为 TestFunc()​ 函数执行完毕而被释放
value​ 会一直存在于外部的 action​ 内,确保调用 action​的逻辑正常执行,除非手动将 action​ 置空

闭包这个特性确保了匿名函数执行时可以使用其作用域以外的局部变量,并且可以不需要担心这个局部变量因为外层函数执行完毕而被释放的问题

闭包带来的影响

一个形成闭包的变量在被多个匿名函数使用时,这个变量值是相同的,唯一的,变量的值取决于 声明变量的作用域执行完毕时 的最终值

以下面的代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Test
{
public event Action action;

public Test()
{
//闭包产生的影响
for (int i = 0; i < 10; i++)
{
action += () =>
{
Console.WriteLine(i);
};
}
}

public void DoSomething()
{
action();
}
}

internal class Program
{
static void Main(string[] args)
{
Test t = new Test();
t.DoSomething();
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
10
10
10
10
10
10
10
10
10
10
10

和直觉不同,这里打印了 10 个 10,而不是 0 到 9

这是因为,每次循环向 action​ 添加的 所有的匿名函数,都共用该循环的同一个临时变量 i
又因为 i​ 在这个循环里 是唯一的,因此每次循环改变 i​ 时都会改变 action​ 内所有匿名函数里的 i

在我们调用 action​ 输出值时,循环已经执行完毕,循环完毕时 i​ 是 10,因此 action​ 内部所有函数的 i​ 都是10,因此会打印 10 个 10

如果我们要打印出 0 到 9,就需要让委托内部的函数引用不同的变量,且变量只存储当时循环时 i​ 的值,后续循环不能影响到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class Test
{
public event Action action;

public Test()
{
//闭包产生的影响
for (int i = 0; i < 10; i++)
{
action += () =>
{
Console.WriteLine(i);
};
}

//如何打印0-9
for (int i = 0; i < 10; i++)
{
//这样就会打印0-9,每次循环都会重新声明一次index
int index = i;
action += () =>
{
Console.WriteLine(index);
};
}
}

public void DoSomething()
{
action();
}
}

internal class Program
{
static void Main(string[] args)
{
Test t = new Test();
t.DoSomething();
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
10
10
10
10
10
10
10
10
10
10
10
0
1
2
3
4
5
6
7
8
9

可见,匿名函数使用在循环内部声明的变量,而非声明循环时声明的变量 i​ 时,就不会打印出 10 个 10,而是 0 到 9,

因为每次循环,都会重新声明一个 index​,且为其赋值当前循环时的 i​ 的值
而每次循环并赋值的 index​,都是重新创建的 index​ ,不是上次循环的 index​,这个循环里的 index​ 不是唯一的
因此 action​ 内的各个函数的 index​ 都是不同的,一个函数对应一个 index​,而这些 index​ 的值各不相同,因此,会打印0-9