自定义 Nullable<T> 扩展方法和 SelectMany
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,这是一个引用类型,不符合[=的约束16=].
请注意,它专门使用匿名 class,而不是 ValueTuple
((v2, v3) => (v2, v3)
)。这在 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 } )
...
很遗憾,您对此无能为力。您可以尝试分叉 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)
如您所见,它在结果选择器中只能接受两个参数——一个是源集合类型,另一个是选择器返回的第二个集合类型。因此,将两个以上的参数向下传递到链中(这样所有参数最终都会到达最后一个结果选择器)的唯一方法是创建匿名类型。这就是它的样子:
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 需要结果选择器的三个参数,并且为此创建了一个中间匿名类型。
您不能让您的扩展方法同时接受可为空的结构和生成的匿名类型(它们是引用类型)。我建议您创建特定于域的扩展方法 Bind 和 Map。这些方法中的一对将与函数式编程领域对齐,而不是 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;
但是,如果您可以流畅地编写用户定义的结构,您会发现使用它会更方便。有什么好的方法吗?
想想编译器如何将查询表达式转换为 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,这是一个引用类型,不符合[=的约束16=].
请注意,它专门使用匿名 class,而不是 ValueTuple
((v2, v3) => (v2, v3)
)。这在 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 } ) ...
很遗憾,您对此无能为力。您可以尝试分叉 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)
如您所见,它在结果选择器中只能接受两个参数——一个是源集合类型,另一个是选择器返回的第二个集合类型。因此,将两个以上的参数向下传递到链中(这样所有参数最终都会到达最后一个结果选择器)的唯一方法是创建匿名类型。这就是它的样子:
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 需要结果选择器的三个参数,并且为此创建了一个中间匿名类型。
您不能让您的扩展方法同时接受可为空的结构和生成的匿名类型(它们是引用类型)。我建议您创建特定于域的扩展方法 Bind 和 Map。这些方法中的一对将与函数式编程领域对齐,而不是 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