2008-10-01 2 views
137

Я недавно смотрел F #, и, хотя я вряд ли скоро скачу забор, он определенно выделяет некоторые области, где C# (или поддержка библиотеки) может облегчить жизнь.switch/pattern matching idea

В частности, я думаю о возможности сопоставления шаблонов F #, что позволяет использовать очень богатый синтаксис - гораздо более выразительный, чем текущий эквивалент C# или условный эквивалент 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, внутр >.

[Примечание - может быть, Переключитесь/Дело здесь неправильные термины ... но он показывает идею]

Для меня, это намного чище, чем эквивалент с использованием повторена, если/другое, или композитный тройная условный (который становится очень грязным для нетривиальных выражений - скобки в изобилии). Он также избегает лота литья и позволяет простое расширение (прямо или через методы расширения) для более конкретных совпадений, например сравнение InRange (...), сопоставимое с выбором VB ... Case x Для использования ".

Я просто пытаюсь оценить, думают ли люди, что существует много преимуществ от конструкций, подобных приведенным выше (при отсутствии поддержки языка)?

Примечание дополнительно, что я играл с 3-мя варианты выше:

  • Func < TSource, TValue > версии для оценки - сравнимая с композитными трехкомпонентными условными операторами
  • действия, <TSource> версия - сопоставима с if/else if/else if/else if/else
  • a Выражение < Func < TSource, TValue > > версия - как первый, но использоваться произвольными поставщиками LINQ

Кроме того, с помощью Expression на основе версии позволяет Expression-дерево переписывание, по существу, встраивание всех ветвей в единое составное условное выражение, а не при помощи повторным призывание. Я недавно не проверял, но в некоторых ранних сборках Entity Framework я, похоже, напоминаю, что это необходимо, так как это не очень понравилось InvocationExpression. Он также позволяет более эффективно использовать LINQ-to-Objects, поскольку он избегает повторных вызовов делегатов - тесты показывают совпадение, подобное приведенному выше (с использованием формы выражения), выполняющееся с одинаковой скоростью [на самом деле быстрее] по сравнению с эквивалентным C# составной условный оператор.Для полноты версия Func <...> взяла в 4 раза длиннее условного оператора C#, но все еще очень быстро и вряд ли станет основным узким местом в большинстве случаев использования.

Я приветствую любые мысли/ввод/критику/и т. Д. Выше (или о возможностях более богатой поддержки языка C# ... здесь надеется; -p).

+10

Вы можете использовать VB .NET, который поддерживает это в своем заявлении о выборе. Ик! – 2008-11-12 08:01:48

+1

Мне нравится эта идея, и она делает очень приятную и более гибкую форму коммутационного футляра; однако, разве это не действительно украшенный способ использования синтаксиса Linq, как if-then wrapper? Я бы не хотел, чтобы кто-то использовал это вместо реальной сделки, т. Е. Заявление `switch-case`. Не поймите меня неправильно, я думаю, что это место, и я, вероятно, буду искать способ реализации. – IAbstract 2010-12-18 15:44:10

+0

Если вы пишете нечто большее, чем привет-мир с активными шаблонами (F # 3.0), я думаю, это подтолкнет вас к краю ... – 2014-07-11 10:50:06

ответ

4

Я знаю, это старая тема, но в 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)); 
} 
4

Хотя для включения типа не очень «C-sharpey», я знаю, что конструкция будет очень полезной в общем использовании - у меня есть хотя бы один личный проект, который мог бы использовать его (хотя его управляемый банкомат). Есть ли много проблем с компиляцией производительности, с повторным написанием дерева выражений?

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 
25

Вероятно, причина того, что C# не делает его простым для включения типа это потому, что это прежде всего объектно-ориентированный язык, а «правильный» способ сделать это в объектно-ориентированных терминах - это определить метод GetRentPrice на транспортном средстве и переопределить его в производных классах.

Это говорит о том, что я провел немного времени, играя с мультипарадигмами и функциональными языками, такими как F # и Haskell, которые обладают такими возможностями, и я столкнулся с рядом мест, где это было бы полезно до (например, когда вы не пишете типы, которые вам нужно включить, чтобы вы не могли реализовать на них виртуальный метод), и это то, что я приветствую на языке вместе с дискриминационными союзами.

[Edit: Убрана часть о производительности, как Marc указала, что она может быть закорочены]

Другой потенциальной проблемой является удобство использования один - это ясно из заключительного вызова, что происходит, если матч не соответствует какой-либо условий, но каково поведение, если оно соответствует двум или более условиям? Должно ли это исключение? Должен ли он вернуть первый или последний матч?

