关于c#:开关/模式匹配的想法

switch / pattern matching idea

我最近一直在关注F,虽然我不太可能很快就跨越这个障碍,但它肯定突出了一些C(或图书馆支持)可以让生活更轻松的领域。

特别是,我在考虑F的模式匹配能力,它允许非常丰富的语法-比当前的开关/条件C等价物更具表现力。我不会试图给出一个直接的例子(我的F做不到),但简而言之,它允许:

  • 按类型匹配(对区分的联合进行完全覆盖检查)[注意,这还推断绑定变量的类型,提供成员访问等]
  • 按谓词匹配
  • 以上的组合(可能还有一些我不知道的其他场景)

虽然C最终借用这些丰富的内容是很有意思的,但在此期间,我一直在研究在运行时可以做些什么——例如,将一些对象组合在一起相当容易:

1
2
3
4
5
6
var getRentPrice = new Switch<Vehicle, int>()
        .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) //"bike" here is typed as Motorcycle
        .Case<Bicycle>(30) // returns a constant
        .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
        .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
        .ElseThrow(); // or could use a Default(...) terminator

其中getRentPrice是func

[注-可能是交换/案例这是错误的术语…但它表明了这个想法]

对我来说,这比使用重复if/else或复合三元条件(对于非平凡表达式来说非常混乱,括号太多)的等价方法要清楚得多。它还避免了大量的强制转换,并允许简单的扩展(直接或通过扩展方法)到更具体的匹配,例如与vb select…case"x to y"用法类似的inrange(…)匹配。

我只是想评估一下人们是否认为像上面这样的结构有很多好处(在缺乏语言支持的情况下)?

另外,请注意,我一直在玩上述3种变体:

  • 用于评估的func版本-与复合三元条件语句相当
  • 与if/else if/else if/else if/else if/else if/else相当的操作
  • 表达式>version-作为第一个,但可由任意LINQ提供程序使用

此外,使用基于表达式的版本可以重新编写表达式树,本质上是将所有分支嵌入到单个复合条件表达式中,而不是使用重复调用。我最近没有检查过,但是在一些早期的实体框架构建中,我似乎记得这是必要的,因为它不太喜欢invocationExpression。它还允许对linq-to对象更有效地使用,因为它避免了重复的委托调用-与等效的C复合条件语句相比,测试显示类似于上面的匹配(使用表达式形式)以相同的速度执行(实际上速度略快)。出于完整性考虑,基于func的版本花费了c条件语句4倍的时间,但仍然非常快速,在大多数用例中不太可能成为主要的瓶颈。

我欢迎任何关于以上(或更丰富的C语言支持的可能性)的想法/输入/评论/等。希望在这里;-p)。


巴特·德·斯密特的优秀博客有一个8部分的系列,讲述了你所描述的事情。在这里找到第一部分。


在试着用C语言做这种"功能性"的事情之后(甚至试着写一本关于它的书),我得出结论:不,除了少数例外,这样的事情没有太多帮助。

主要原因是,像f这样的语言从真正支持这些特性中获得了很大的能力。不是"你能做到",而是"很简单,很清楚,这是意料之中的"。

例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配,或者何时将永远不会命中另一个匹配。这对于开放式类型不太有用,但是在匹配有区别的并集或元组时,它非常漂亮。在F中,您期望人们进行模式匹配,这一点立即变得有意义。

"问题"是,一旦你开始使用一些功能概念,你自然会想继续下去。然而,利用C中的元组、函数、部分方法应用和转换、模式匹配、嵌套函数、泛型、monad支持等,会很快变得非常难看。这很有趣,一些非常聪明的人用C语言做了一些非常酷的事情,但实际上使用它感觉很重。

