这是真 最后一章了,写一下委托的匿名函数和Lambda表达式。
由于我们在委托中,每一次创建实例都会声明一个方法与之匹配,但如果方法里的逻辑不太复杂,这种每次都要new的操作会让代码变得臃肿,所以C#引入了匿名方法这种机制即为本地函数。
匿名方法是用于“简化代码使用”。顾名思义,它是没有函数名的函数。
它使用Delegate关键字创建,顾名思义它是给委托使用的,我们来看一个小例子:
public delegate int NoName(int i);
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
NoName nonameDelegate = delegate (int i)
{
return i + 1;
};
int b = nonameDelegate.Invoke(a);
Console.WriteLine("返回值是"+b);
}
如果是无参函数,就要加上空括号()。
如果省略匿名函数delegate后面的参数定义,这样将定义一种特殊的匿名方法,它可以指派给具有任何签名的任何委托。注意,这样指派可以不要参数列表,但是返回值必须存在:
public delegate int NoName(int i);
//匿名方法
class Program
{
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
NoName nonameDelegate = delegate
{
return a + 1;
};
int b = nonameDelegate.Invoke(a);
Console.WriteLine("返回值是" + b);
}
}
它们输出的结果很明显:
内部调用逻辑
在CIL代码中,编译器为源代码中的每一个匿名方法都创建了一个对应的方法,采用了和创建委托实例相同的操作,创建的方法作为回调函数由委托实例包装起来。
那么匿名方法能实现什么与其他普通函数不同的功能呢?
闭包
我们平常看到的一个函数,它内部的变量都是两个来源。1:自己创建的。2:通过形参引入。
那么我们的闭包函数由于是和其他的逻辑写在一起的原因,能捕获外部变量。外部变量的定义如下
- 外部变量指的是匿名方法的作用域内的局部变量或者参数。
- 由此,捕获外部变量指的是在匿名方法中使用的外部变量。
例如:
public delegate void BiBao();
public void Function()
{
int n = 0;
BiBao biBao = delegate
{
n++;
};
}
这个例子里面,该匿名函数作用域即为Function函数里面的逻辑块。匿名函数捕获的外部变量即为n。
我们都知道,一个函数的生存周期中,其中所有的局部变量会和方法一样分配在栈上。当方法返回后,局部变量会随着方法返回的栈帧一起被销毁
但我们看个例子:
public delegate int NoName(int i);
//匿名方法
class Program
{
static void Main(string[] args)
{
TestNoName noName = new TestNoName();
Action<int> act = noName.CreateActionInstance();
act(10);
}
}
public class TestNoName
{
public Action<int> CreateActionInstance()
{
int count = 0;
Action<int> action = delegate (int number)
{
count+=number;
Console.WriteLine("匿名方法输出" + count);
};
action.Invoke(1);
return action;
}
}
在上文中,我们有一个CreateActionInstance的函数,里面有一个匿名函数。我们在外部调用了该函数匿名函数的时候,会发生什么呢?
第一次调用的时候输出1不奇怪,函数CreateActionInstance内部的委托调用时输入了1。第二次的时候,我们输入10,我们发现CreateActionInstance中的n被保留下来了,加上我们输入的十变成了11。在使用闭包的特性的时候我们也应该注意:
闭包的内部实现
闭包的特性就体现出来了,当匿名函数调用的时候使用了上下文中的变量的时候,CIL中会临时隐式生成一个包含匿名函数捕获外部变量的类,我们之所以可以在有匿名函数的方法结束后还可以得到里面的变量,就是因为方法保留了对临时类的引用,通过类型实例进而操作count变量。
我们可以理解为,对于一个被捕获的外部变量(无论是引用类型还是值类型),只会实例化一次,对于匿名函数来说,它都是一个引用。
当外部变量被匿名函数引用时,不会像其他普通的局部变量分配在栈上,它会与临时类的实例一起分配在托管堆上。
由此我们也可以知道,并非所有的局部变量都会随方法一起分配在栈上,闭包的变量会由于匿名函数而分配在堆上。
通过闭包的作用:
- 一个局部变量可以根据匿名函数的调用延长生存周期,函数调用生成的值在下次调用仍然保持。
- 而从安全性的角度而言,闭包有利于信息隐蔽,私有变量只在该函数内可见。保证了数据的安全性。
委托实例数组闭包的对象引用问题
我们上文说到,对于匿名函数内部来说,外部变量都是引用,那么如果多个委托实例同时捕获一个外部变量,那么捕获到的引用所指向的地址也是同一个。来看一个复杂一些的例子:
public delegate void MethodInvoker();
class Program
{
public delegate void MethodInvoker();
static void Main()
{
List<MethodInvoker> invokers = new List<MethodInvoker>();
int outSide = 0;
for (int i = 0; i < 5; i++)
{
invokers.Add(delegate
{
Console.WriteLine(outSide);
outSide++;
});
}
foreach (MethodInvoker invoker in invokers)
{
invoker.Invoke();
}
}
}
在这个例子中,我们创建了一个外部变量,然后让count在委托调用的时候输出,然后加一,由于每个匿名函数捕获到的都是同一个外部变量,操作的仅仅是这个变量的引用。所以我们的输出结果应该是 0 1 2 3 4:
由此引申出来的坑:
由于多个匿名函数很可能捕获到同一个变量的引用,所以存在很多个委托实例时,容易将不同的委托实例混淆不清,例如下面的例子:
public delegate void MethodInvoker();
class Program
{
public delegate void MethodInvoker();
static void Main()
{
List<MethodInvoker> invokers = new List<MethodInvoker>();
int outSide = 0;
for (int i = 0; i < 2; i++)
{
int inSide = 0;
invokers.Add(delegate
{
Console.WriteLine(outSide + " " + inSide);
outSide++;
inSide++;
});
}
Test(invokers);
}
static void Test(List<MethodInvoker> invokers)
{
invokers[0].Invoke();
invokers[0].Invoke();
invokers[0].Invoke();
invokers[1].Invoke();
invokers[1].Invoke();
}
}
这个例子中最终的输出结果比较诡异,由于outSide是被所有委托都捕获到的引用,所以每次执行不同的委托,outSide都会++,而inSide分属于不同的委托,所以不同的委托调用时的inSide需要分开处理,所以输出的结果也比较奇怪:
这样的闭包结果是我们在使用匿名函数中需要避免的,因为同种逻辑的匿名函数由于闭包捕获引用的问题造成的引用不一致的问题会让代码的输出变得晦涩。那么我们在使用闭包的时候需要注意什么呢?
闭包的使用规则
- 如果不使用闭包代码逻辑同样简单,那么就不要用闭包
- 对于匿名函数的委托数组,在循环中对匿名函数进行声明时若产生闭包,闭包的外部变量最好在每次循环中另外创建。
- 如果捕获的外部变量不对其值进行改变,则不需要考虑上个问题。
- 闭包的逻辑复杂程度要取决于被捕获的变量是否能从匿名函数中跳出,例如作为返回值、或者被启动线程。如果没有这些操作,那么闭包的情况就简单的多。
- 对于被捕获的引用类型变量,由于闭包延长了其生存周期。如果该变量在内存中的开销较大,那么对其闭包的情况需要认真考虑。
Lambda表达式
Lambda表达式是在C#3引入的,匿名方法能做到的,Lambda表达式一样也能做到。
Lambda是匿名方法的进一步演化,相较于匿名方法来说更加易读。它和匿名方法一样,需要匹配其对应的委托实例。
我们可以把我们匿名方法的第一个例子改成Lambda表达式的形式:
public delegate int NoName(int i);
class Program
{
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
NoName nonameDelegate = (int i) =>
{
return i + 1;
};
int b = nonameDelegate.Invoke(a);
Console.WriteLine("返回值是" + b);
}
}
我们的Lambda表达式的语法是:(参数列表)=> { 函数逻辑 };
通过非常具有标志性的“=>”语法我们可以区分得出Lambda表达式。
而且,Lambda在匿名函数上进一步简化的结果就是:=>的左边可以直接是函数返回值。例如我们上面的例子可以改成:
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
Func<int, string> func = (int i) => "返回值是:" + i.ToString();
string b = func.Invoke(a);
Console.WriteLine(b);
}
我们使用Func泛型委托,=>左边是Lambda表达式参数列表,右边即为函数的返回值了。
但是,有两行以上的逻辑必须加上花括号。
甚至于,我们使用泛型委托的时候,可以定义一个函数形参但是不指定其类型,类型由编译器从委托的参数列表类型推断出来!
如下面的例子:
static void Main(string[] args)
{
int a = int.Parse(Console.ReadLine());
int b = int.Parse(Console.ReadLine());
Func<int, int, string> func = (i, j) => "返回值是:" + (i + j).ToString();
string c = func.Invoke(a, b);
Console.WriteLine(b);
}
它返回的结果是显而易见的:
甚至于我们参数列表中只有一个参数的时候,我们可以省略参数的括号:
Func<int, string> func = i => "返回值是:" + i.ToString();
这样就是Lambda的用法,它在匿名函数上进一步简化。
- 在Lambda中,编译器看到Lambda表达式时会在类中自定义一个新的、不易读的私有方法。和匿名函数一样,它同样存在闭包,并且可以捕获外部变量。
- Lambda表达式取代了匿名方法,但是匿名方法可以不设定参数列表来派给具有任何签名的任何委托,这是Lambda不能完成的
以上就是我们的匿名函数和Lambda表达式的用法。