идея переключения / сопоставления с образцом - PullRequest
146 голосов
/ 01 октября 2008

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

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

[примечание - возможно Switch / Case здесь неправильные термины ... но это показывает идею]

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

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

Обратите внимание, что я играл с 3 вариантами выше:

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

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

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

Ответы [ 11 ]

82 голосов
/ 01 октября 2008

Отличный блог Барта Де Смета состоит из 8 частей, посвященных тому, как именно вы описываете. Найдите первую часть здесь .

37 голосов
/ 12 октября 2008

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

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

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

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

То, что я часто использовал (в разных проектах) в C #:

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

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

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

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

25 голосов
/ 01 октября 2008

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

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

[Редактировать: Удалена часть, касающаяся производительности, поскольку Марк указал, что она может быть закорочена]

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

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

22 голосов
/ 01 октября 2008

Я не думаю, что такого рода библиотеки (которые действуют как языковые расширения), вероятно, получат широкое признание, но с ними интересно играть, и они могут быть действительно полезны для небольших команд, работающих в определенных областях, где это полезно , Например, если вы пишете тонны «бизнес-правил / логики», которые выполняют тесты произвольного типа, как этот, и тому подобное, я могу видеть, как это было бы удобно.

Я понятия не имею, может ли это когда-либо быть особенностью языка 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
21 голосов
/ 13 декабря 2017

Я знаю, что это старая тема, но в 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));
}
13 голосов
/ 05 сентября 2011

Чтобы ответить на ваш вопрос, да, я думаю, что синтаксические конструкции сопоставления с образцом полезны. Я хотел бы видеть синтаксическую поддержку в 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 голосов
/ 13 августа 2009

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

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

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

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

5 голосов
/ 16 июня 2009

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

4 голосов
/ 01 октября 2008

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

3 голосов
/ 01 октября 2008

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

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

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

...