简体   繁体   English

自定义可为空<t>扩展方法和 SelectMany</t>

[英]Custom Nullable<T> Extension Methods and SelectMany

There are extension methods for Nullable<T> like below. 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);
        }
    }
}

This allows Nullable<T> to be used in query syntax.这允许在查询语法中使用Nullable<T> The following tests will pass.以下测试将通过。

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

However, if you try to chain 3 or more, it will be treated as an anonymous type and will not compile.但是,如果您尝试链接 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);
        }

You can work around this issue by manually specifying to use ValueTuple as below, but this is ugly.您可以通过如下所示手动指定使用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);
        }

These simplified examples can be solved simply by using the + operator: var q = nv1 + nv2 + nv3;这些简化的例子可以简单地通过使用+运算符来解决: var q = nv1 + nv2 + nv3;

However, you would find it more convenient to work with user-defined structs if you could write it fluently.但是,如果您能流畅地编写用户定义的结构,您会发现使用它会更方便。 Is there any good way?有什么好办法吗?

Think about how the compiler would turn the query expression into SelectMany calls.想想编译器如何将查询表达式转换为SelectMany调用。 It would turn it into something like:它会把它变成这样的东西:

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

Note how the V of the second SelectMany call is inferred to be an anonymous class, which is a reference type, and doesn't fit the constraint of : struct .注意第二个SelectMany调用的V是如何被推断为匿名 class 的,这是一个引用类型,不符合: struct的约束。

Note that it specifically uses an anonymous class, rather than a ValueTuple ( (v2, v3) => (v2, v3) ).请注意,它专门使用匿名 class,而不是ValueTuple ( (v2, v3) => (v2, v3) )。 This is specified in the language spec :这是在语言规范中指定的:

A query expression with a second from clause followed by something other than a select clause:带有第二个 from 子句的查询表达式,后跟 select 子句以外的内容:

 from x1 in e1 from x2 in e2...

is translated into被翻译成

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

So unfortunately, you can't do anything about it.所以不幸的是,你对此无能为力。 You can try forking the Roslyn compiler to make it compile to create a ValueTuple instead, but technically that's not "C#" anymore.您可以尝试分叉Roslyn编译器,使其编译为创建一个ValueTuple ,但从技术上讲,这不再是“C#”了。

OTOH, this idea could work if you write your own Nullable<T> type, and not constrain T to a value type, but I'm not sure that's worth it. OTOH,如果您编写自己的Nullable<T>类型并且不将T限制为值类型,这个想法可能会奏效,但我不确定这是否值得。

Let's take a look at this query我们来看看这个查询

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?

Such queries will be compiled as a chain of SelectMany calls此类查询将编译为 SelectMany 调用链

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

As you can see, it can accept only two arguments in result selector - of one of source collection type and one of second collection type returned by selector.如您所见,它在结果选择器中只能接受两个 arguments - 一个是源集合类型,另一个是选择器返回的第二个集合类型。 So the only way to pass more than two arguments down the chain (so that all arguments eventually will arrive in the last result selector) is by creating anonymous types.因此,将两个以上的 arguments 向下传递到链中(以便所有 arguments 最终将到达最后一个结果选择器)的唯一方法是创建匿名类型。 And this is how it looks like:这就是它的样子:

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

Again, the result selector limited with two input arguments. So for your passing Test1 and Test2 anonymous type is not created, because both arguments can be passed to the result selector.同样,结果选择器限制了两个输入 arguments。因此,对于您传递的 Test1 和 Test2,不会创建匿名类型,因为两个 arguments 都可以传递给结果选择器。 But Test3 requires three arguments for the result selector and an intermediate anonymous type is created for that.但是 Test3 需要三个 arguments 作为结果选择器,并为此创建了一个中间匿名类型。


You cannot make your extension method to accept both nullable structs and generated anonymous types (which are reference types).您不能使您的扩展方法同时接受可为空的结构和生成的匿名类型(它们是引用类型)。 I would suggest you create domain-specific extension methods Bind and Map .我建议您创建特定于域的扩展方法BindMap Pair of these methods will be aligned with functional programming domain much more than from v1 in nv1 queries:这些方法中的一对将与函数式编程领域保持一致,而不是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;

And usage和用法

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

There are extension methods for Nullable<T> like below. 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);
        }
    }
}

This allows Nullable<T> to be used in query syntax.这允许Nullable<T>在查询语法中使用。 The following tests will pass.以下测试将通过。

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

However, if you try to chain 3 or more, it will be treated as an anonymous type and will not compile.但是,如果您尝试链接 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);
        }

You can work around this issue by manually specifying to use ValueTuple as below, but this is ugly.您可以通过手动指定使用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);
        }

These simplified examples can be solved simply by using the + operator: var q = nv1 + nv2 + nv3;这些简化的例子可以通过使用+运算符简单地解决: var q = nv1 + nv2 + nv3;

However, you would find it more convenient to work with user-defined structs if you could write it fluently.但是,如果您能流利地编写它,您会发现使用用户定义的结构会更方便。 Is there any good way?有什么好办法吗?

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM