C#语法——委托

2020年01月12日 11:42 0 点赞 0 评论 更新于 2025-11-21 21:29

本篇文章主要介绍委托的应用。委托是大家常见的语法,但会用与精通之间存在巨大差别。一个程序员若不能精通委托,就难以成为架构师,因此委托是必须掌握的技能之一。

委托的定义

委托的本质

委托实际上是一种引用类型。微软使用 delegate 关键字来声明委托,delegateintstringdouble 等关键字一样,用于声明类型。以下是声明两个委托的示例代码:

public delegate void TestDelegate1(string message);
public delegate int TestDelegate2(MyType m, long num);

可能有人会疑惑,delegate 既然和 intstring 是同等地位的关键字,为何后面还能跟 void 或者 int 呢?我们可以把 delegate 后面的 void TestDelegate(string message) 理解为一个特殊的变量声明。delegate 关键字专门用于定义这种复杂的变量,该变量可以包含一个返回值和任意数目、任意类型的传入参数。

从官方定义来看,委托类型的声明与方法签名相似,所以其书写方式类似函数定义。而且,用 delegate 定义的变量,只能用函数赋值,示例如下:

public delegate void TestDelegate(string message);
public delegate long TestDelegate2(int m, long num);

public static void Excute()
{
TestDelegate2 td = Double;
}

static long Double(int m, long num)
{
return m * num;
}

委托的基本应用

委托的赋值与使用

学会委托的赋值后,就可以开始使用委托了。委托的使用方式如下:

public static void Excute()
{
TestDelegate2 td = Double;
string result = td(51, 8);
Console.WriteLine(result);
}

可以发现,委托的使用方式与函数调用相同。这是因为委托是用函数来赋值的,所以调用方式一样并不奇怪。换个说法,委托封装了一个函数。

由于委托是封装函数的引用类型,其第一种常规应用就很明显了,即作为引用类型的函数。如果函数是引用类型,只要它没被内存回收,就可以被调用。对于 public 函数或者 public static 函数,还能实现跨类调用、跨程序集调用等,这就是委托的基本应用。

匿名委托的应用

匿名委托的发展

在 2.0 之前的 C# 版本中,声明委托的唯一方式是使用命名方法。C# 2.0 引入了匿名方法,在 C# 3.0 及更高版本中,Lambda 表达式取代匿名方法成为编写内联代码的首选方式。

匿名委托的使用

下面通过代码学习匿名委托的使用:

delegate string anonymousDelegate(int m, long num);

public static void Excute()
{
// 2.0 时代的匿名委托
anonymousDelegate ad = delegate (int m, long num) { return m.ToString() + num.ToString(); };
// 3.0 以后的匿名委托(Lambda 表达式)
anonymousDelegate ad2 = (m, num) => { return m.ToString() + num.ToString(); };
}

匿名委托也叫 Lambda 表达式,对于不理解其原理的同学,记住这种固定写法并应用即可。虽然匿名委托减少了一些代码,但仍需我们自己声明委托。实际上,还可以进一步简写。

Action 与 Func

ActionFunc 是微软预先定义好的委托变量。其中,Action 是不带返回值的委托,Func 是带返回值的委托。可以说,ActionFunc 涵盖了我们日常使用所需的全部委托变量,使用它们就无需再手动声明委托。

以下是最简单的 ActionFunc 的定义示例:

Action a1 = () => { };
Func<int> f1 = () => { return 1; }; // 必须写 return 1;

ActionFunc 是泛型委托,最多支持 16 个入参变量。下面是一个入参的定义示例,多参数的情况以此类推:

Action<int> a1 = (i) => { };
Func<string, int> f1 = (str) => { return 1; }; // 必须写 return 1;

委托的线程应用

委托的线程应用是委托的第二种用法,分为线程使用委托和委托的异步应用两种。

线程使用委托

以下代码展示了一个无入参匿名 Action 和一个无入参匿名 Func 的使用:

Task taskAction = new Task(() => { }); // 无入参匿名 Action
taskAction.Start();

Task<int> taskFunc = new Task<int>(() => { return 1; }); // 无入参匿名 Func
taskFunc.Start();

int result = taskFunc.GetAwaiter().GetResult(); // 获取线程返回结果

可以看到,这两种委托应用的代码都非常简洁。

委托的异步应用

下面是最简单的异步调用示例:

Action action = new Action(() => { });
IAsyncResult result = action.BeginInvoke((iar) =>
{
}, null);