我经常(跨项目)在C中使用的内容:

  • 序列函数,通过IEnumerable的扩展方法。例如foreach或process("apply"?--对序列项执行操作,因为C语法很好地支持它。
  • 抽象公共语句模式。复杂的try/catch/finally块或其他涉及的(通常是非常通用的)代码块。将Linq扩展到SQL也适用于这里。
  • 在某种程度上,元组。

但请注意:缺少自动泛化和类型推断确实会妨碍这些特性的使用。**

正如其他人提到的,在一个小团队中,为了一个特定的目的,所有这些都说了,是的,如果你被C困住了,也许他们能帮上忙。但根据我的经验,他们通常觉得比自己的价值更麻烦。

其他一些链接:

  • Mono.Rocks游乐场有许多类似的东西(以及非功能性编程,但有用的补充)。
  • Luca Bolognese的功能C库
  • Matthew Podwysocki在msdn上的功能C


可以说,C不能简单地打开类型是因为它主要是一种面向对象的语言,在面向对象的术语中,正确的方法是在车辆上定义一个getRentPrice方法,并在派生类中覆盖它。

也就是说,我花了一点时间在多范式和功能语言上玩,比如F和haskell,它们具有这种能力,我遇到过许多以前有用的地方(例如,当你不写你需要打开的类型,所以你不能在它们上实现一个虚拟方法),这是很重要的。我欢迎加入这门语言以及歧视工会。

[编辑:删除了Marc表示可能短路的性能部分]

另一个潜在的问题是可用性问题——从最后一次调用中可以清楚地看到,如果匹配不满足任何条件,会发生什么情况,但是如果匹配两个或更多条件,会发生什么行为?它应该引发异常吗?它应该返回第一个匹配还是最后一个匹配?

我解决此类问题的一种方法是使用一个字典字段,其中类型为键,lambda为值,这对于使用对象初始值设定项语法构造非常简单;但是,这只说明具体的类型,不允许附加谓词,因此可能不适用于更复杂的情况。[旁注-如果您查看C编译器的输出,它经常将switch语句转换为基于字典的跳转表,因此似乎没有很好的理由不支持切换类型]


我不认为这些类型的库(类似于语言扩展)有可能获得广泛的接受,但是它们很有趣,对于在特定领域工作的小团队来说非常有用。例如,如果您正在编写大量的"业务规则/逻辑",这些规则/逻辑执行像这样或那样的任意类型测试,那么我可以看到它是如何方便的。

我不知道这是否可能是一个C语言功能(似乎有疑问,但谁能看到未来?).

作为参考,相应的f约为:

1
2
3
4
5
6
7
let getRentPrice (v : Vehicle) =
    match v with
    | :? Motorcycle as bike -> 100 + bike.Cylinders * 10
    | :? Bicycle -> 30
    | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20
    | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20
    | _ -> failwith"blah"

假设您定义了一个沿着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Vehicle() = class end

type Motorcycle(cyl : int) =
    inherit Vehicle()
    member this.Cylinders = cyl

type Bicycle() = inherit Vehicle()

type EngineType = Diesel | Gasoline

type Car(engType : EngineType, doors : int) =
    inherit Vehicle()
    member this.EngineType = engType
    member this.Doors = doors


我知道这是一个古老的话题,但在C 7中,你可以做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch(shape)
{
    case Circle c:
        WriteLine($"circle with radius {c.Radius}");
        break;
    case Rectangle s when (s.Length == s.Height):
        WriteLine($"{s.Length} x {s.Height} square");
        break;
    case Rectangle r:
        WriteLine($"{r.Length} x {r.Height} rectangle");
        break;
    default:
        WriteLine("<unknown shape>");
        break;
    case null:
        throw new ArgumentNullException(nameof(shape));
}


为了回答你的问题,是的,我认为模式匹配句法结构是有用的。我个人希望在C中看到对它的语法支持。

下面是我对一个类的实现,它提供(几乎)与您描述的相同的语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class PatternMatcher<Output>
{
    List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>();

    public PatternMatcher() { }        

    public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function)
    {
        cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function));
        return this;
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function)
    {
        return Case(
            o => o is T && condition((T)o),
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Func<T, Output> function)
    {
        return Case(
            o => o is T,
            o => function((T)o));
    }

    public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o)
    {
        return Case(condition, x => o);
    }

    public PatternMatcher<Output> Case<T>(Output o)
    {
        return Case<T>(x => o);
    }

    public PatternMatcher<Output> Default(Func<Object, Output> function)
    {
        return Case(o => true, function);
    }

    public PatternMatcher<Output> Default(Output o)
    {
        return Default(x => o);
    }

    public Output Match(Object o)
    {
        foreach (var tuple in cases)
            if (tuple.Item1(o))
                return tuple.Item2(o);
        throw new Exception("Failed to match");
    }
}

