开关/模式匹配的想法


151

我最近一直在研究F#,虽然我不太可能很快就克服障碍,但它无疑突出了C#(或库支持)可以使生活更轻松的某些领域。

特别是,我正在考虑F#的模式匹配功能,该功能允许使用非常丰富的语法-比当前的switch / conditional C#等效项更具表现力。我不会尝试举一个直接的例子(我的F#不符合要求),但总而言之,它允许:

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

虽然C#最终会借用其中的一些功能很不错,但在此期间,我一直在研究可以在运行时完成的工作-例如,将某些对象组合在一起以允许以下操作相当容易:

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 <Vehicle,int>。

[注意-也许这里的Switch / Case是错误的术语...但是它表明了这个主意]

对我来说,这比使用重复的if / else或复合三元条件(对于非平凡的表达式会变得非常混乱-方括号)更加清楚。它还避免了很多转换,并允许简单扩展(直接扩展或通过扩展方法)到更具体的匹配项,例如,与VB Select ... Case相当的InRange(...)匹配项。 ”的用法。

我只是在尝试衡量人们是否认为上述结构(在没有语言支持的情况下)有很多好处?

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

  • 用于评估的Func <TSource,TValue>版本-与复合三元条件语句相当
  • 一个Action <TSource>版本-与if / else if / else if / else if / else相当
  • 一个Expression <Func <TSource,TValue >>版本-作为第一个版本,但可由任意LINQ提供程序使用

此外,使用基于表达式的版本可以重写表达式树,实质上将所有分支内联到单个复合条件表达式中,而不是使用重复调用。我最近没有检查过,但是在某些早期的Entity Framework构建中,我似乎想起来这是必要的,因为它不太喜欢InvocationExpression。由于它避免了重复的委托调用,因此还可以更高效地使用LINQ-to-Objects-测试显示,与上述C#相比,上述匹配(使用Expression形式)以相同的速度(实际上要快得多)执行匹配复合条件语句。为了完整起见,基于Func <...>的版本花费的时间是C#条件语句的四倍,但仍然非常快,并且在大多数用例中不太可能成为主要瓶颈。

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


“我只是在试图衡量人们是否认为上述结构(在没有语言支持的情况下)有很多好处?” 恕我直言,是的。还不存在类似的东西吗?如果没有,鼓励写一个轻量级的库。
康拉德·鲁道夫

10
您可以使用VB .NET在其select case语句中支持此功能。ek!
Jim Burger

我还将嘟嘟响自己的号角,并添加到我的库的链接:Functional-dotnet
Alexey Romanov

1
我喜欢这个想法,它为开关盒提供了一种非常不错且更灵活的形式。但是,这不是将类似于Linq的语法用作if-then包装程序的一种巧妙方法吗?我不鼓励有人用它代替实际交易,即switch-case声明。不要误会我的意思,我认为它已经存在,并且我可能会寻找一种实现方法。
IAbstract 2010年

2
尽管这个问题已有两年多的历史了,但值得一提的是,具有模式匹配功能的C#7即将问世。
Abion47年

Answers:


22

我知道这是一个老话题,但是在c#7中,您可以执行以下操作:

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#和F#之间的显着区别是模式匹配的完整性。模式匹配涵盖了所有可能的情况(已充分描述),如果没有,则来自编译器的警告。尽管您可以正确地说默认情况可以做到这一点,但实际上它通常也是运行时异常。
VoronoiPotato

37

尝试在C#中完成此类“功能性”事情(甚至尝试编写一本书)之后,我得出的结论是,除了少数例外,此类事情没有太大帮助。

主要原因是诸如F#之类的语言通过真正支持这些功能而获得了强大的功能。不是“您可以做到”,而是“这很简单,很明显,很期待”。

例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配,或者何时永远不会再匹配其他匹配。这对于开放式类型不太有用,但是当与已区分的联合或元组匹配时,它非常漂亮。在F#中,您希望人们进行模式匹配,并且这立即有意义。

“问题”是,一旦您开始使用某些功能概念,就自然会继续。但是,在C#中利用元组,函数,部分方法应用程序和currying,模式匹配,嵌套函数,泛型,monad支持等变得非常丑陋,非常迅速。这很有趣,并且一些非常聪明的人在C#中做了一些很酷的事情,但是实际上使用它感觉很沉重。

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

  • 序列函数,通过IEnumerable的扩展方法。诸如ForEach或Process(“ Apply”?–对序列项进行操作)之类的东西很适合,因为C#语法很好地支持了它。
  • 抽象常见的语句模式。复杂的try / catch / finally块或其他涉及的(通常是非常通用的)代码块。扩展LINQ-to-SQL也适用于此。
  • 元组,在某种程度上。

**但请注意:缺乏自动归纳和类型推断确实阻碍了甚至使用这些功能。**

正如其他人所提到的,所有这些都是在一个小型团队中,出于特定的目的,是的,如果您对C#有所了解,他们可能会提供帮助。但是以我的经验,他们通常觉得比他们值得的麻烦更多-YMMV。

其他一些链接:


25

可以说,C#不能简单地打开类型的原因是因为它主要是一种面向对象的语言,而用面向对象的术语来做到这一点的“正确”方法是在Vehicle和在派生类中重写它。

