文章目录
- 匿名函数
- 匿名函数的定义
- 匿名函数作为参数传递
- 匿名函数的缺点
- lambda表达式
- 什么是lambda表达式
- 闭包
匿名函数
为什么我们要使用匿名函数?匿名函数存在的意义是为了简化一些函数的定义,特别是那些定义了之后只会被调用一次的函数,与其大费周折拖动文件然后定义在类中的某个位置,匿名函数将更加方便简洁。
匿名函数的定义
以下两种无参数匿名函数:
Action A = delegate { Debug.Log("Hello"); };
A += delegate ()
{
Debug.Log("Hello");
};
匿名函数声明需要配合委托使用,并且声明时需要在函数头加上delegate
,匿名函数的无参构造可以省略括号。
匿名函数定义不允许使用泛型(很好理解,泛型是为了函数调用时能够灵活地接受不同类型的参数,但是使用匿名函数意味着它只会被调用一次,与其使用泛型不如让我们直接指定)
Action<int, string> A = delegate (int i, string s)
{
int q = Int32.Parse(s) + i;
};
定义带参数的匿名函数只需像正常函数定义即可。如果需要返回值只需使用Func
:
Func<int, string,int> A = delegate (int i, string s)
{
int q = Int32.Parse(s) + i;
return q;
};
匿名函数作为参数传递
现有如下定义:
class Test
{
public Action action;
public void Dosomething(int a ,Action fun)
{
fun();
}
public Action MyFun()
{
return delegate () { Debug.Log("返回委托类型的匿名函数"); };
}
}
在上述类中,我们定义了两个方法,Dosomething
需要传入一个Action
委托fun
,Myfun
则返回一个Action
类型的返回值(定义的委托是可以作为返回值类型的),在其中我们将一个匿名函数作为委托类型的返回值。
void Start()
{
Test t = new Test();
t.Dosomething(1, delegate { Debug.Log("匿名函数作为参数传入"); });
Action A = delegate { Debug.Log("委托装载匿名函数并作为参数传入)"); };
t.Dosomething(1, A);
}
匿名函数可以作为委托类型的参数传入。也可以返回时作为委托类型(猜测应当是匿名函数作为参数传递时自动被封装为了对应委托):
void Start()
{
Test t = new Test();
t.action = t.MyFun();
t.action(); // 输出:返回委托类型的匿名函数
t.MyFun()(); // 只有委托返回值类型才能这么使用,调用函数后再加个括号直接调用委托
}
委托类型返回值的函数在调用后再加上括号可以直接调用该委托。
匿名函数的缺点
使用匿名函数的最大优点就是方便,但是匿名函数最大的缺点就是匿名,如下所示:
Action A = delegate { Debug.Log("你好"); };
A -= delegate { Debug.Log("你好"); }; // 无效
A();// 依旧会输出你好,因为匿名函数无法删除
A = null; // 只有清空A才能从中删除匿名函数
想要删除委托中的匿名函数,即使我们在A中减去相同定义的匿名函数也无济于事,因为此函数非彼函数。匿名函数没有函数名,也就无法被减去,即使我们定义了相同的匿名函数,他们的地址本质上也不是同一个函数。
所以如果一个委托只存一个函数,那就可以使用匿名函数,但是如果一个委托存在多个函数,那么当你想要去除这个匿名函数的时候,就只能清空这个委托。所以在设计使用匿名函数的时候,要么用一个委托只存储匿名函数,要么作为参数传入委托类型的返回值的函数进行调用(就像上述的t.MyFun()();
)。
lambda表达式
什么是lambda表达式
lambda表达式就是匿名函数的一种创建方式,使用lambda表达式可以省略匿名函数的delegate
定义:
Action A = () => { Debug.Log("你好"); };
lambda表达式省略了delegate
关键字,并使用 lambda 声明运算符=>
来声明。 同理,有参和有返回的lambda表达式也和匿名函数的定义相同。
Action<int, string> B = (int i, string s) =>
{
int q = Int32.Parse(s) + i;
};
Func<int, string,int> C = (int i, string s) =>
{
int q = Int32.Parse(s) + i;
return q;
};
// 委托已经指定类型,可以省略参数类型
Func<int, string,int> C = (i, s) =>
{
int q = Int32.Parse(s) + i;
return q;
};
当lambda表达式只执行一个语句的时候,连花括号都不需要了, 使用空括号指定0个输入参数:
Action line = () => Console.WriteLine();
不带返回值类型的lambda表达式的简洁用法,单个参数可以省略小括号:
Action<int> square = x => Console.WriteLine("x");
甚至lambda表达式可以省略返回值定义,只需定义委托类型即可:
Func<int, int> square = x => x * x; // return x*x,在带返回值类型的委托中省略return
Func<int, int, bool> testForEquality = (x, y) => x == y; //多个入参还是需要括号和逗号区分的
在多个入参的情况下,lambda用弃元来表示哪些参数不被使用
(多嘴一下,在委托定义了入参数量,然后lambda表达式需要使用到的入参比定义的入参数量少的情况下当然可以使用弃元。但是作为添加到委托内的匿名函数,如果委托需要装载多个函数的话,实际上不太适合使用匿名函数):
Func<int, int, int> constant = (_, _) => 42;
闭包
关于闭包的具体说明已经在此文【Lua学习笔记】Lua进阶——函数和闭包中解释过了,此处便不再赘述。
简单来说,闭包就是内部函数引用了外部函数的变量,导致本应该在栈中释放的外部函数的生命周期遭到改变(延长)。这个(这些)变量被我们称为upvalues,它们原本的作用域是外部函数,但是随着内部函数的使用,它们的作用域又包括了内部函数。
就如同下面的例子:
public event Action action;
public void Test()
{
int value = 100;
action = () => { Debug.Log(value); };//使用了value,但value作用域在函数Test而非匿名函数内
}
上述例子中我们在匿名函数内部使用了外部函数Test的变量value
,结果是可以正常执行的。照理说局部变量value
在Test
中没被使用就会释放,但是匿名函数的使用延长了它的生命周期,这就是闭包。
再看下列的例子:
public void Test()
{
for (int i = 0; i < 10; i++)
{
action += () => { Debug.Log(i); };
}
}
action();
当我们执行上述语句的时候输出答案是什么?可能你以为是0123456789,但真正的答案是输出10次10,原因其实也很简单,在委托中存储了十个匿名函数,每个的指令是输出i
,但是不要忘了i
是upvalue,它不是赋值到内部这个匿名函数的局部变量,而是匿名函数直接调用在外部函数的i
的值,当循环结束时i=10
,本应当释放的i
的生命周期得到了延长,当我们执行委托的时候,则匿名函数才会调用这个i
,而此时i=10
,因此输出了10次10。
public void Test()
{
for (int i = 0; i < 10; i++)
{
int index = i;
action += () => { Debug.Log(index); };
}
}
action();
然而使用int index = i
则可以实现上述功能,原因在于它是值类型的变量,我们每次在for循环中int
一个index
,实际上都是声明了一个新的int
变量。所以每个匿名函数调用的index实际上是10个不同的index。
如果害怕lambda表达式不小心捕获了外部函数的upvalue,则可以使用static
关键字进行限制:
Func<double, double> square = static x => x * x;