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