以下是一些测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
    public enum EngineType
    {
        Diesel,
        Gasoline
    }

    public class Bicycle
    {
        public int Cylinders;
    }

    public class Car
    {
        public EngineType EngineType;
        public int Doors;
    }

    public class MotorCycle
    {
        public int Cylinders;
    }

    public void Run()
    {
        var getRentPrice = new PatternMatcher<int>()
            .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10)
            .Case<Bicycle>(30)
            .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20)
            .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20)
            .Default(0);

        var vehicles = new object[] {
            new Car { EngineType = EngineType.Diesel, Doors = 2 },
            new Car { EngineType = EngineType.Diesel, Doors = 4 },
            new Car { EngineType = EngineType.Gasoline, Doors = 3 },
            new Car { EngineType = EngineType.Gasoline, Doors = 5 },
            new Bicycle(),
            new MotorCycle { Cylinders = 2 },
            new MotorCycle { Cylinders = 3 },
        };

        foreach (var v in vehicles)
        {
            Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v));
        }
    }


模式匹配(如本文所述)的目的是根据类型规范解构值。但是,C中的类(或类型)的概念与您不一致。

多范式语言设计没有错,相反,使用C语言的lambdas非常好,haskell可以做一些必要的事情,例如io。但这不是一个非常优雅的解决方案,不是哈斯克尔的风格。

但是,由于顺序程序编程语言可以用lambda微积分来理解,而且C恰好适合顺序程序语言的参数,所以它是一种很好的适合。但是,从say haskell的纯功能上下文中提取一些东西,然后将该特性放入一种不纯粹的语言中,那么,仅仅这样做并不能保证更好的结果。

我的观点是,使模式匹配勾选的是语言设计和数据模型。尽管如此,我不认为模式匹配是C的一个有用特性,因为它不能解决典型的C问题,也不能很好地适应命令式编程范式。


我不知道这样做的方式是访客模式。您的访问者成员方法只是充当case结构,您可以让语言本身处理适当的分派,而不必"偷看"类型。


虽然打开类型不是很"c-sharpey",但我知道构造在一般使用中非常有用-我至少有一个个人项目可以使用它(尽管它是可管理的ATM)。在表达式树重新编写的情况下,是否存在许多编译性能问题?


我觉得这看起来很有趣(+1),但有一点要小心:C编译器非常擅长优化switch语句。不仅仅是短路-你会得到完全不同的IL,这取决于你有多少个案例等等。

您的具体示例确实做了一些我会发现非常有用的事情——没有与逐类型的大小写等价的语法,因为(例如)typeof(Motorcycle)不是常量。

这在动态应用程序中变得更有趣——这里的逻辑可以很容易地由数据驱动,从而实现"规则引擎"风格的执行。


你可以通过使用我写的一个图书馆,叫做

switchifexceptions as control flow相比,它的主要优点是编译时安全—没有默认的处理程序或失败

1
2
3
4
5
6
7
8
9
10
   OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types
   var getRentPrice = vehicle
        .Match(
            bike => 100 + bike.Cylinders * 10, //"bike" here is typed as Motorcycle
            bike => 30, // returns a constant
            car => car.EngineType.Match(
                diesel => 220 + car.Doors * 20
                petrol => 200 + car.Doors * 20
            )
        );

它在nuget上,目标是net451和netstandard1.6