簡體   English   中英

自定義可為空<t>擴展方法和 SelectMany</t>

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

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

但是,如果您嘗試鏈接 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;

但是,如果您能流暢地編寫用戶定義的結構,您會發現使用它會更方便。 有什么好辦法嗎?

想想編譯器如何將查詢表達式轉換為SelectMany調用。 它會把它變成這樣的東西:

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

注意第二個SelectMany調用的V是如何被推斷為匿名 class 的,這是一個引用類型,不符合: struct的約束。

請注意,它專門使用匿名 class,而不是ValueTuple ( (v2, v3) => (v2, v3) )。 這是在語言規范中指定的:

帶有第二個 from 子句的查詢表達式,后跟 select 子句以外的內容:

 from x1 in e1 from x2 in e2...

被翻譯成

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

所以不幸的是,你對此無能為力。 您可以嘗試分叉Roslyn編譯器,使其編譯為創建一個ValueTuple ,但從技術上講,這不再是“C#”了。

OTOH,如果您編寫自己的Nullable<T>類型並且不將T限制為值類型,這個想法可能會奏效,但我不確定這是否值得。

我們來看看這個查詢

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)

如您所見,它在結果選擇器中只能接受兩個 arguments - 一個是源集合類型,另一個是選擇器返回的第二個集合類型。 因此,將兩個以上的 arguments 向下傳遞到鏈中(以便所有 arguments 最終將到達最后一個結果選擇器)的唯一方法是創建匿名類型。 這就是它的樣子:

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

同樣,結果選擇器限制了兩個輸入 arguments。因此,對於您傳遞的 Test1 和 Test2,不會創建匿名類型,因為兩個 arguments 都可以傳遞給結果選擇器。 但是 Test3 需要三個 arguments 作為結果選擇器,並為此創建了一個中間匿名類型。


您不能使您的擴展方法同時接受可為空的結構和生成的匿名類型(它們是引用類型)。 我建議您創建特定於域的擴展方法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

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

但是,如果您嘗試鏈接 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;

但是,如果您能流利地編寫它,您會發現使用用戶定義的結構會更方便。 有什么好辦法嗎?

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM