委托

委托是引用类型,可以引用一个方法。

委托的定义

语法

[可见性修饰符] delegate 返回类型 委托名称(参数列表)

委托的定义就如同函数的签名。

public delegate void MyAction();

public delegate int MyFunc(int a,int b);

委托都继承自System.MulticastDelegate类,使用一个函数地址作为参数,拥有Invoke,BeginInvoke,EndInvoke方法,进行同步或异步调用。

委托的使用

委托的创建语法

委托名称 变量名 = new 委托名称(函数名称);//完整写法
委托名称 变量名 = 函数名称;//简写

其中的函数名称可以是静态函数,也可以是成员函数。

委托的调用

[返回值类型 变量名 = ]委托类型变量.Invoke(参数列表);//完整写法
[返回值类型 变量名 = ]委托类型变量(参数列表);//简写

无参数无返回值的委托使用

public static void F(){
    Console.WriteLine("ok");
}

MyAction action = new MyAction(F);
action.Invoke();

MyAction action2 = F;
action2();

有参数有返回值的委托使用

public static int Add(int a,int b){
    return a+b;
}

MyFunc func = new Func(Add);
int result = Add(100,200);

MyFunc func2 = Add;
int result2 =  func2(100,200);

委托的协变和逆变

委托的返回值是协变的

public delegate object GetObject();

public string GetString(){
    return null;
}

GetObject getString = GetString;

可以用返回值为object类型的委托,保存返回值为string的函数。

委托的参数是逆变的

public delegate void SetString(string s);

public void SetObject(object obj){

}

SetString setObject = SetObject;

可以使用参数数string的委托,保存参数是object的函数。

匿名委托

上面创建委托需要一个已经定义好的方法,匿名委托可以使用临时的方法创建委托。

语法

委托类型 委托变量 = delegate[(参数列表)]
{
    [return 返回值];
};

如果不需要使用参数,参数列表可以省略。

无参数的匿名委托的创建

MyAction acion = delegate
{
    Console.WriteLine("ok");
}
action();

有参数的匿名委托的创建

MyFunc func = delegate(int a, int b)
{
    return a + b;
}

int result = func(100,200);

在内部,编译器创建了一个内部类和方法。

编译器生成的无参匿名委托

class G{
    void F()
    {
        Console.WriteLine("ok");
    }
}
G g = new G();
MyAction action = g.F;
action();

编译器生成的有参数匿名委托

class G{
    int F(int a, int b)
    {
        return a + b;
    }
}
G g = new G();
MyFunc func = g.F;
func(100, 200);

编译器简化了我们的代码量。

Lambda表达式

使用匿名委托创建委托还略显麻烦,lambda表达式提供了更方便的语法。

语法

委托类型 变量名 = (参数列表) => {语句}
//没有参数不能省略圆括号
MyAction action = () => Console.WriteLine("ok");
action();

//参数类型可以省略
MyFunc func = (a, b) => a + b;
int result = func(100, 200);

Lambda表达式的背后,也是编译器为我们生成内部类和方法。

创建方式选择

在大部分情况下,Lambda表达式都更简洁。 参数多于1个而且在不需要参数时,匿名委托更加简洁,比如事件处理程序。

闭包

闭包是指在匿名委托和Lambda表达式语句内部,可以读写语句外部的变量。

内部访问外部变量

int value = 10;//外部
MyAction action = () =>
{
	Console.WriteLine(value);//内部
};
action();

输出结果是10。

外部修改影响内部

 int value = 10;
 MyAction action = () =>
 {
 	Console.WriteLine(value);
 };
 value=20;
 action();
 

输出结果是20。说明外部修改影响内部

内部修改影响外部

  int value = 10;
  MyAction action = () =>
  {
  	value =20;
  };
  action();
  Console.WriteLine(value);

输出结果是20。说明内部修改会影响外部。

结论

总之,闭包内部和外部其实是同一个变量,而不是副本。 在内部,编译器使用一个类来容纳所有被闭包引用的局部变量。被闭包引用的局部变量实际是该类的字段,其生存期也被延长至和委托一致。

编译器生产的代码

编译器生成的访问外部变量

class G{
    int value;
    void F(){
        Console.WriteLine(value);
    }
}

G g = new G();
g.value = 10;
MyAction action = g.F;
action();

编译器生成的修改外部变量

class G{
    int value;
    void F(){
        Console.WriteLine(value);
    }
}

G g = new G();
g.value = 10;
MyAction action = g.F;
g.value = 20;
action();

编译器生成的修改内部变量

class G{
    int value;
    void F(){
        value = 20;
    }
}

G g = new G();
g.value = 10;
MyAction action = g.F;
action();
Console.WriteLine(g.value);

通过查看编译器生产的代码,结果是什么就一目了然了。

使用闭包能大大简化代码,但应该始终记得,在使用闭包时,局部变量已经不是局部变量,而是一个字段。

循环中的闭包

如下代码输出10次10,这是由于闭包捕获的是最后i的结果

static void F()
{
    List<Action> actions = new List<Action>();

    for (var i = 0; i < 10; i++)
    {
        var action = new Action(() => Console.WriteLine(i));
        actions.Add(action);
    }

    foreach (var action in actions)
    {
        action();
    }
}

如果想输出0-9,需要借助一个临时变量

static void F()
{
    List<Action> actions = new List<Action>();

    for (var i = 0; i < 10; i++)
    {
        var j = i;
        var action = new Action(() => Console.WriteLine(j));
        actions.Add(action);
    }

    foreach (var action in actions)
    {
        action();
    }
}

在集合类型不同或编译环境不同的foreach循环,对循环变量的捕获结果都不确定。

保险起见,在闭包引用循环变量时,始终在使用临时变量保存的循环变量。

委托的作用

委托的作用,在于可以将一个操作作为数据使用。

举例来说,需要打印符合条件的数组元素,这个条件就参数,就可以将这个条件定义为委托。 定义

public delegate bool Predicate(int value);

static void Print(int[] array,Predicate predicate)
{
    foreach (var item in array)
    {
        if (predicate(item))
        {
            Console.WriteLine(item);
        }
    }
}

使用

int[] array = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//打印偶数
Print(array, value => value % 2 == 0);
//打印小于8的数
Print(array, value => value < 8);

接口和委托

从功能上来说,接口也能够实现委托的功能

使用接口代替无参无返回值委托

interface IMyFunc
{
    int Invoke(int a, int b);
}

class MyActionImpl : IMyAction
{
    public void Invoke()
    {
        Console.WriteLine("ok");
    }
}

IMyAction action = new MyActionImpl();
action.Invoke();

支持委托是.NET的选择,C#提供了简化语法。

委托的不足

即使参数返回值都相同的委托,只要类型不同,就不能互相转换。

public delegate bool Predicate<T>(T item);
public delegate TResult Func<T,TResult>(T item);

class Program
{
    static void Main(string[] args)
    {
        Predicate<int> isEven = x => x%2==0;
        Func<int,bool> func = isEven;//编译错误,不支持转换
        Func<int, bool> func = new Func<int, bool>(isEven);;//只能够手动再次调用
    }
}

编译器既然如此强大,为何不能支持转换来简化代码?