正常代码、Rx 和 YieldAwait 对比






4.80/5 (10投票s)
对比使用普通事件处理程序、Rx(响应式扩展)和 YieldAwait 库时的代码风格。
上述应用程序的截图
本文描述了什么?
在 C# 中,我们有几种方法可以等待事件,例如:
- 使用普通事件处理程序
- 使用lambda 函数作为事件处理程序
- 使用Rx(响应式扩展)
我创建了一个名为 YieldAwait 的库,它允许您在需要等待事件的地方停止代码的执行,从而利用yield
语句的功能。这意味着它为您提供了一种等待事件的新方式。所以我们现在有了另一种方法。 - 使用YieldAwait 库
因此,在本文中,我将分别比较使用这四种方法的代码风格。
什么是 YieldAwait 库?
YieldAwait 库正在 CodePlex 上开发。您可以在那里下载源代码和更多示例。它包含了使用此库等待事件的高级示例。
假设一个示例场景
例如,我们设想创建一个代码,通过等待Timer.Tick
事件来闪烁Label
UI 控件的背景颜色。闪烁的方式是:黑色 -> 灰色 -> 白色 -> 灰色 -> 黑色……
闪烁行为的最终目标应该如下面的图像所示
为了进行比较,我们使用与下面所示相同的默认窗体布局,其中包含一个Label
UI 控件label1
和一个Timer
控件timer1
。
1. 使用普通事件处理程序
首先,这是通过在第 [C] 行使用普通事件处理程序来等待Timer.Tick
事件,从而闪烁Label
UI 控件的backcolor
的代码。
public partial class NormalEventHandlerForm : Form {
public NormalEventHandlerForm() {
InitializeComponent();
}
private int _Bright = 0; // [A]
private int _Step = 10; // [B]
private void timer1_Tick(object sender, EventArgs e) { // [C]
_Bright += _Step; // [D]
if (_Bright > 255) { // [E]
_Step = -10;
_Bright += _Step;
} else if (_Bright < 0) {
_Step = 10;
_Bright += _Step;
}
label1.BackColor = Color.FromArgb(_Bright, _Bright, _Bright); // [F]
}
}
在这种情况下,您必须使用_Bright
和_Step
两个字段变量来在每个Timer.Tick
事件之间保留颜色闪烁信息。这段代码看起来非常典型,我们通常以这种方式编写事件驱动的程序。
2. 使用 lambda 函数作为事件处理程序
在本节中,lambda 函数被用作第 [C] 行的事件处理程序,这与第 1 节(事件处理程序)不同。
public partial class LambdaEventHandlerForm : Form {
public LambdaEventHandlerForm() {
InitializeComponent();
}
private void LambdaEventHandlerForm_Load(object sender, EventArgs e) {
var bright = 0; // [A]
var step = 10; // [B]
timer1.Tick += (_sender, _e) => { // [C]
bright += step; // [D]
if (bright > 255) { // [E]
step = -10;
bright += step;
} else if (bright < 0) {
step = 10;
bright += step;
}
label1.BackColor = Color.FromArgb(bright, bright, bright); // [F]
};
}
}
在这种情况下,代码风格与第 1 节(事件处理程序)非常相似,但您无需定义字段变量。
3. 使用 Rx(响应式扩展)
我们也可以为这个示例使用 Rx(响应式扩展)的功能。Rx 在重复等待Timer.Tick
事件的情况下也很有用。
public partial class RxForm : Form {
public RxForm() {
InitializeComponent();
}
private void RxForm_Load(object sender, EventArgs e) {
var enum_bright = Enumerable // [G]
.Range(0, 25 + 1)
.Concat(Enumerable
.Range(0, 25)
.Reverse())
.Select(_i => _i * 10)
.ToArray();
Observable
.FromEvent<EventArgs>(timer1, "Tick") // [H]
.Zip(enum_bright, (_e, _bright) => _bright) // [I]
.Repeat()
.Subscribe(_bright => {
label1.BackColor = Color.FromArgb(_bright, _bright, _bright); // [F]
});
}
}
代码风格与前面的章节截然不同,您将看到。如果您有使用 Rx 的经验,很容易理解代码是如何工作的。
在第 [G] 行,创建了一个名为enum_bright
的数组,其中包含 [0, 10, ... 240, 250, 240, ... 10, 0] 的列表。使用 Rx 的Observable
,在第 [H] 行捕获timer1.Tick
事件,并在第 [I] 行将数组和事件组合起来。每次事件发生时,将调用第 [F] 行的代码,变量_bright
将像 0, 10, ... 240, 250, 240, ... 10, 0 这样变化。因此,label1
的背景颜色将从黑色闪烁到白色再回到黑色。
4. 使用 YieldAwait 库
本节使用了YieldAwait
库。该库对于需要按顺序等待多个事件的此类情况非常有用。
public partial class YieldAwaitForm : Form {
public YieldAwaitForm() {
InitializeComponent();
}
IEnumerable<bool> TestFunc(EventWaiter waiter) {
while (true) { // [J]
for (var bright = 0; bright < 255; bright += 10) { // [K1]
label1.BackColor = Color.FromArgb(bright, bright, bright); // [F1]
yield return waiter.Wait(timer1, "Tick"); // [L1]
}
for (var bright = 255; bright > 0; bright -= 10) { // [K2]
label1.BackColor = Color.FromArgb(bright, bright, bright); // [F2]
yield return waiter.Wait(timer1, "Tick"); // [L2]
}
}
}
private void YieldAwaitForm_Load(object sender, EventArgs e) {
new EventWaiter(TestFunc);
}
}
它非常直接。您可以直接在代码中表达您想做的事情。您可以像您想的那样编写代码。即使您不知道如何有效地使用 Rx,也可以这样做。
代码有两个for循环。第 [K1] 行的循环用于从黑色到白色闪烁,第 [K2] 行的循环用于从白色到黑色闪烁。在 [K1]for循环中,变量bright
将从0
增加到250
,例如0
、10
、...240
、250
。for循环每次将停止在第 [L1] 行,并在发生timer.Tick
事件时再次运行。因此,label1
的背景颜色将从黑色变为白色。 [K2]for循环以相同的方式工作,而 [J]while循环使闪烁永远持续。
如果您想了解如何使用这个库,CodePlex 上的项目页面有易于理解的解释和一个非常简单的示例代码。所以请也去看看那个页面。
5. 使用带 Yield 函数的普通事件处理程序
在看过第 3 节(Rx)和第 4 节(YieldAwait
)的代码风格后,您可能会认为也可以使用带yield
函数的普通事件处理程序,如下所示:
public partial class EventHandlerAndYieldForm : Form {
public EventHandlerAndYieldForm() {
InitializeComponent();
}
private IEnumerator<int> _BrightEnumerator;
private void timer1_Tick(object sender, EventArgs e) { // [C]
_BrightEnumerator.MoveNext();
var bright = _BrightEnumerator.Current;
label1.BackColor = Color.FromArgb(bright, bright, bright); // [F]
}
IEnumerator<int> _GetBrightEnumerator() {
while (true) {
for (var bright = 0; bright < 255; bright += 10) {
yield return bright;
}
for (var bright = 255; bright > 0; bright -= 10) {
yield return bright;
}
}
}
private void EventHandlerAndYieldForm_Load(object sender, EventArgs e) {
_BrightEnumerator = _GetBrightEnumerator();
}
}
我认为它可读性较差,与第 4 节(YieldAwait
)相比,但您也可以使用yield
函数以这种方式编写代码。
关注点
首先,您可以用多种风格编写闪烁标签程序的代码。我不能说哪种好哪种不好。所有风格可能都有优点和缺点。
但是,我认为有些有趣的方面是:
- 当您使用像第 3 节那样的 Rx 时,
- 代码中很可能出现许多小的 lambda 函数,
- 并且代码中使用的每个变量的作用域很可能很小。
- 小作用域意味着复杂性低且易于阅读。
- 当您使用像第 4 节那样的
YieldAwait
库时,- 您不使用任何 lambda 函数,
- 并且代码看起来非常普通,因为它不使用任何技术性编码(如方法链或 lambda 函数……)而是使用
yield
语句。 - 由于代码将完全按照所写内容逐行执行,因此可读性会更高。
- 其用法和行为与使用 C# 5.0 的新关键字
await
非常相似。
您认为这些观点是什么?欢迎您的评论。
链接和更多信息
项目
技术解释
以前的论坛
- Visual C# Developer Center > Visual C# Forums > Visual C# General
- Visual C# Developer Center > Visual C# Forums > Visual C# General (日文)
历史
- 2011/01/10
- 撰写了第一版文章
- 2011/01/18
- 在第 4 节和第 5 节中添加了一些解释
- 为示例应用程序添加了截图
- 2011/01/19
- 在“有趣之处”部分添加了一些解释
- 2011/01/28
- 添加了相关链接