【问题标题】:Implementing pattern matching in C#在 C# 中实现模式匹配
【发布时间】:2011-05-17 13:32:25
【问题描述】:

在 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");
    }

我得到的最接近的方法是将函数单独添加到对象中,然后对值调用访问:

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

知道如何通过单个方法调用来做到这一点吗?

【问题讨论】:

  • 有点像字典,键是类型,值是函数...
  • 您使用的是 C# 3 还是 4?在 C# 4 中,Func 类型的形参类型是逆变的,这为您提供了更大的转换灵活性。
  • @Eric Lippert:在这种情况下,我认为我实际上想要协方差而不是逆变。我想接受可能无法接受 T 类型参数的函数(而您通常希望接受任何接受 T 类型参数的函数,其中包括接受 U 类型参数的函数,其中 T <: u>
  • @Michael:如果您想要委托类型的不安全协变,那么您可能会遇到一些困难。类型系统旨在帮助您防止此类事情,而不是帮助您做到这一点。
  • @Eric:类型安全是这段代码的原因:visitor.Method.GetGenericArguments()[0].IsAssignableFrom(value.GetType())。我想允许可能不接受输入的函数,因为我想遍历传递的函数并从第一个接受输入的函数返回结果。

标签: c# pattern-matching


【解决方案1】:

模式匹配是 F# 等函数式编程语言中最常见的可爱特性之一。在名为 Functional C# 的 codeplex 中有一个很棒的项目正在进行。 考虑以下 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)

使用函数式 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>();

【讨论】:

    【解决方案2】:

    使用函数式 C#(来自 @Alireza)

    var title = content.Match()
       .With<BlogPost>(blogPost => blogPost.Blog.Title + ": " + blogPost.Title)
       .With<Blog>(blog => blog.Title)
       .Result<string>();
    

    【讨论】:

    • Functional C# 似乎使用与我使用相同的方法,即在单独的方法调用中传递每个 lambda,这一事实似乎表明在单个方法调用中完成所有操作只是' t 可能(至少,在保持类型安全的同时)。啊,好吧。
    【解决方案3】:

    为了确保完全匹配模式,您需要将函数构建到类型本身中。这是我的做法:

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

    【讨论】:

    • 这本质上是一个访问者模式的实现,除了一个有两个方法的类,你传递了两个 lambdas。
    【解决方案4】:

    查看我的模式匹配实现:repo

    它基于表达式,因此它提供与嵌套 if 相同的性能。

    示例用法:

    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 package

    【讨论】:

      【解决方案5】:

      我正在使用的通用实现,可以匹配类型、条件或值:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-06-07
        • 1970-01-01
        • 1970-01-01
        • 2015-07-29
        • 2012-12-17
        相关资源
        最近更新 更多