【问题标题】:Is there idiomatic C# equivalent to C's comma operator?是否有与 C 的逗号运算符等效的惯用 C#?
【发布时间】:2011-10-30 04:14:27
【问题描述】:

我在 C# 中使用了一些函数式的东西,并且一直被 List.Add 不返回更新列表这一事实所困扰。

一般来说,我想在一个对象上调用一个函数,然后返回更新后的对象。

例如,如果 C# 有一个逗号运算符,那就太好了:

((accum, data) => accum.Add(data), accum)

我可以像这样编写自己的“逗号运算符”:

static T comma(Action a, Func<T> result) {
    a();
    return result();
}

看起来它可以工作,但调用站点会很难看。我的第一个例子是这样的:

((accum, data) => comma(accum.Add(data), ()=>accum))

足够的例子!如果没有其他开发人员稍后出现并因代码气味而皱起鼻子,那么最干净的方法是什么?

【问题讨论】:

  • List.Add 不会返回一个新列表,而只是在原地修改它。从这个意义上说,它没有功能。

标签: c# functional-programming


【解决方案1】:

我知道这个帖子很老了,但我想为未来的用户附加以下信息:

目前没有这样的运营商。在 C# 6 开发周期中,添加了 semicolon operator,如下所示:

int square = (int x = int.Parse(Console.ReadLine()); Console.WriteLine(x - 2); x * x);

可以这样翻译:

int square = compiler_generated_Function();

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private int compiler_generated_Function()
{
    int x = int.Parse(Console.ReadLine());

    Console.WriteLine(x - 2);

    return x * x;
}

但是,此功能在最终 C# 版本之前已被删除

【讨论】:

【解决方案2】:

另一种直接来自函数式编程的技术如下。像这样定义一个 IO 结构:

/// <summary>TODO</summary>
public struct IO<TSource> : IEquatable<IO<TSource>> {
    /// <summary>Create a new instance of the class.</summary>
    public IO(Func<TSource> functor) : this() { _functor = functor; }

    /// <summary>Invokes the internal functor, returning the result.</summary>
    public TSource Invoke() => (_functor | Default)();

    /// <summary>Returns true exactly when the contained functor is not null.</summary>
    public bool HasValue => _functor != null;

    X<Func<TSource>> _functor { get; }

    static Func<TSource> Default => null;
}

并使用以下扩展方法使其成为支持 LINQ 的 monad:

[SuppressMessage("Microsoft.Naming", "CA1724:TypeNamesShouldNotMatchNamespaces")]
public static class IO {
    public static IO<TSource> ToIO<TSource>( this Func<TSource> source) {
        source.ContractedNotNull(nameof(source));
        return new IO<TSource>(source);
    }

    public static IO<TResult> Select<TSource,TResult>(this IO<TSource> @this,
        Func<TSource,TResult> projector
    ) =>
        @this.HasValue && projector!=null
             ? New(() => projector(@this.Invoke()))
             : Null<TResult>();

    public static IO<TResult> SelectMany<TSource,TResult>(this IO<TSource> @this,
        Func<TSource,IO<TResult>> selector
    ) =>
        @this.HasValue && selector!=null
             ? New(() => selector(@this.Invoke()).Invoke())
             : Null<TResult>();

    public static IO<TResult> SelectMany<TSource,T,TResult>(this IO<TSource> @this,
        Func<TSource, IO<T>> selector,
        Func<TSource,T,TResult> projector
    ) =>
        @this.HasValue && selector!=null && projector!=null
             ? New(() => { var s = @this.Invoke(); return projector(s, selector(s).Invoke()); } )
             : Null<TResult>();

    public static IO<TResult> New<TResult> (Func<TResult> functor) => new IO<TResult>(functor);

    private static IO<TResult> Null<TResult>() => new IO<TResult>(null);
}

现在您可以使用 LINQ 综合语法

