158

我最近一直在研究 F#,虽然我不太可能很快跳过栅栏,但它肯定突出了 C#(或库支持)可以使生活更轻松的一些领域。

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

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

虽然 C# 最终借用 [ahem] 一些丰富性会很可爱,但在此期间,我一直在研究在运行时可以做什么 - 例如,很容易将一些对象组合在一起以允许:

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 "x To y 相当的 InRange(...) 匹配“ 用法。

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

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

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

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

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

4

10 回答 10

37

在尝试在 C# 中做这样的“功能性”事情(甚至尝试写一本关于它的书)之后,我得出的结论是,不,除了少数例外,这样的事情并没有太大帮助。

主要原因是诸如 F# 之类的语言从真正支持这些功能中获得了很大的力量。不是“你能做到”,而是“很简单,很清楚,在意料之中”。

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

“问题”是一旦你开始使用一些函数概念,很自然地想要继续。然而,在 C# 中利用元组、函数、部分方法应用和柯里化、模式匹配、嵌套函数、泛型、monad 支持等变得非常难看,非常快。很好玩,一些很聪明的人用C#做了一些很酷的事情,但实际使用起来感觉很重。

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

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

** 但请注意:缺乏自动泛化和类型推断确实阻碍了这些功能的使用。**

正如其他人所提到的,所有这一切都表明,在一个小团队中,出于特定目的,是的,如果你被 C# 卡住了,也许他们可以提供帮助。但根据我的经验,他们通常觉得麻烦多于他们的价值——YMMV。

其他一些链接:

于 2008-10-12T11:38:24.843 回答
25

可以说 C# 不能简单地打开类型的原因是因为它主要是一种面向对象的语言,而在面向对象的术语中这样做的“正确”方法是在 Vehicle 上定义一个 GetRentPrice 方法和在派生类中覆盖它。

就是说,我花了一些时间使用具有这种功能的多范式和函数式语言,例如 F# 和 Haskell,并且我遇到过许多以前有用的地方(例如,当您没有编写您需要打开的类型,因此您无法在它们上实现虚拟方法),这是我欢迎与有区别的联合一起使用该语言的东西。

[编辑:删除了关于性能的部分,因为 Marc 表示它可能会短路]

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

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

于 2008-10-01T07:59:57.430 回答
23

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

我不知道这是否可能成为 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
于 2008-10-01T07:26:01.347 回答
23

在 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));
}
于 2017-12-13T14:18:13.207 回答
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));
        }
    }
于 2011-09-04T20:22:40.137 回答
9

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

多范式语言设计没有错,相反,在 C# 中有 lambdas 非常好,而且 Haskell 可以对例如 IO 做命令式的东西。但这不是一个非常优雅的解决方案,不是 Haskell 时尚。

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

我的观点是,是什么让模式匹配与语言设计和数据模型相关联。话虽如此,我不认为模式匹配是 C# 的有用特性,因为它不能解决典型的 C# 问题,也不适合命令式编程范式。

于 2009-08-13T07:29:01.857 回答
5

在我看来,做这些事情的面向对象的方式是访问者模式。您的访问者成员方法只是充当案例构造,您让语言本身处理适当的调度,而不必“窥视”类型。

于 2009-06-16T07:05:34.040 回答
4

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

于 2008-10-01T07:13:46.400 回答
3

需要注意的一件事:C# 编译器非常擅长优化 switch 语句。不仅仅是为了短路——你会得到完全不同的 IL,这取决于你有多少案例等等。

您的具体示例确实做了一些我认为非常有用的事情 - 没有与按类型区分的语法等效,因为(例如)typeof(Motorcycle)不是常量。

这在动态应用程序中变得更加有趣——您的逻辑可以很容易地由数据驱动,提供“规则引擎”风格的执行。

于 2008-10-01T09:16:07.557 回答
1

您可以使用我编写的名为OneOf的库来实现您的目标

switch(and ifand )的主要优点exceptions 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

于 2017-09-13T15:30:29.357 回答