简体   繁体   中英

Custom Nullable<T> Extension Methods and SelectMany

There are extension methods for Nullable<T> like below.

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. 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.

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

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

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. 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 .

Note that it specifically uses an anonymous class, rather than a 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 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.

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.

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(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. 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. 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. But Test3 requires three arguments for the result selector and an intermediate anonymous type is created for that.


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 . Pair of these methods will be aligned with functional programming domain much more than from v1 in nv1 queries:

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.

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. 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.

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

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

However, you would find it more convenient to work with user-defined structs if you could write it fluently. Is there any good way?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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