停止编写 Switch 和 If-Else 语句!






4.89/5 (43投票s)
使用元组和扩展方法实现 Match 函数的乐趣
引言
虽然本文有些半开玩笑,但它也是对如何使用元组、扩展方法和可变参数进行有趣探索。我想看看我能做到什么程度,而本文就是结果。
此代码至少需要 C# 7.1!
F# 有一个漂亮的 match 表达式,如下所示
match [something] with
| pattern1 -> expression1
| pattern2 -> expression2
| pattern3 -> expression3
我们可以在 C# 中做类似的事情吗?
当然!实际上,improved
switch 关键字可以评估表达式并执行模式匹配。 例如
- 表达式求值:
case var testAge when ((testAge >= 90) & (testAge <=
99)) - 模式匹配:
case Planet p:
好吧,这很酷,但让我们探索一些更像 F# 语法的东西。
四种变体
我将带您了解我实现的四种变体,从第一个概念验证开始,到第四个“啊,这就是解决方案”结束。然后,我们将看看您可以用它做些什么其他有趣的事情。
第一步 -- 将模式和表达式添加到 Match 集合
我想从一些基本的东西开始,所以我实现了一个 Rule
类
public class Rule<T>
{
protected List<(Func<T, bool> qualifier,
Action<T> action)> matches = new List<(Func<T, bool> qualifier, Action<T> action)>();
public Rule<T> MatchOn(Func<T, bool> qualifier, Action<T> action)
{
matches.Add((qualifier, action));
return this;
}
public Rule<T> Match(T val)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}
return this;
}
}
它的用法如下
var rule = new Rule<int>()
.MatchOn(n => n == 0, _ => Console.WriteLine("Zero"))
.MatchOn(n => n == 1, _ => Console.WriteLine("One"))
.MatchOn(n => n == 2, _ => Console.WriteLine("Two"));
rule.Match(0);
输出当然是“Zero”。这真的不是我想要的,因为每次运行规则时我们都会创建一个集合(除非我们将规则保存在某个地方,呸),而且它很冗长。 但这是我们在 F# 中看到的整体 pattern -> expression
语法的概念验证。
第二步 - 使用扩展方法
我的第二次尝试摆脱了 Rule
类,但有一个主要缺陷 -- 即使找到匹配项,也会评估每个表达式。这是一个相当愚蠢的解决方案,但这就是它
public static class ExtensionMethods
{
public static (bool, T) Match<T>(this T item, Func<T, bool> qualifier, Action<T> action)
{
return (false, item).Match(qualifier, action);
}
public static (bool, T) Match<T>(this (bool hasMatch, T item) src,
Func<T, bool> qualifier, Action<T> action)
{
bool hasMatch = src.hasMatch;
if (!hasMatch)
{
hasMatch = qualifier(src.item);
if (hasMatch)
{
action(src.item);
}
}
return (hasMatch, src.item);
}
}
请注意每个表达式如何返回一个元组,指示是否找到匹配项以及源项。 使用元组是这些变体中唯一一致的事情。 几个用法示例
(false, 1)
.Match(n => n == 0, _ => Console.WriteLine("Zero"))
.Match(n => n == 1, _ => Console.WriteLine("One"))
.Match(n => n == 2, _ => Console.WriteLine("Two"));
2
.Match(n => n == 0, _ => Console.WriteLine("Zero"))
.Match(n => n == 1, _ => Console.WriteLine("One"))
.Match(n => n == 2, _ => Console.WriteLine("Two"));
这种方法的一个优点是我不再创建 pattern -> expression
情况的集合 -- 这些是“实时”评估的。
第三步 - 带有可变参数的静态方法
所以,然后,我回到了具有 static
方法的更简单的情况,这基本上就像一个扩展方法,只是这种形式有时更容易理解。 它看起来像这样
public static class Matcher<T>
{
public static void Match(T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}
}
}
并像这样使用
Matcher<int>.Match(3,
(n => n == 0, _ => Console.WriteLine("Zero")),
(n => n == 1, _ => Console.WriteLine("One")),
(n => n == 2, _ => Console.WriteLine("Two")),
(n => n == 3, _ => Console.WriteLine("Three")));
好的,现在我们开始进展了。 我们已经为每个 pattern -> expression
消除了 Match
方法名称,但我们仍然有类名、static
方法名称,甚至更糟糕的是,我们必须指定泛型参数类型。 所以,一步前进,两步后退。
第四步 - 再次拯救扩展方法
在这个版本中,我通过使用扩展方法来摆脱 static
类方法,但在这种情况下,扩展方法具有可变数量的参数
public static void Match<T>(this T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
break;
}
}
}
用法示例
2.Match(
(n => n == 0, _ => Console.WriteLine("Zero")),
(n => n == 1, _ => Console.WriteLine("One")),
(n => n == 2, _ => Console.WriteLine("Two")),
(n => n == 3, _ => Console.WriteLine("Three")),
(n => n == 4, _ => Console.WriteLine("Four")));
漂亮! 这是最接近 F# 语法的。
我们还能做什么?
MatchAll
这是您无法使用 F# match
或 C# switch
语句执行的操作:MatchAll
!
public static void MatchAll<T>(this T val, params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (match.qualifier(val))
{
match.action(val);
}
}
}
我们在这里所做的只是消除了 break
语句。 用法示例
10.ForEach(n => n.MatchAll(
(v => v % 2 == 0, v => Console.WriteLine($"{v} is even.")),
(v => v % 2 == 1, v => Console.WriteLine($"{v} is odd.")),
(_ => true, v => Console.WriteLine($"{v} * 10 = {v * 10}."))
));
我确信关于 10.ForEach
的尖叫声会爆发。 此外,如果您不熟悉“$
”表示法(我自己很少使用它),请查找 字符串插值。 无论如何,这是输出
0 is even.
0 * 10 = 0.
1 is odd.
1 * 10 = 10.
2 is even.
2 * 10 = 20.
3 is odd.
3 * 10 = 30.
4 is even.
4 * 10 = 40.
5 is odd.
5 * 10 = 50.
6 is even.
6 * 10 = 60.
7 is odd.
7 * 10 = 70.
8 is even.
8 * 10 = 80.
9 is odd.
9 * 10 = 90.
我个人觉得非常漂亮。
MatchAsync
这可能对需要一段时间才能执行的模式或表达式很有用,可能是一些 I/O 操作或 DB 查询
public async static void MatchAsync<T>(this T val,
params (Func<T, bool> qualifier, Action<T> action)[] matches)
{
foreach (var match in matches)
{
if (await Task.Run(() => match.qualifier(val)))
{
await Task.Run(() => match.action(val));
break;
}
}
}
一个简单(且奇怪)的例子
Console.WriteLine(DateTime.Now.ToString("hh:MM:ss"));
1000.MatchAsync(
(v => { Thread.Sleep(v); return true; }, _ => Console.WriteLine(DateTime.Now.ToString("hh:MM:ss")))
);
Console.WriteLine("Ready!");
Console.ReadLine();
输出正如您所期望的 -- 显示时间,由于任务正在运行,因此匹配失败,然后执行 async
操作的延续
08:05:09
Ready!
08:05:10
现在这非常有趣!
MatchReturn
正如 wkempf 在消息部分评论道:“在 F# 中,match
是一个表达式(它返回一个值)而不是一个语句”,我们可以实现一个返回值的 match
扩展方法
public static U MatchReturn<T, U>(this T val, params
(Func<T, bool> qualifier, Func<T, U> func)[] matches)
{
U ret = default(U);
foreach (var match in matches)
{
if (match.qualifier(val))
{
ret = match.func(val);
break;
}
}
return ret;
}
所有返回类型相同时的用法
string ret = 2.MatchReturn(
(n => n == 0, _ => "Zero"),
(n => n == 1, _ => "One"),
(n => n == 2, _ => "Two"),
(n => n == 3, _ => "Three"),
(n => n == 4, _ => "Four"));
Console.WriteLine(ret);
表达式返回不同类型时的用法
5.ForEach(q =>
{
dynamic retd = q.MatchReturn<int, dynamic>(
(n => n == 0, _ => "Zero"),
(n => n == 1, n => n),
(n => n == 2, n => new BigInteger(n)),
(n => n == 3, n => new Point(n, n)),
(n => n == 4, n => new Size(n, n)));
Console.WriteLine(retd.ToString());
});
输出
Zero
1
2
{X=3,Y=3}
{Width=4, Height=4}
由于无法推断返回类型,请注意我们必须显式提供指定输入类型和 dynamic
返回类型的泛型参数。
性能
关于性能问题,一个简单的性能测试器
DateTime start = DateTime.Now;
1000000.ForEach(q => (q % 5).MatchReturn(
(n => n == 0, _ => "Zero"),
(n => n == 1, _ => "One"),
(n => n == 2, _ => "Two"),
(n => n == 3, _ => "Three"),
(n => n == 4, _ => "Four")));
DateTime stop = DateTime.Now;
Console.WriteLine("MatchReturn for 1,000,000 runs took " + (stop - start).TotalMilliseconds + "ms");
string sret;
start = DateTime.Now;
1000000.ForEach(q =>
{
switch (q % 5)
{
case 0:
sret = "Zero";
break;
case 1:
sret = "One";
break;
case 2:
sret = "Two";
break;
case 3:
sret = "Three";
break;
case 4:
sret = "Four";
break;
}
});
stop = DateTime.Now;
Console.WriteLine("Switch for 1,000,000 runs took " + (stop - start).TotalMilliseconds + "ms");
显示与简单的数字 switch
相比,此实现的性能有多差(相差 22 倍)
MatchReturn for 1,000,000 runs took 224ms
Switch for 1,000,000 runs took 7.9983ms
结论
我个人发现,最初只是一个愚蠢的想法变得越来越有趣,特别是 MatchAll
和 MatchAsync
的实现,我发现在我正在开发的应用程序中已经很有用。 但是,是的,我确实在狂野的一面行走。