Linq:IEnumerable 上的扩展方法,可在执行选择时自动执行空值检查
Linq: Extension method on IEnumerable to automatically do null-checks when performing selects
在 IEnumerable
上执行 Select
时,我认为检查空引用是一种很好的做法,所以我经常在 Select
之前有一个 Where
这个:
someEnumerable.Where(x => x != null).Select(x => x.SomeProperty);
访问子属性时会变得更加复杂:
someEnumerable.Where(x => x != null && x.SomeProperty != null).Select(x => x.SomeProperty.SomeOtherProperty);
要遵循此模式,我需要多次调用 Where
。我想在 IEnumerable
上创建一个扩展方法,它根据 Select
中引用的内容自动执行此类空值检查。像这样:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty);
someEnumerable.SelectWithNullCheck(x => x.SomeProperty.SomeOtherProperty);
这能做到吗?是fx吗?创建这样的扩展方法时,可以从 selector
参数中检索选定的属性吗?
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Where(THIS IS WHERE THE AUTOMATIC NULL-CHECKS HAPPEN).Select(selector);
}
编辑:我使用 C# 5.0 和 .NET Framework 4.5
为什么不使用 ?.
运算符?
someEnumerable.Where(x => x?.SomeProperty != null).Select(x => x.SomeProperty.SomeOtherProperty);
(注意这可能 return 空值)
或
someEnumerable.Select(x => x?.SomeProperty?.SomeOtherProperty).Where(x => x != null);
(这不会 return 任何空值)
这并不是什么好事或坏事,这取决于你想要什么 return
由于您使用的是 C# 5.0,因此您可以按以下方式编写扩展方法:
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
return source.Where(x => x != null).Select(selector).Where(x => x != null);
}
投影前后 (Select call)
应用检查结果不为空。
那么用法将是:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty)
.SelectWithNullCheck(y => y.SomeOtherProperty);
请注意,每次调用中的项目类型都不同。
如果你确实想要类似这样的:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty.SomeOtherProperty);
那么您需要按照@Treziac 的建议使用 ?.
运算符(在 C# 6.0 中引入),然后过滤掉空值:
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Select(selector).Where( x=> x != null);
}
someEnumerable.SelectWithNullCheck(x => x?.SomeProperty?.SomeOtherProperty);
另一种选择是将选择空值检查拆分为自定义运算符(例如 WhereNotNull
)。将它与 ?.
运算符结合起来,以一种非常有表现力的方式解决你的问题。
public static IEnumerable<TSource> WhereNotNull<TSource>(this IEnumerable<TSource> source)
{
return source.Where(x=> x != null);
}
这允许你写:
someEnumerable.Select(x => x?.SomeProperty?.SomeOtherProperty)
.WhereNotNull();
如果不是,您可以随时链接 selects
(对于 C# 6 之前的版本):
someEnumerable.Select(x => x.SomeProperty)
.Select(x => x.SomeOtherProperty)
.WhereNotNull();
鉴于您绝对想访问 x.SomeProperty.SomeOtherProperty
,最后一个选择是赶上 NullReferenceException
。
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Select(x =>
{
try
{
return selector(x);
}
catch(NullReferenceException ex)
{
return default(TResult);
}
})
.Where(x=> default(TResult) != x);
}
您可以使用基于 Expression 的解决方案。以下是 field / 属性 链调用的基本可行解决方案。它适用于非常深的调用链。它并不完美。例如,如果链中有 方法调用 将无法工作 (obj.Prop1.MethodCall() .Prop2).
基于表达式的解决方案通常较慢,因为需要编译 lambda 表达式来委托,应该考虑到这一点.
性能统计数据:
使用嵌套调用级别为 2 (obj.Prop1.Prop2) 的 200k 对象集合进行测试,其中所有对象都因条件而失败。
LINQ C# 6 在哪里?。运算符:2 - 4 毫秒
基于异常(尝试/捕获):14,000 - 15,000 毫秒
基于表达式:4 - 10 毫秒
注意:基于表达式的解决方案每次调用都会增加几毫秒的开销,这个数字不依赖于集合大小,因为表达式会为每个调用编译呼叫这是一项昂贵的操作。有兴趣的可以考虑缓存机制
基于表达式的解决方案来源::
public static IEnumerable<T> IgnoreIfNull<T, TProp>(this IEnumerable<T> sequence, Expression<Func<T, TProp>> expression)
{
var predicate = BuildNotNullPredicate(expression);
return sequence.Where(predicate);
}
private static Func<T, bool> BuildNotNullPredicate<T, TProp>(Expression<Func<T, TProp>> expression)
{
var root = expression.Body;
if (root.NodeType == ExpressionType.Parameter)
{
return t => t != null;
}
var pAccessMembers = new List<Expression>();
while (root.NodeType == ExpressionType.MemberAccess)
{
var mExpression = root as MemberExpression;
pAccessMembers.Add(mExpression);
root = mExpression.Expression;
}
pAccessMembers.Reverse();
var body = pAccessMembers
.Aggregate(
(Expression)Expression.Constant(true),
(f, s) =>
{
if (s.Type.IsValueType)
{
return f;
}
return Expression.AndAlso(
left: f,
right: Expression.NotEqual(s, Expression.Constant(null))
);
});
var lambda = Expression.Lambda<Func<T, bool>>(body, expression.Parameters[0]);
var func = lambda.Compile();
return func;
}
是这样使用的:
var sequence = ....
var filtered = sequence.IgnoreIfNull(x => x.Prop1.Prop2.Prop3 ... etc);
在 IEnumerable
上执行 Select
时,我认为检查空引用是一种很好的做法,所以我经常在 Select
之前有一个 Where
这个:
someEnumerable.Where(x => x != null).Select(x => x.SomeProperty);
访问子属性时会变得更加复杂:
someEnumerable.Where(x => x != null && x.SomeProperty != null).Select(x => x.SomeProperty.SomeOtherProperty);
要遵循此模式,我需要多次调用 Where
。我想在 IEnumerable
上创建一个扩展方法,它根据 Select
中引用的内容自动执行此类空值检查。像这样:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty);
someEnumerable.SelectWithNullCheck(x => x.SomeProperty.SomeOtherProperty);
这能做到吗?是fx吗?创建这样的扩展方法时,可以从 selector
参数中检索选定的属性吗?
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Where(THIS IS WHERE THE AUTOMATIC NULL-CHECKS HAPPEN).Select(selector);
}
编辑:我使用 C# 5.0 和 .NET Framework 4.5
为什么不使用 ?.
运算符?
someEnumerable.Where(x => x?.SomeProperty != null).Select(x => x.SomeProperty.SomeOtherProperty);
(注意这可能 return 空值)
或
someEnumerable.Select(x => x?.SomeProperty?.SomeOtherProperty).Where(x => x != null);
(这不会 return 任何空值)
这并不是什么好事或坏事,这取决于你想要什么 return
由于您使用的是 C# 5.0,因此您可以按以下方式编写扩展方法:
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector)
{
return source.Where(x => x != null).Select(selector).Where(x => x != null);
}
投影前后 (Select call)
应用检查结果不为空。
那么用法将是:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty)
.SelectWithNullCheck(y => y.SomeOtherProperty);
请注意,每次调用中的项目类型都不同。
如果你确实想要类似这样的:
someEnumerable.SelectWithNullCheck(x => x.SomeProperty.SomeOtherProperty);
那么您需要按照@Treziac 的建议使用 ?.
运算符(在 C# 6.0 中引入),然后过滤掉空值:
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Select(selector).Where( x=> x != null);
}
someEnumerable.SelectWithNullCheck(x => x?.SomeProperty?.SomeOtherProperty);
另一种选择是将选择空值检查拆分为自定义运算符(例如 WhereNotNull
)。将它与 ?.
运算符结合起来,以一种非常有表现力的方式解决你的问题。
public static IEnumerable<TSource> WhereNotNull<TSource>(this IEnumerable<TSource> source)
{
return source.Where(x=> x != null);
}
这允许你写:
someEnumerable.Select(x => x?.SomeProperty?.SomeOtherProperty)
.WhereNotNull();
如果不是,您可以随时链接 selects
(对于 C# 6 之前的版本):
someEnumerable.Select(x => x.SomeProperty)
.Select(x => x.SomeOtherProperty)
.WhereNotNull();
鉴于您绝对想访问 x.SomeProperty.SomeOtherProperty
,最后一个选择是赶上 NullReferenceException
。
public static IEnumerable<TResult> SelectWithNullCheck<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)
{
return source.Select(x =>
{
try
{
return selector(x);
}
catch(NullReferenceException ex)
{
return default(TResult);
}
})
.Where(x=> default(TResult) != x);
}
您可以使用基于 Expression 的解决方案。以下是 field / 属性 链调用的基本可行解决方案。它适用于非常深的调用链。它并不完美。例如,如果链中有 方法调用 将无法工作 (obj.Prop1.MethodCall() .Prop2).
基于表达式的解决方案通常较慢,因为需要编译 lambda 表达式来委托,应该考虑到这一点.
性能统计数据:
使用嵌套调用级别为 2 (obj.Prop1.Prop2) 的 200k 对象集合进行测试,其中所有对象都因条件而失败。
LINQ C# 6 在哪里?。运算符:2 - 4 毫秒
基于异常(尝试/捕获):14,000 - 15,000 毫秒
基于表达式:4 - 10 毫秒
注意:基于表达式的解决方案每次调用都会增加几毫秒的开销,这个数字不依赖于集合大小,因为表达式会为每个调用编译呼叫这是一项昂贵的操作。有兴趣的可以考虑缓存机制
基于表达式的解决方案来源::
public static IEnumerable<T> IgnoreIfNull<T, TProp>(this IEnumerable<T> sequence, Expression<Func<T, TProp>> expression)
{
var predicate = BuildNotNullPredicate(expression);
return sequence.Where(predicate);
}
private static Func<T, bool> BuildNotNullPredicate<T, TProp>(Expression<Func<T, TProp>> expression)
{
var root = expression.Body;
if (root.NodeType == ExpressionType.Parameter)
{
return t => t != null;
}
var pAccessMembers = new List<Expression>();
while (root.NodeType == ExpressionType.MemberAccess)
{
var mExpression = root as MemberExpression;
pAccessMembers.Add(mExpression);
root = mExpression.Expression;
}
pAccessMembers.Reverse();
var body = pAccessMembers
.Aggregate(
(Expression)Expression.Constant(true),
(f, s) =>
{
if (s.Type.IsValueType)
{
return f;
}
return Expression.AndAlso(
left: f,
right: Expression.NotEqual(s, Expression.Constant(null))
);
});
var lambda = Expression.Lambda<Func<T, bool>>(body, expression.Parameters[0]);
var func = lambda.Compile();
return func;
}
是这样使用的:
var sequence = ....
var filtered = sequence.IgnoreIfNull(x => x.Prop1.Prop2.Prop3 ... etc);