using Xunit;
[Fact]
public static void IOTest() {
    bool isExecuted1 = false;
    bool isExecuted2 = false;
    bool isExecuted3 = false;
    bool isExecuted4 = false;
    IO<int> one = new IO<int>( () => { isExecuted1 = true; return 1; });
    IO<int> two = new IO<int>( () => { isExecuted2 = true; return 2; });
    Func<int, IO<int>> addOne = x => { isExecuted3 = true; return (x + 1).ToIO(); };
    Func<int, Func<int, IO<int>>> add = x => y => { isExecuted4 = true; return (x + y).ToIO(); };

    var query1 = ( from x in one
                   from y in two
                   from z in addOne(y)
                   from _ in "abc".ToIO()
                   let addOne2 = add(x)
                   select addOne2(z)
                 );
    Assert.False(isExecuted1); // Laziness.
    Assert.False(isExecuted2); // Laziness.
    Assert.False(isExecuted3); // Laziness.
    Assert.False(isExecuted4); // Laziness.
    int lhs = 1 + 2 + 1;
    int rhs = query1.Invoke().Invoke();
    Assert.Equal(lhs, rhs); // Execution.

    Assert.True(isExecuted1);
    Assert.True(isExecuted2);
    Assert.True(isExecuted3);
    Assert.True(isExecuted4);
}

如果需要一个组合但只返回 void 的 IO monad,请定义此结构和相关方法:

public struct Unit : IEquatable<Unit>, IComparable<Unit> {
    [CLSCompliant(false)]
    public static Unit _ { get { return _this; } } static Unit _this = new Unit();
}

public static IO<Unit> ConsoleWrite(object arg) =>
    ReturnIOUnit(() => Write(arg));

public static IO<Unit> ConsoleWriteLine(string value) =>
    ReturnIOUnit(() => WriteLine(value));

public static IO<ConsoleKeyInfo> ConsoleReadKey() => new IO<ConsoleKeyInfo>(() => ReadKey());

这很容易允许编写这样的代码片段:

from pass  in Enumerable.Range(0, int.MaxValue)
let counter = Readers.Counter(0)
select ( from state in gcdStartStates
         where _predicate(pass, counter())
         select state )
into enumerable
where ( from _   in Gcd.Run(enumerable.ToList()).ToIO()
        from __  in ConsoleWrite(Prompt(mode))
        from c   in ConsoleReadKey()
        from ___ in ConsoleWriteLine()
        select c.KeyChar.ToUpper() == 'Q' 
      ).Invoke()
select 0;

旧的 C 逗号操作符很容易被识别出来:一元 compose 操作。

当人们尝试以流利的风格编写该片段时,理解语法的真正优点就显而易见了:

( Enumerable.Range(0,int.MaxValue)
            .Select(pass => new {pass, counter = Readers.Counter(0)})
            .Select(_    => gcdStartStates.Where(state => _predicate(_.pass,_.counter()))
                                          .Select(state => state)
                   )
).Where(enumerable => 
   ( (Gcd.Run(enumerable.ToList()) ).ToIO()
        .SelectMany(_ => ConsoleWrite(Prompt(mode)),(_,__) => new {})
        .SelectMany(_ => ConsoleReadKey(),          (_, c) => new {c})
        .SelectMany(_ => ConsoleWriteLine(),        (_,__) => _.c.KeyChar.ToUpper() == 'Q')
    ).Invoke()
).Select(list => 0);