Func<int> func = new Func<int>(() => { return 1; });
IAsyncResult resultfunc = func.BeginInvoke((iar) =>
{
var res = func.EndInvoke(iar);
}, null);

这里使用委托的 BeginInvoke 方法来开启线程,进行异步调用。上述代码介绍了 ActionFunc 的最基础的异步应用。

委托的核心应用

第一核心应用——随手线程

在开发中,父类通常用于编写公共属性和函数,方便子类调用。委托的第一个核心应用就是在父类的公共函数中随手启动线程。

首先创建父类代码:

class BaseDelegateSyntax
{
public void AsyncLoad(Action action)
{
}

public void AsyncLoad(Action action, Action callback)
{
IAsyncResult result = action.BeginInvoke((iar) =>
{
callback();
}, null);
}

public void AsyncLoad<T>(Action<T> action, T para, Action callback)
{
IAsyncResult result = action.BeginInvoke(para, (iar) =>
{
callback();
}, null);
}

public void AsyncLoad<T, R>(Func<T, R> action, T para, Action<R> callback)
{
IAsyncResult result = action.BeginInvoke(para, (iar) =>
{
var res = action.EndInvoke(iar);
callback(res);
}, null);
}
}

在父类中添加了四个异步委托的调用函数后,就可以在继承该类的子类中随手开启线程了。子类代码如下:

class ChildDelegateSyntax : BaseDelegateSyntax
{
public void Excute()
{
// 开启异步方法
base.AsyncLoad(() => { });

// 开启异步方法,并且在异步结束后,触发回调方法
base.AsyncLoad(() => { },
() =>
{
// 我是回调方法
});

// 开启异步有入参的方法,传递参数,并且在异步结束后,触发回调方法
base.AsyncLoad<string>((s) => { }, "Kiba518",
() =>
{
// 我是回调方法
});

// 开启异步有入参的方法,传递字符串参数 Kiba518,之后返回 int 型结果 518,
// 并且在异步结束后,触发回调方法,回调函数中可以获得结果 518
base.AsyncLoad<string, int>((s) =>
{
return 518;
}, "Kiba518",
(result) =>
{
// 我是回调方法 result 是返回值 518
});
}
}

通过上述父子类的代码可以看出,委托让繁杂的线程世界变得简洁了。

第二核心应用——穿越你的世界

委托的第二种核心用法是穿越应用,这是最常见、最普通的应用。由于委托是引用类型,A 类里定义的委托,在被内存回收之前,可以被其他类调用。

在各种论坛上,经常有人询问 A 页面如何调用 B 页面的属性、方法,或者父页面获取子页面的属性、方法,子页面获取父页面的属性、方法等问题。实际上,只要定义好委托,并将委托正确传递,就可以实现穿越调用。

以下是穿越应用的代码示例:

public class FirstDelegateSyntax
{
public FirstDelegateSyntax()
{
Console.WriteLine(" First 开始 ");
SecondDelegateSyntax sds = new SecondDelegateSyntax(() =>
{
Console.WriteLine(" First 传给 Second 委托被触发 ");
});
sds.Excute();
Console.WriteLine(" First 结束 ");
}
}

public class SecondDelegateSyntax
{
public Action Action { get; set; }

public SecondDelegateSyntax(Action _action)
{
Console.WriteLine(" Second 的构造函数 ");
Action = _action;
}

public void Excute()
{
Console.WriteLine(" Second 的 Excute 被触发 ");
Action();
}
}

从代码中可以看到,传递的委托穿越了自身所属的类,在 SecondDelegateSyntax 类中被触发。

第三核心应用——回调函数

请记住,所有的回调函数都是委托的穿越应用。因为委托是引用类型,可以进行址传递,而函数本身不能被传递。当传递函数时,实际上是匿名传递了一个委托的地址。

结语

委托是我们常用的语法,它将函数封装成引用类型的变量,供其他单位调用。由于委托是引用类型,可以进行址传递,就像穿梭于系统代码中的列车,我们可以在“列车”上放置很多东西,在需要的“站点”叫停列车并使用托运的东西。理论上,合理利用委托可以大量减少冗余代码。

但需要注意的是,委托由每个程序员自行定义,如果一个项目中有多个开发者,可能会出现定义相同委托的情况,导致“撞车”现象。因此,在使用委托时,应尽量做到有序传递,预先规划好委托的使用路径,避免定义可被任何单位调用的公共委托。

本文来源: C#语法——委托