【问题标题】:Custom Nullable<T> Extension Methods and SelectMany自定义 Nullable<T> 扩展方法和 SelectMany
【发布时间】:2021-01-16 02:02:30
【问题描述】:

Nullable&lt;T&gt; 有如下扩展方法。

using System;
using System.Runtime.CompilerServices;

namespace DoNotationish
{
    public static class NullableExtensions
    {
        public static U? Select<T, U>(this T? nullableValue, Func<T, U> f)
            where T : struct
            where U : struct
        {
            if (!nullableValue.HasValue) return null;
            return f(nullableValue.Value);
        }

        public static V? SelectMany<T, U, V>(this T? nullableValue, Func<T, U?> bind, Func<T, U, V> f)
            where T : struct
            where U : struct
            where V : struct
        {
            if (!nullableValue.HasValue) return null;
            T value = nullableValue.Value;
            U? bindValue = bind(value);
            if (!bindValue.HasValue) return null;
            return f(value, bindValue.Value);
        }
    }
}

这允许在查询语法中使用Nullable&lt;T&gt;。 以下测试将通过。

        [Test]
        public void Test1()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            var q = from v1 in nv1
                    from v2 in nv2
                    select v1 + v2;
            Assert.AreEqual(8, q);
        }

        [Test]
        public void Test2()
        {
            int? nv1 = null;
            int? nv2 = 3;
            var q = from v1 in nv1
                    from v2 in nv2
                    select v1 + v2;
            Assert.IsNull(q);
        }

但是,如果您尝试链接 3 个或更多,它将被视为匿名类型并且不会编译。

        [Test]
        public void Test3()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            int? nv3 = 8;
            var q = from v1 in nv1
                    from v2 in nv2  // Error CS0453: anonymous type is not struct
                    from v3 in nv3
                    select v1 + v2 + v3;
            Assert.AreEqual(16, q);
        }

您可以通过手动指定使用ValueTuple 来解决此问题,如下所示,但这很难看。

        [Test]
        public void Test3_()
        {
            int? nv1 = 5;
            int? nv2 = 3;
            int? nv3 = 8;
            var q = from v1 in nv1
                    from v2 in nv2
                    select (v1, v2) into temp      // ugly
                    from v3 in nv3
                    select temp.v1 + temp.v2 + v3; // ugly
            Assert.AreEqual(16, q);
        }

这些简化的例子可以简单地使用+操作符来解决:var q = nv1 + nv2 + nv3;

但是,如果您可以流利地编写用户定义的结构,您会发现使用它会更方便。有什么好办法吗?

【问题讨论】:

    标签: c# linq monads


    【解决方案1】:

    想想编译器如何将查询表达式转换为SelectMany 调用。它会把它变成这样的东西:

    var q =
        nv1.SelectMany(x => 
           nv2.SelectMany(x => nv3, (v2, v3) => new { v2, v3 }), 
           (v1, v2v3) => v1 + v2v3.v2 + v2v3.v3);
    

    注意第二个SelectMany 调用的V 是如何被推断为匿名类的,它是一个引用类型,不符合: struct 的约束。

    请注意,它专门使用匿名类,而不是 ValueTuple ((v2, v3) =&gt; (v2, v3))。这是在language spec 中指定的:

    带有第二个 from 子句的查询表达式,后面跟着一些东西 除了选择子句:

    from x1 in e1
    from x2 in e2
    ...
    

    被翻译成

    from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } )
    ...
    

    很遗憾,您对此无能为力。您可以尝试分叉 Roslyn 编译器,使其编译为创建 ValueTuple,但从技术上讲,这不再是“C#”了。

    OTOH,如果您编写自己的 Nullable&lt;T&gt; 类型,而不是将 T 限制为值类型,这个想法可能会奏效,但我不确定这是否值得。

    【讨论】:

    • 当你写“如果你写自己的 Nullable 类型”时,同样的想法击中了我。所以,是的,有这个“自己的Nullable&lt;T&gt;”类型,所以,我刚刚将它应用于这个问题。:^
    【解决方案2】:

    让我们看看这个查询

    from a in source1
    from b in source2
    from c in source3
    from d in source4
    // etc
    select selector // how is it possible that a, b, c, d available in selector?
    

    此类查询将编译为 SelectMany 调用链

    SelectMany(IEnumerable<TSource> source, 
               Func<TSource, IEnumerable<TCollection>> collectionSelector,
               Func<TSource, TCollection, TResult> resultSelector)
    

    如您所见,它在结果选择器中只能接受两个参数 - 一种是源集合类型,另一种是选择器返回的第二种集合类型。因此,将两个以上的参数沿链传递(以便所有参数最终到达最后一个结果选择器)的唯一方法是创建匿名类型。这就是它的样子:

    source1
      .SelectMany(a => source2, (a, b) => new { a, b })
      .SelectMany(x1 => source3, (x1, c) => new { x1, c })
      .SelectMany(x2 => source4, (x2, d) => selector(x2.x1.a, x2.x1.b, x2.c, d));
    

    同样,结果选择器受限于两个输入参数。所以对于你传递的 Test1 和 Test2 匿名类型不会被创建,因为这两个参数都可以传递给结果选择器。但是 Test3 需要三个参数作为结果选择器,并为此创建了一个中间匿名类型。


    您不能让您的扩展方法同时接受可为空的结构和生成的匿名类型(它们是引用类型)。我建议您创建特定于域的扩展方法 BindMap。这对方法将比from v1 in nv1 查询更符合函数式编程领域:

    public static U? Bind<T, U>(this T? maybeValue, Func<T, U?> binder)
        where T : struct
        where U : struct
            => maybeValue.HasValue ? binder(maybeValue.Value) : (U?)null;
    
    public static U? Map<T, U>(this T? maybeValue, Func<T, U> mapper)
        where T : struct
        where U : struct
            => maybeValue.HasValue ? mapper(maybeValue.Value) : (U?)null;
    

    及用法

    nv1.Bind(v1 => nv2.Bind(v2 => nv3.Map(v3 => v1 + v2 + v3)))
       .Map(x => x * 2) // eg
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-05-07
      • 1970-01-01
      • 1970-01-01
      • 2012-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多