идея переключения / сопоставления с образцом

Я недавно смотрел на 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 <Vehicle, int>.

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

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

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

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

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

1.10.2008 06:34:13
«Я просто пытаюсь оценить, считают ли люди, что конструкции, подобные приведенным выше, приносят большую пользу (при отсутствии языковой поддержки)?» ИМХО, да. Разве что-то подобное уже не существует? Если нет, то рекомендуется написать облегченную библиотеку.
Konrad Rudolph 1.10.2008 09:13:12
Вы можете использовать VB .NET, который поддерживает это в своем операторе select case. Ик!
Jim Burger 12.11.2008 08:01:48
Я также соберу свой рог и добавлю ссылку на мою библиотеку: functions-dotnet
Alexey Romanov 15.12.2008 20:55:37
Мне нравится эта идея, и она создает очень красивую и более гибкую форму распределительного шкафа; однако, разве это не приукрашенный способ использования Linq-подобного синтаксиса в качестве оболочки if-then? Я бы не рекомендовал кому-то использовать это вместо реальной сделки, то есть switch-caseзаявления. Не поймите меня неправильно, я думаю, что это имеет место, и я, вероятно, буду искать способ реализации.
IAbstract 18.12.2010 15:44:10
Хотя этому вопросу уже более двух лет, стоит упомянуть, что C # 7 скоро выйдет (ish) с возможностями сопоставления с образцом.
Abion47 12.12.2016 09:19:46
10 ОТВЕТОВ
РЕШЕНИЕ

Я знаю, что это старая тема, но в 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));
}
22
13.12.2017 14:18:13
вы абсолютно правы - новые языковые возможности идеально подходят здесь
Marc Gravell♦ 13.12.2017 14:22:05
Заметная разница между C # и F # заключается в полноте сопоставления с образцом. То, что сопоставление с образцом охватывает все возможные, полностью описанные, предупреждения, предупреждения от компилятора, если вы этого не сделаете. Хотя вы можете справедливо утверждать, что случай по умолчанию делает это, на практике он также часто является исключением во время выполнения.
VoronoiPotato 6.08.2019 15:36:16

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

4
1.10.2008 07:13:46
Нет, если вы кешируете объект для повторного использования (во многом так работает лямбда-выражение C #, за исключением того, что компилятор скрывает код). Переписывание определенно улучшает производительность компиляции - однако для регулярного использования (а не LINQ-to-Something) я ожидаю, что версия делегата может быть более полезной.
Marc Gravell♦ 1.10.2008 07:24:02
Также обратите внимание - это не обязательно включает тип - он также может быть использован как составной условный (даже через LINQ) - но без грязного теста x =>? Результат1: (Тест2? Результат2: (Тест3? Результат 3: Результат4))
Marc Gravell♦ 1.10.2008 07:26:03
Приятно знать, хотя я имел в виду производительность фактической компиляции : сколько времени занимает csc.exe - я недостаточно знаком с C #, чтобы понять, действительно ли это когда-либо является проблемой, но это большая проблема для C ++.
Simon Buchan 1.10.2008 09:15:08
csc не будет моргать - это так похоже на работу LINQ, а компилятор C # 3.0 довольно хорош в LINQ / методах расширения и т. д.
Marc Gravell♦ 1.10.2008 09:21:07

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

Я понятия не имею, может ли это когда-либо быть особенностью языка 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
22
1.10.2008 07:26:01
Спасибо за версию F #. Полагаю, мне нравится, как F # справляется с этим, но я не уверен, что (в целом) F # является правильным выбором в данный момент, поэтому я должен идти по этому пути ...
Marc Gravell♦ 1.10.2008 07:30:46

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

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

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

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

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

25
20.02.2014 21:35:52
На самом деле - версия, которую я имею, делает короткое замыкание как в версии делегата, так и в версии выражения. Версия выражения компилируется в составное условие; версия делегата - это просто набор предикатов и func / actions - после совпадения он останавливается.
Marc Gravell♦ 1.10.2008 08:27:31
Интересно - из беглого взгляда я предположил, что он должен будет выполнить хотя бы базовую проверку каждого условия, так как оно выглядит как цепочка методов, но теперь я понимаю, что методы на самом деле связывают экземпляр объекта для его построения, чтобы вы могли это сделать. Я отредактирую свой ответ, чтобы удалить это утверждение.
Greg Beech 1.10.2008 08:33:03

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

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

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

3
1.10.2008 09:16:07

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

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

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

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

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

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

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

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

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

37
12.10.2008 11:38:24

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

5
16.06.2009 07:05:34

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

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

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

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

9
13.08.2009 07:29:01
Может быть. На самом деле, мне было бы трудно придумать убедительный аргумент «убийцы» о том, зачем это нужно (в отличие от «возможно, хорошего в нескольких крайних случаях ценой усложнения языка»).
Marc Gravell♦ 13.08.2009 07:42:47

Чтобы ответить на ваш вопрос, да, я думаю, что синтаксические конструкции сопоставления с образцом полезны. Я хотел бы видеть синтаксическую поддержку в 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));
        }
    }
13
4.09.2011 20:22:40

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

0
13.09.2017 15:35:54