如何在深层Lambda表达式中检查null值?

16

如何在深层 Lambda 表达式中检查 null 值?

例如,假设我有一个嵌套多层的类结构,并且想要执行以下 Lambda 表达式:

x => x.Two.Three.Four.Foo

我希望它在Two、Three或Four为null时返回null,而不是抛出System.NullReferenceException异常。

public class Tests
{
    // This test will succeed
    [Fact]
    public void ReturnsValueWhenClass2NotNull()
    {
        var one = new One();
        one.Two = new Two();
        one.Two.Three = new Three();
        one.Two.Three.Four = new Four();
        one.Two.Three.Four.Foo = "blah";

        var result = GetValue(one, x => x.Two.Three.Four.Foo);

        Assert.Equal("blah", result);
    }

    // This test will fail
    [Fact]
    public void ReturnsNullWhenClass2IsNull()
    {
        var one = new One();

        var result = GetValue(one, x => x.Two.Three.Four.Foo);

        Assert.Equal(null, result);
    }

    private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression)
    {
        var func = expression.Compile();
        var value = func(model);
        return value;
    }

    public class One
    {
        public Two Two { get; set; }
    }

    public class Two
    {
        public Three Three { get; set; }
    }

    public class Three
    {
        public Four Four { get; set; }
    }

    public class Four
    {
        public string Foo { get; set; }
        public string Bar { get; set; }
    }
}

更新:

一种解决方法是像这样捕获NullReferenceException:

    private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression)
    {
        TResult value;
        try
        {
            var func = expression.Compile();
            value = func(model);
        }
        catch (NullReferenceException)
        {
            value = default(TResult);
        }
        return value;
    }

但我不希望因捕获我认为并非异常的异常而产生开销。我预计在我的领域中这种情况会经常出现。

更新2:

另一种解决方法是像这样修改属性获取器:

    public class One
    {
        private Two two;
        public Two Two
        {
            get
            {
                return two ?? new Two();
            }
            set
            {
                two = value;
            }
        }
    }

对于我的领域来说大部分都没问题,但有时我确实希望一个属性返回null。我查看了Josh E的回答,认为在某些情况下它非常接近我需要的。

10个回答

19
您可以使用通用的帮助扩展方法来实现此功能,例如:
public static class Get {
    public static T IfNotNull<T, U>(this U item, Func<U, T> lambda) where U: class {
        if (item == null) {
            return default(T);
        }
        return lambda(item);
    }
}

var one = new One();
string fooIfNotNull = one.IfNotNull(x => x.Two).IfNotNull(x => x.Three).IfNotNull(x => x.Four).IfNotNull(x => x.Foo);

在这种情况下,我想返回类型Foo或Bar的默认值。我真正想避免的是如果表达式树中更高层次的某些内容为空时抛出异常。 - JohnRudolfLewis
我编辑了我的答案并添加了一段代码示例,它可以成功编译并且应该能解决问题。 - Lucero
1
那么简单?那么优雅?+1 而且没有反射带来的运行时性能损失。 有人对比过这个解决方案和Gabe的解决方案或者“正常”的方法吗? - Simon D.

16

你不能以简明的方式完成这个操作。你可以将lambda函数拆分成多行,或使用嵌套的三目运算符:

