回调,委托,事件介绍

我被代码海扁 @codebeatme-s @103466775
阅读 7:35·字数 2278·更新 

本节中提到函数的部分,同样适用于类或实例的方法。

回调

回调(Callback)是指将一部分代码逻辑以某种形式,通常是函数,交付给另一方,而另一方会在某种情况下执行交付的代码。如果交付是以函数形式进行的,那么这些函数也被称为回调函数,当然,回调并非只能采用回调函数,语言可以拓展出任意一种设计,比如委托。

为何需要回调?
使用回调的原因在于,虽然知道需要执行的逻辑,但无法确定执行的具体时间,这往往是异步执行造成的。比如,我们知道需要在数据上传完成后通知用户,但无法确定上传完成的具体时间,因为上传采用了异步的方式。

什么是异步和同步?
异步不会保留控制权,当你调用支持异步的函数时,并不会产生阻塞,这意味着可以立即进入下一行代码,不必等待函数的执行结果,异步通常是由多线程来实现的。与异步相反的是同步,同步函数会保留控制权,这意味着必须等待函数执行完毕,才能进入下一行代码。

需要说明的是,这里提到的函数并不是回调函数,而是对应了另一方,回调函数往往会作为这些函数的实参。

什么是线程和进程?
线程被包含在进程内,你可以将其理解为实际执行的代码,一个进程可以拥有多个线程。线程通过串行的方式执行其包含的代码,一行代码执行完毕后,才会进入另一行。而多个线程可以被并行执行,不需要彼此等待,并共享进程内的资源。虽然在逻辑上具有并行的效果,但线程仍可能以并发的方式获取 CPU 的使用权,因为不存在无限的资源来实现真正的同时执行。

通过多线程,你可以实现异步操作,以避免不必要的等待时间。比如,我们可以创建一个新的线程来执行耗时的网络存储任务,以便用户在此期间进行其他操作。

进程可以被理解为操作系统中执行的程序或应用,比如,打开的记事本应用

在下面的 JavaScript 代码中,message会作为回调函数传递给setTimeout,并在3秒后显示消息。

callbacks_and_delegates.js
// 回调函数 message,显示一条消息
function message() {
	console.log('一条消息?!')
}

// 函数 setTimeout 将在 3 秒后调用 message setTimeout(message, 3000) // console.log 会紧接着 setTimeout 执行 console.log('请等待 3 秒钟!')
请等待 3 秒钟!
约 3 秒后…
一条消息?!

JavaScript 的 setTimeout 是否为异步函数?
是的,无论 JavaScript 的运行环境是否支持多线程,setTimeout都是一个异步函数。一些观点以线程数量为依据持相反的观点,但异步和多线程并没有必然的联系,多线程只是实现异步的方式之一。

委托

与回调函数类似,委托可以向另一方交付代码逻辑,他包含了对函数或方法的引用。为了提供一定的规范,委托可以指定签名,以排除不符合要求的函数或方法。

在一些语言中,比如 C#,委托是通过类来实现的,因此对比回调函数,委托提供了更多的可操作性,其中最为常见的是委托合并。

什么是委托合并?
委托合并也被称为委托多播,他所实现的效果是,将多个委托实例合并,这些实例所包含的代码逻辑将被捆绑在一起。

当然,委托移除提供了与合并相反的操作。

函数
想要了解签名,你可以查看函数签名一段。

在下面的 C# 代码中,我们创建了委托Loaded的两个实例并将他们合并为loaded,然后传递给函数Load。在使用 Lambda 表达式为showMessageshowData赋值时,两个匿名函数的签名与委托Loaded规定的签名保持了一致。

*.cs
// 委托 Loaded 的实例 showMessage
Loaded showMessage = data => Console.WriteLine("数据下载完毕!");

// 委托 Loaded 的实例 showData Loaded showData = data => Console.WriteLine($"数据内容为:{data}");
// 合并 showMessage,showData Loaded loaded = showMessage + showData; // 将 loaded 传递给函数 Load Load(loaded);
// 函数 Load,用于从服务器下载数据 void Load(Loaded loaded) { // 这里假设 data 是从服务器获取的数据 string data = "好吧,假设这是从服务器获取的数据!";
// 调用 loaded 所包含的匿名函数 loaded.Invoke(data); }
callbacks_and_delegates.cs
// delegate 是一个语法糖,他会转换为一个类的定义
delegate void Loaded(string data);
数据下载完毕!
数据内容为:好吧,假设这是从服务器获取的数据!

事件

从功能的角度讲,事件是委托在特定场景下的另一种称呼,即使两者的代码书写存在差异,实现互换也是完全有可能的。

事件和委托之间的区别
如果将事件和委托视为是不同的,那么他们之间的区别可能如下。委托倾向于一次性效果,并作为另一方完成任务的必须部分。比如,在异步获取数据时,我们可以通过委托告知获取数据后的下一步操作,否则单纯的获取将毫无意义。事件倾向于持久效果,不是另一方的必须部分。比如,按钮控件通常具有的点击事件,是否为该事件编写代码并不影响按钮自身的正常运作。

C# 类MiddleSchool拥有事件OnMessage,该事件会在方法ClassBegin被调用时触发。

callbacks_and_delegates.cs
// 类 MiddleSchool,表示中学
class MiddleSchool
{
	// 事件 OnMessage,表示学校发出了消息
	public event EventHandler? OnMessage;

// 方法 ClassBegin,开始上课 public void ClassBegin() { // 触发事件,外部将收到来自学校的消息 OnMessage?.Invoke(this, new MessageArgs() { Message = "上课了!" }); } }
// 类 MessageArgs,事件参数,包含了消息的内容 class MessageArgs : EventArgs { // 消息的内容 public string Message = string.Empty; }

创建MiddleSchool类的实例school,并为其编写OnMessage事件的代码,随着ClassBegin方法被调用,可以看到这些代码的执行效果。

*.cs
// 创建 MiddleSchool 类的实例
MiddleSchool school = new();
// 为事件 OnMessage 编写代码
school.OnMessage += (object? sender, EventArgs e) =>
	Console.WriteLine($"接到了学校的消息:{(e as MessageArgs)?.Message}");

// 调用 ClassBegin,事件 OnMessage 将被触发 school.ClassBegin();
接到了学校的消息:上课了!

源码

callbacks_and_delegates.js·codebeatme/programming·GitHub
callbacks_and_delegates.cs·codebeatme/programming·GitHub