65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.89/5 (43投票s)

2018 年 5 月 3 日

CPOL

4分钟阅读

viewsIcon

84676

downloadIcon

315

使用元组和扩展方法实现 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

结论

我个人发现,最初只是一个愚蠢的想法变得越来越有趣,特别是 MatchAllMatchAsync 的实现,我发现在我正在开发的应用程序中已经很有用。 但是,是的,我确实在狂野的一面行走。

© . All rights reserved.