【讨论】:

    【解决方案3】:

    您几乎可以使用 C# 3.0 中的代码块自然地完成第一个示例。

    ((accum, data) => { accum.Add(data); return accum; })
    

    【讨论】:

      【解决方案4】:

      这就是 Concat http://msdn.microsoft.com/en-us/library/vstudio/bb302894%28v=vs.100%29.aspx 的用途。只需将单个项目包装在一个数组中。功能代码不应改变原始数据。如果性能是一个问题,而这还不够好,那么您将不再使用函数式范例。

      ((accum, data) => accum.Concat(new[]{data}))
      

      【讨论】:

      • 对我来说似乎是 LINQ-y-est 的答案。 耸耸肩
      【解决方案5】:

      我认为制作一个不需要您编写包装器方法的包装器类答案版本会很有趣。

      public class FList<T> : List<T>
      {
          public FList<T> Do(string method, params object[] args)
          {
              var methodInfo = GetType().GetMethod(method);
      
              if (methodInfo == null)
                  throw new InvalidOperationException("I have no " + method + " method.");
      
              if (methodInfo.ReturnType != typeof(void))
                  throw new InvalidOperationException("I'm only meant for void methods.");
      
              methodInfo.Invoke(this, args);
              return this;
          }
      }
      
      {
          var list = new FList<string>();
      
          list.Do("Add", "foo")
              .Do("Add", "remove me")
              .Do("Add", "bar")
              .Do("RemoveAt", 1)
              .Do("Insert", 1, "replacement");
      
          foreach (var item in list)
              Console.WriteLine(item);    
      }
      

      输出:

      foo 
      replacement 
      bar
      

      编辑

      您可以通过利用 C# 索引属性来精简语法。

      只需添加这个方法:

      public FList<T> this[string method, params object[] args]
      {
          get { return Do(method, args); }
      }
      

      现在调用看起来像:

      list = list["Add", "foo"]
                 ["Add", "remove me"]
                 ["Add", "bar"]
                 ["RemoveAt", 1]
                 ["Insert", 1, "replacement"];
      

      当然,换行符是可选的。

      破解语法有点乐趣。

      【讨论】:

      • 我从来没有想过使用这样的索引属性。同时令人惊叹和最丑陋;)(有两个原因:更改 get{} 属性中的对象,以及使用魔术字符串)
      • @devio,我不会质疑丑陋(但它也很酷 :)),但这些不是魔术字符串。魔术字符串是产生特殊/独特结果的东西。但是使用像这样的硬编码文字当然会绕过方法名称(没有符号!)和类型的编译时检查。它有点将 C# 变成了一种平滑的弱类型动态语言(更不用说慢得要命了!)。有趣:)
      • 您可以使用 Enum 作为方法名称(在运行时转换为字符串) - 它不会产生任何技术差异,但它会更整洁,更不容易出错。 (或者你可以使用 consts)
      【解决方案6】:

      扩展方法可以说是最好的解决方案,但为了完整起见,不要忘记明显的替代方案:包装类。

      public class FList<T> : List<T>
      {
          public new FList<T> Add(T item)
          {
              base.Add(item);
              return this;
          }
      
          public new FList<T> RemoveAt(int index)
          {
              base.RemoveAt(index);
              return this;
          }
      
          // etc...
      }
      
      {
           var list = new FList<string>();
           list.Add("foo").Add("remove me").Add("bar").RemoveAt(1);
      }
      

      【讨论】:

        【解决方案7】:

        我知道这是Fluent

        使用扩展方法的 List.Add 的流畅示例

        static List<T> MyAdd<T>(this List<T> list, T element)
        {
            list.Add(element);
            return list;
        }
        

        【讨论】:

        • 很好地呼吁包含 Fluent 定义。
        • 但是你为什么不直接使用 LINQ 来做这种事情呢?
        • @KevinRoche:没有人建议您不使用 linq。事实上,这种方法似乎可以很好地与 linq 集成。
        • @recursive LINQ(或任何“功能”代码)操作不应该改变数据,这就是这样做的。执行此操作的 LINQ 方法是 .Concat(new []{element})
        • @novaterata:是的,从功能意义上说,这并不是真正的“纯粹”,但这个问题专门询问了突变。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-25
        • 2017-07-11
        • 2010-12-16
        • 1970-01-01
        • 2019-07-06
        相关资源
        最近更新 更多