var result = GetValue(one, x => x.Two == null ? null :
                                x.Two.Three == null ? null :
                                x.Two.Three.Four == null ? null :
                                x.Two.Three.Four.Foo;

很丑,我知道。


2
这是更加简洁的写法。Maybe(x=>x.Two.Three.Four.Foo); 详情请见http://maybe.codeplex.com/ - Maslow

7

简洁地完成这个任务需要一个尚未实现的运算符。我们曾考虑在C# 4.0中添加一个".?"运算符,它将具有您所期望的语义,但不幸的是,它并未适应我们的预算。我们将在假设的将来版本的语言中考虑它。


这太棒了!Delphi Prism使用其“:”运算符执行相同的操作:http://prismwiki.codegear.com/en/Colon_Operator - Gabe Moothart
我赞同。这会让很多代码变得更加简洁! - mdonatas
这个功能现在已经在C# 6中了! - Gabe Moothart
我尝试在这个问题中使用"?.",但是不被允许...它导致错误Error CS8072 An expression tree lambda may not contain a null propagating operator.关于此的一些讨论:Null-propagating operator ?. in Expression Trees - Alielson Piffer

4

现在您可以在codeplex上使用Maybe项目。

语法为:

string result = One.Maybe(o => o.Two.Three.Four.Foo);

string cityName = Employee.Maybe(e => e.Person.Address.CityName);

使用表达式树会影响性能吗? - Michael Freidgeim
3
做不同的事情会改变性能特征吗?是的。但是否足够让您或用户注意到呢?我不知道,需要对您的使用情况进行分析。 - Maslow
你有你(或其他人)使用的任何性能统计数据吗? - Michael Freidgeim
2
我不会。如果你尝试了,或许可以和我分享一下结果?=) - Maslow

3
我已经编写了一个扩展方法,可以让您实现这个功能:
blah.GetValueOrDefault(x => x.Two.Three.Four.Foo);

它使用表达式树在每个节点检查 null 条件,然后返回表达式值;创建的表达式树会被编译成一个函数并缓存,所以对于相同调用的后续使用应该运行在几乎本地的速度。
如果需要,您还可以传入默认值来返回:
blah.GetValueOrDefault(x => x.Two.Three.Four.Foo, Foo.Empty);

我已经写了一篇关于它的博客,点击这里查看。


2
我不擅长C#,但也许有一些方法可以实现来自Ruby的“andand”模式,它可以解决这个问题而不会污染实现。
这个概念在Haskell中也被称为Maybe Monad。 this文章的标题似乎很有前途。

1
有趣,这篇文章几乎和我自己思考出来的解决方案一模一样,看看我的帖子... - Lucero
哇,完全错过了,我想我是在寻找“可能”这个词。 - krusty.ar
http://maybe.codeplex.com/可以做到。 - Maslow

0
您可以修改您的getter方法,使其读取类似以下内容:
private Two _two;
public Two Two
{
     get 
     {
       if (null == _two)
         return new Two();
       else
         return _two;
      }
}

修改实现以在客户端代码中节省一些行应该会触发各种警报。 - krusty.ar
1
我倾向于不认为这是一个问题:我会称其为防御性编程。上述代码确保属性的值永远不会为 null,而不与该属性/对象的任何使用者共享该知识。 - Josh E
1
如果我在_two为空的情况下不断调用Two,我会不断获得新的Two实例...呕。 - Lucas
1
好的点子。在这种情况下,您可以修改它以将_two设置为一个新的Two()实例,然后返回它,例如:if (null == _two) _two = new Two();return _two; - Josh E

0

我将一个使用了很多if语句来避免空值的函数转换为使用.IFNotNull方法的类,这些类是从XSD转换而来,深度为4到5级。

以下是转换后代码的几行:

ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_VALUE = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.VALUE).ToDouble();  
ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_UOM = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.UOM);  

以下是一些有趣的统计数据:

1)这种新方法运行时间比使用If语句的变体长了3.7409倍。
2)我将函数行数从157减少到59。
3)DevExpress的CodeRush具有“维护复杂度”评分。当我转换为Lambda语句时,它从984增加到2076,理论上更难维护。


0

在使用属性之前,始终对其进行初始化。向类 One、Two、Three 和 Four 添加构造函数。在构造函数中初始化属性,以确保它们不为 null。


我通常会这样做,但在这个领域中,属性有时可能会被设置为null。这是有效的行为。 - JohnRudolfLewis

0

我有时会发现 coalesce 运算符对此非常有用。但是,只有在你可以放入一个默认/空等效版本的对象时,它才有所帮助。

例如,有时候当我打开 XML 文件时...

IEnumeratable<XElement> sample;
sample.Where(S => (S.Attribute["name"] ?? new XAttribute("name","")).Value.StartsWith("Hello"))...

根据默认对象的检索方式,这可能会很冗长,上面的示例并不是一个很好的用法,但你可以理解。对于读取XML属性的特定情况,我有一个扩展方法,它返回属性值或空字符串。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接