就是说,我花了一些时间来研究具有这种功能的多范式和功能性语言,例如F#和Haskell,并且在很多地方,我以前都曾使用过它(例如,当您并没有编写您需要打开的类型,因此您无法在它们上实现虚拟方法),这是我所欢迎的语言以及可区分的联合。

[编辑:删除了有关性能的部分,如马克所指出的可能短路]

另一个潜在的问题是可用性问题-从最终调用中可以清楚看出,如果比赛不符合任何条件,会发生什么,但是如果匹配两个或多个条件,会发生什么行为?它应该引发异常吗?它应该返回第一个还是最后一个比赛?

我倾向于用来解决此类问题的一种方法是使用字典字段,其类型作为键,而lambda作为值,使用对象初始值设定项语法构造起来非常简洁;但是,这仅考虑具体类型,不允许附加谓词,因此可能不适用于更复杂的情况。[附带说明-如果您查看C#编译器的输出,它经常将switch语句转换为基于字典的跳转表,因此似乎没有充分的理由表明它不支持打开类型]


1
实际上-我拥有的版本在代理版本和表达式版本中都存在短路。表达式版本编译为复合条件;委托版本只是一组谓词和func / actions-一旦匹配,它将停止。
马克·格雷夫

有趣的是-从粗略的外观看,我认为它必须至少对每个条件进行基本检查,因为它看起来像方法链,但是现在我意识到这些方法实际上是在链接对象实例以构建它,因此您可以执行此操作。我将编辑答案以删除该声明。
格雷格·比奇

22

我认为这类库(起语言扩展的作用)可能不会获得广泛的接受,但是它们很有趣,并且对于在特定领域工作的小型团队确实很有用。例如,如果您编写大量的“业务规则/逻辑”来进行诸如此类的任意类型测试,那么我会发现它很方便。

我不知道这是否有可能是C#语言的功能(似乎值得怀疑,但是谁能看到未来?)。

作为参考,相应的F#近似为:

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"

假设您已经沿着

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

2
感谢您的F#版本。我想我喜欢F#的处理方式,但目前不确定(整体)F#是正确的选择,所以我必须走中间路...
Marc Gravell

13

要回答您的问题,是的,我认为模式匹配语法构造很有用。我希望看到C#的语法支持。

这是我提供的(几乎)与您描述的语法相同的类的实现

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");
    }
}

这是一些测试代码:

    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));
        }
    }

9

模式匹配(如所描述的在这里),其目的是根据其类型规范来解构值。但是,C#中的类(或类型)的概念与您不同意。

相反,多范例语言设计存在错误,相反,在C#中使用lambda很好,Haskell可以对IO做必要的事情。但这不是一个非常优雅的解决方案,不是Haskell的方式。

但是,由于可以使用lambda演算来理解顺序过程编程语言,并且C#恰好适合顺序过程语言的参数,因此非常适合。但是,从Haskell的纯函数上下文中获取一些信息,然后将该功能放入一种不纯净的语言中,那么,这样做不能保证获得更好的结果。

我的意思是,使模式匹配刻度线与语言设计和数据模型相关联。话虽如此,我不认为模式匹配是C#的有用功能,因为它不能解决典型的C#问题,也不能很好地适应命令式编程范例。


1
也许。确实,我将很难想到一个令人信服的“杀手级”论据来说明为什么需要它(而不是“在某些极端情况下可能会很好,但会使语言变得更加复杂”)。
Marc Gravell

5

恕我直言,OO做这种事情的方式是访客模式。您的访问者成员方法只是充当大小写构造,您可以让语言本身处理适当的调度,而不必“窥视”类型。


4

尽管打开类型不是很“ C尖锐”,但我知道该构造在一般使用中会很有帮助-我至少有一个可以使用它的个人项目(尽管它是可管理的ATM)。用表达式树重写会有很多编译性能问题吗?


如果您缓存对象以供重用则不是这样(这在很大程度上是C#lambda表达式的工作方式,只是编译器会隐藏代码)。重写肯定会提高编译性能-但是,对于常规使用(而不是LINQ-to-Something),我希望委托版本可能会更有用。
马克·格雷夫

还要注意-它不一定是类型的开关-它也可以用作复合条件式(甚至通过LINQ)-但没有凌乱的x => Test?结果1 :(测试2?结果2 :(测试3?结果3:结果4))
马克·

很高兴知道,尽管我的意思是实际编译的性能:csc.exe需要多长时间-我对C#不够熟悉,无法确定这是否真的是一个问题,但这对C ++来说是个大问题。
西蒙·布坎

CSC都不会眨一下这-它是如此的相似,LINQ是如何工作的,以及C#3.0编译器是在LINQ /扩展方法等相当不错的
马克·Gravell

3

我认为这看起来确实很有趣(+1),但要注意的一件事:C#编译器非常擅长优化switch语句。不仅是为了短路-根据所遇到的情况,您将获得完全不同的IL,依此类推。

您的特定示例做了一些我认为非常有用的事情-没有语法等效于按类型区分的情况,例如(例如) typeof(Motorcycle)不是常量。

这在动态应用程序中变得更加有趣-您的逻辑可以轻松地由数据驱动,从而提供“规则引擎”样式的执行。


0

您可以使用我写的一个库来实现您所追求的目标 OneOf目标

switch(和ifexceptions as control flow)相比的主要优点是它在编译时安全-没有默认处理程序或失败

   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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.