Выполнение сопоставления шаблонов в C #

17

В Scala вы можете использовать сопоставление образцов для получения результата в зависимости от типа ввода. Например:

val title = content match {
    case blogPost: BlogPost => blogPost.blog.title + ": " + blogPost.title
    case blog: Blog => blog.title
}

В C # я идеально хотел бы писать:

var title = Visit(content,
    (BlogPost blogPost) => blogPost.Blog.Title + ": " + blogPost.Title,
    (Blog blog) => blog.Title
);

Возможно ли это? Когда я пробовал писать его как единственный метод, я не знаю, как указать дженерики. Следующая реализация кажется правильной, не считая того, что функция проверки типов допускает функции, которые принимают подтипы T:

    public TResult Visit<T, TResult>(T value, params Func<T, TResult>[] visitors)
    {
        foreach (var visitor in visitors)
        {
            if (visitor.Method.GetGenericArguments()[0].IsAssignableFrom(value.GetType()))
            {
                return visitor(value);
            }
        }
        throw new ApplicationException("No match");
    }

Самое близкое, что я получил, это добавить функции к объекту индивидуально, а затем вызвать visit на значение:

    public class Visitor<T, TResult>
    {
        private class Result
        {
            public bool HasResult;
            public TResult ResultValue;
        }

        private readonly IList<Func<T, Result>> m_Visitors = new List<Func<T, Result>>();

        public TResult Visit(T value)
        {
            foreach (var visitor in m_Visitors)
            {
                var result = visitor(value);
                if (result.HasResult)
                {
                    return result.ResultValue;
                }
            }
            throw new ApplicationException("No match");
        }

        public Visitor<T, TResult> Add<TIn>(Func<TIn, TResult> visitor) where TIn : T
        {
            m_Visitors.Add(value =>
            {
                if (value is TIn)
                {
                    return new Result { HasResult = true, ResultValue = visitor((TIn)value) };
                }
                return new Result { HasResult = false };
            });
            return this;
        }
    }

Это можно использовать так:

var title = new Visitor<IContent, string>()
    .Add((BlogPost blogPost) => blogPost.Blog.Title + ": " + blogPost.Title)
    .Add((Blog blog) => blog.Title)
    .Visit(content);

Любая идея, как это сделать с помощью одного вызова метода?

    
задан Michael Williamson 17.05.2011 в 15:32
источник
  • Kinda выглядит как словарь, где ключ является типом, а значение является функцией ... –  Roly 17.05.2011 в 15:43
  • Используете ли вы C # 3 или 4? В C # 4 тип Func контравариантен по своим формальным типам параметров, что дает вам большую гибкость в преобразованиях. –  Eric Lippert 17.05.2011 в 16:09
  • @ Эрик Липперт: в этом случае, я думаю, я действительно хочу ковариацию, а не контравариантность. Я хочу принять функции, которые не могут принимать параметры типа T (тогда как вы обычно хотите принять любую функцию, которая принимает параметры типа T, которая включает в себя функции, которые принимают параметры типа U, где T <: U) –  Michael Williamson 17.05.2011 в 16:20
  • @ Майкл: Если вы хотите небезопасную ковариацию по типам делегатов, вы, вероятно, столкнетесь с некоторыми трудностями. Система типов предназначена для того, чтобы помочь вам предотвратить подобные вещи, а не помогать вам в этом. –  Eric Lippert 17.05.2011 в 16:23
  • @ Эрик: тип безопасности является причиной этого фрагмента кода: visitor.Method.GetGenericArguments () [0] .IsAssignableFrom (value.GetType ()). Я хочу разрешить функции, которые потенциально могут не принимать входные данные, поскольку я хочу выполнять итерацию переданных функций и возвращать результат из первой функции, которая принимает вход. –  Michael Williamson 17.05.2011 в 16:32
Показать остальные комментарии

5 ответов

9

Использование функционального C # (из @Alireza)

var title = content.Match()
   .With<BlogPost>(blogPost => blogPost.Blog.Title + ": " + blogPost.Title)
   .With<Blog>(blog => blog.Title)
   .Result<string>();
    
ответ дан Richard Schneider 17.05.2011 в 16:21
источник
  • Тот факт, что Functional C #, похоже, использует тот же подход, который я использовал, то есть, передавая каждую лямбду в отдельных вызовах метода, кажется, предполагает, что выполнение всего этого в одном вызове метода просто невозможно (по крайней мере , при сохранении безопасности типов). Ах хорошо. –  Michael Williamson 19.05.2011 в 23:36
14

Совместимость шаблонов - одна из тех прекрасных функций, которые чаще всего встречаются в функциональных языках программирования, таких как F #. В кодексе есть большой проект, названный Функциональный C # . Рассмотрим следующий код F #:

let operator x = match x with
                 | ExpressionType.Add -> "+"

let rec toString exp = match exp with
                       | LambdaExpression(args, body) -> toString(body)
                       | ParameterExpression(name) -> name
                       | BinaryExpression(op,l,r) -> sprintf "%s %s %s" (toString l) (operator op) (toString r)

