自定义 Nullable<T> 扩展方法和 SelectMany

4

下面是针对Nullable<T>的扩展方法。

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<T>可以在查询语法中使用。下面的测试将通过。

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

不过,如果你尝试链式调用三个或更多的方法,它将被视为匿名类型并且无法编译。

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

然而,如果您希望编写流畅的代码,更方便地使用用户定义的结构体是否有好的方法呢?

2个回答

3

想想编译器如何将查询表达式转换为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) => (v2, v3))。这在语言规范中有明确规定。

A query expression with a second from clause followed by something other than a select clause:

from x1 in e1
from x2 in e2
...

is translated into

from * in ( e1 ) . SelectMany( x1 => e2 , ( x1 , x2 ) => new { x1 , x2 } )
...
很不幸,你无法改变这一点。你可以尝试分叉Roslyn编译器以使其编译为创建ValueTuple,但严格来说那已经不是“C#”了。
另一方面,如果你编写自己的Nullable<T>类型,并且不将T限制为值类型,那么这个想法可能可行,但我不确定是否值得这样做。

当你写下“如果你自己编写了Nullable<T>类型”时,我也想到了同样的想法。所以,是的,我有这个“自己的Nullable<T>类型”,所以我就把它应用到了这个问题上。 :^ - Teneko

1

让我们来看一下这个查询

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接