Способ, который я обычно использую для решения этой проблемы, заключается в использовании поля словаря с типом в качестве ключа и лямбда в качестве значения, которое довольно сложно построить с использованием синтаксиса инициализатора объекта; однако это только учет конкретного типа и не допускает дополнительных предикатов, поэтому не может быть подходящим для более сложных случаев. [Замечание: если вы посмотрите на результат компилятора C#, он часто конвертирует операторы switch в таблицы перехода на словаря, поэтому, похоже, нет веской причины, по которым он не может поддерживать типы включения]

82

Bart De Smet's excellent blog содержит 8 частей, посвященных тому, что вы описываете. Найти первую часть here.

3

Я думаю, что это выглядит действительно интересно (+1), но нужно быть осторожным: компилятор C# довольно хорош в оптимизации операторов switch.Не только для короткого замыкания - вы получаете совершенно разные ИЛ в зависимости от того, сколько случаев у вас есть и так далее.

Ваш конкретный пример действительно делает что-то, что я нашел бы очень полезным - синтаксис не эквивалентен случаю по типу, так как (например) typeof(Motorcycle) не является константой.

Это становится более интересным в динамическом приложении - ваша логика здесь может быть легко управляемой данными, предоставляя стиль стиля «правила-двигатель».

37

После попытки сделать такие «функциональные» вещи на C# (и даже попытаться книгу на нем), я пришел к выводу, что нет, за некоторыми исключениями, таких вещей не слишком помогает.

Основная причина в том, что такие языки, как F #, получают большую часть своей силы от подлинной поддержки этих функций. Не «вы можете это сделать», но «это просто, ясно, это ожидается».

Например, при сопоставлении с образцом вы получаете компилятор, сообщающий вам, есть ли неполное совпадение или когда другой матч никогда не будет удален. Это менее полезно для открытых типов, но при сопоставлении с дискриминационным союзом или кортежами это очень изящно. В F # вы ожидаете, что люди будут сопоставлять шаблон, и это мгновенно имеет смысл.

«Проблема» в том, что после того, как вы начнете использовать некоторые функциональные концепции, естественно продолжать работу. Однако использование кортежей, функций, частичного применения метода и каррирования, сопоставления шаблонов, вложенных функций, дженериков, поддержки монады и т. Д. В C# получает очень уродливый, очень быстро. Это весело, и некоторые очень умные люди сделали очень классные вещи в C#, но на самом деле с использованием чувствуют себя тяжело.

То, что я закончил с использованием часто (по-проектов) в C#: функции

  • Sequence, с помощью методов расширения для IEnumerable. Такие вещи, как ForEach или Process («Применить»? - выполнять действие над элементом последовательности, как оно перечислено) вставляются, потому что синтаксис C# поддерживает его хорошо.
  • Абстрактные общие шаблоны утверждения. Сложные блоки try/catch/finally или другие задействованные (часто сильно генерируемые) кодовые блоки. Расширение LINQ-to-SQL подходит и здесь.
  • Кортежи, в некоторой степени.

** Но обратите внимание: отсутствие автоматического обобщения и вывода типа действительно препятствует использованию даже этих функций. **

Все это сказало, как кто-то еще упомянул, в небольшой команде, для определенной цели, да, возможно, они могут помочь, если вы застряли с C#. Но, по моему опыту, они обычно чувствовали себя более хлопотными, чем они стоили - YMMV.

Некоторые другие ссылки:

5

ИМХО OO способ делать такие вещи, является образцом для посетителей. Методы вашего посетителя просто действуют как конструкторы case, и вы позволяете самому языку обрабатывать соответствующую отправку, не «заглядывая» в типы.

9

Соответствие шаблону (как описано here), его целью является деконструировать значения в соответствии с их спецификацией типа. Однако концепция класса (или типа) в C# не согласуется с вами.

В документе с несколькими парадигмами отмечается неправильное оформление, напротив, очень хорошо иметь лямбды в C#, и Haskell может делать императивные вещи, например. IO. Но это не очень элегантное решение, а не мода Haskell.

Но так как последовательные языки программирования программирования могут быть поняты с точки зрения лямбда-исчисления, а C# очень хорошо вписывается в параметры последовательного процедурного языка, это хорошо подходит. Но, взяв что-то из чистого функционального контекста, скажем, Haskell, а затем помещая эту особенность в язык, который не является чистым, хорошо, делая именно это, не гарантирует лучшего результата.

Мое мнение таково, что делает привязку к шаблону привязанной к языку дизайна и модели данных. Сказав это, я не считаю, что сопоставление образцов является полезной функцией C#, поскольку оно не решает типичных проблем C# и не подходит в рамках парадигмы императивного программирования.

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

Вы можете добиться того, что вы после этого с помощью используя библиотеку, которую я написал, под названием OneOf

Главное преимущество перед switchif и 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