Используя библиотеку Functional C #, эквивалент C # будет выглядеть следующим образом:

var Op = new Dictionary<ExpressionType, string> { { ExpressionType.Add, "+" } };

Expression<Func<int,int,int>> add = (x,y) => x + y;

Func<Expression, string> toString = null;
 toString = exp =>
 exp.Match()
    .With<LambdaExpression>(l => toString(l.Body))
    .With<ParameterExpression>(p => p.Name)
    .With<BinaryExpression>(b => String.Format("{0} {1} {2}", toString(b.Left), Op[b.NodeType], toString(b.Right)))
    .Return<string>();
    
ответ дан Alireza Maddah 17.05.2011 в 15:49
источник
5

Чтобы обеспечить полное сопоставление шаблонов, вам нужно будет построить функцию в самом типе. Вот как я это сделаю:

public abstract class Content
{
    private Content() { }

    public abstract T Match<T>(Func<Blog, T> convertBlog, Func<BlogPost, T> convertPost);

    public class Blog : Content
    {
        public Blog(string title)
        {
            Title = title;
        }
        public string Title { get; private set; }

        public override T Match<T>(Func<Blog, T> convertBlog, Func<BlogPost, T> convertPost)
        {
            return convertBlog(this);
        }
    }

    public class BlogPost : Content
    {
        public BlogPost(string title, Blog blog)
        {
            Title = title;
            Blog = blog;
        }
        public string Title { get; private set; }
        public Blog Blog { get; private set; }

        public override T Match<T>(Func<Blog, T> convertBlog, Func<BlogPost, T> convertPost)
        {
            return convertPost(this);
        }
    }

}

public static class Example
{
    public static string GetTitle(Content content)
    {
        return content.Match(blog => blog.Title, post => post.Blog.Title + ": " + post.Title);
    }
}
    
ответ дан kvb 17.05.2011 в 17:46
источник
  • Это по существу реализация шаблона посетителя, за исключением, кроме класса с двумя методами, вы передаете два лямбда. –  Jeff Walker Code Ranger 27.02.2016 в 19:11
1

Проверьте мою реализацию соответствия шаблону: repo

Он основан на выражениях, поэтому он предлагает равные возможности с вложенными ifs.

Пример использования:

string s1 = "Hello";
string s2 = null;

Func<Option<string>> match = new Matcher<Option<string>>
{
     {s => s is None, s => Console.WriteLine("None")},
     {s => s is Some, s => Console.WriteLine((string)s) // or s.Value
};

match(s1); // Hello
match(s2); // None

Доступно через NuGet: пакет Nuget

    
ответ дан danaslapman 23.04.2014 в 10:12
источник
  • Опубликовать необходимые фрагменты кода, пожалуйста. –  Christian St. 23.04.2014 в 10:32
0

Общая реализация, которую я использую, может соответствовать типу, условию или значению:

public static class Match
{
    public static PatternMatch<T, R> With<T, R>(T value)
    {
        return new PatternMatch<T, R>(value);
    }

    public struct PatternMatch<T, R>
    {
        private readonly T _value;
        private R _result;

        private bool _matched;

        public PatternMatch(T value)
        {
            _value = value;
            _matched = false;
            _result = default(R);
        }

        public PatternMatch<T, R> When(Func<T, bool> condition, Func<R> action)
        {
            if (!_matched && condition(_value))
            {
                _result = action();
                _matched = true;
            }

            return this;
        }

        public PatternMatch<T, R> When<C>(Func<C, R> action)
        {
            if (!_matched && _value is C)
            {
                _result = action((C)(object)_value);
                _matched = true;
            }
            return this;
        }


        public PatternMatch<T, R> When<C>(C value, Func<R> action)
        {
            if (!_matched && value.Equals(_value))
            {
                _result = action();
                _matched = true;
            }
            return this;
        }


        public R Result => _result;

        public R Default(Func<R> action)
        {
            return !_matched ? action() : _result;
        }
    }
}

И в вашем случае использование будет выглядеть так:

Match.With<IContent, string>(content)
     .When<BlogPost>(blogPost => blogPost.Blog.Title)
     .When<Blog>(blog => blog.Title)
     .Result; // or just .Default(()=> "none");

Некоторые другие примеры:

var result = Match.With<IFoo, int>(new Foo() { A = 5 })
    .When<IFoo>(foo => foo.A)
    .When<IBar>(bar => bar.B)
    .When<string>(Convert.ToInt32)
    .Result;
Assert.Equal(5, result);

var result = Match.With<int, string>(n)
    .When(x => x > 100, () => "n>100")
    .When(x => x > 10, () => "n>10")
    .Default(() => "");
Assert.Equal("n>10", result);

 var result = Match.With<int, string>(5)
     .When(1, () => "1")
     .When(5, () => "5")
     .Default(() => "e");
 Assert.Equal("5", result);
    
ответ дан Hrvoje Hudo 02.03.2016 в 17:21
источник