C#中检查属性的属性是否为空的优雅方式

126
在C#中,假设你想从这个例子中的PropertyC中获取一个值,而ObjectAPropertyAPropertyB都可以为空。
ObjectA.PropertyA.PropertyB.PropertyC

如何以最少的代码安全地获取PropertyC

目前我会检查:

if(ObjectA != null && ObjectA.PropertyA !=null && ObjectA.PropertyA.PropertyB != null)
{
    // safely pull off the value
    int value = objectA.PropertyA.PropertyB.PropertyC;
}

做类似于这样的事情会很不错(伪代码)。

int value = ObjectA.PropertyA.PropertyB ? ObjectA.PropertyA.PropertyB : defaultVal;

甚至可以使用空值合并运算符进一步简化。

编辑:最初我说我的第二个示例类似于js,但我将其更改为伪代码,因为正确指出它在js中不起作用。

20个回答

164

在C# 6中,您可以使用Null条件运算符。因此,原始测试将是:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;

5
你能解释一下这个代码的作用吗?如果 PropertyC 为空,value 等于什么?如果 PropertyB 为空呢?如果 Object A 为空呢? - Kellen Stuart
6
如果这些属性中的任何一个为 null,则整个语句将返回 null。从左到右进行判断。不使用语法糖,相当于一系列 if 语句,其中 if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......,最终的表达式是 if(propertyZ != null) { value = propertyZ } - DetectivePikachu
@DetectivePikachu - 或更简单的说, objectA == null || objectA.PropertyA == null || objectA.PropertyA.PropertyB == null ? null : objectA.PropertyA.PropertyB.PropertyC - ToolmakerSteve

28

简短的扩展方法:

public static TResult IfNotNull<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

使用

PropertyC value = ObjectA.IfNotNull(x => x.PropertyA).IfNotNull(x => x.PropertyB).IfNotNull(x => x.PropertyC);

你可以在http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/上找到这个简单的扩展方法以及更多的内容。

编辑:

使用一段时间后,我认为这个方法的适当名称应该是IfNotNull()而不是原来的With()。


16

您能在类中添加方法吗?如果不能,您是否考虑使用扩展方法?您可以为对象类型创建一个名为GetPropC()的扩展方法。

例如:

public static class MyExtensions
{
    public static int GetPropC(this MyObjectType obj, int defaltValue)
    {
        if (obj != null && obj.PropertyA != null & obj.PropertyA.PropertyB != null)
            return obj.PropertyA.PropertyB.PropertyC;
        return defaltValue;
    }
}

用法:

int val = ObjectA.GetPropC(0); // will return PropC value, or 0 (defaltValue)

顺便提一下,这假设你正在使用 .NET 3 或更高版本。


12
您正在做的方法是正确的。您可以使用像这里描述的技巧,使用Linq表达式:
int value = ObjectA.NullSafeEval(x => x.PropertyA.PropertyB.PropertyC, 0);

但是相较于手动检查每个属性,它要慢得多...


11

11

1
当您仅阅读属性时,我不认为对象图仅有三个级别需要重构。如果OP想要通过PropertyC引用的对象调用方法,则我会同意,但是当它只是需要在读取之前检查null的属性时则不需要。在这个例子中,它可能就像Customer.Address.Country一样简单,其中Country可以是KeyValuePair等引用类型。您将如何重构此代码以避免需要进行null引用检查? - Darren Lewis
OP的例子实际上是4层嵌套。我的建议不是删除空引用检查,而是将它们定位在最有可能正确处理它们的对象中。像大多数“经验法则”一样,也有例外,但我并不认为这是一个例外。我们能否达成不同意见的共识? - rtalbot
4
我同意@rtalbot的观点(尽管公平地说,@Daz Lewis提出了一个四层嵌套的例子,因为最后一项是一个KeyValuePair)。如果某个东西正在处理客户对象,那么我不明白它为什么要查看Address对象继承结构。假设以后你决定KeyValuePair对于Country属性并不是一个好主意。那么,每个人的代码都必须更改。这不是一个好的设计。 - Jeffrey L Whitledge

8

显然,您正在寻找Nullable Monad

string result = new A().PropertyB.PropertyC.Value;

变成

string result = from a in new A()
                from b in a.PropertyB
                from c in b.PropertyC
                select c.Value;

如果可空属性中有任何一个为null,则返回null;否则返回Value的值。

class A { public B PropertyB { get; set; } }
class B { public C PropertyC { get; set; } }
class C { public string Value { get; set; } }

LINQ扩展方法:
public static class NullableExtensions
{
    public static TResult SelectMany<TOuter, TInner, TResult>(
        this TOuter source,
        Func<TOuter, TInner> innerSelector,
        Func<TOuter, TInner, TResult> resultSelector)
        where TOuter : class
        where TInner : class
        where TResult : class
    {
        if (source == null) return null;
        TInner inner = innerSelector(source);
        if (inner == null) return null;
        return resultSelector(source, inner);
    }
}

为什么这里有扩展方法?它没有被使用。 - Mladen Mihajlovic
1
@MladenMihajlovic:SelectMany 扩展方法被 from ... in ... from ... in ... 语法所使用。 - dtb

6
假设您有类型为空的值,一个方法是这样的:
var x = (((objectA ?? A.Empty).PropertyOfB ?? B.Empty).PropertyOfC ?? C.Empty).PropertyOfString;

我非常喜欢C#,但是Java新版本(1.7?)中很好的一个特性是 .? 操作符:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;

1
它真的会在Java 1.7中吗?虽然C#已经要求很长时间了,但我怀疑它是否会实现... - Thomas Levesque
很遗憾,我没有空值。不过那个Java语法看起来很棒!我会点赞的,因为我想要那个语法! - Jon Kragh
3
Thomas: 上次我查看 http://tech.puredanger.com/java7/ 时,它暗示Java会得到它。但现在当我重新检查时,它说:空指针安全处理:否。因此,我撤销我的陈述并用一个新的陈述来取代它:它被建议用于Java 1.7,但没有实现。 - Just another metaprogrammer
另一种方法是由monad.net使用的方法。 - Just another metaprogrammer
1
似乎 Visual Studio 2015 中有 ?. 运算符 [https://msdn.microsoft.com/en-us/library/dn986595.aspx] (https://msdn.microsoft.com/en-us/library/dn986595.aspx)。 - Edward

5

我看到了C# 6.0中的一些新特性,可以使用'?'来代替检查语句。

例如,可以用以下代码:

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
  var city = person.contact.address.city;
}

你只需要使用
var city = person?.contact?.address?.city;

我希望能帮到某些人。
更新:
现在可以这样做
 var city = (Person != null)? 
           ((Person.Contact!=null)? 
              ((Person.Contact.Address!= null)?
                      ((Person.Contact.Address.City!=null)? 
                                 Person.Contact.Address.City : null )
                       :null)
               :null)
            : null;

4

这段代码是“最少量的代码”,但并不是最佳实践:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}

1
我经常看到这样的代码,尽管它会导致性能损失,但最大的问题是它会使调试变得复杂,因为真正的异常会淹没在数百万个无用的空引用异常中。 - Just another metaprogrammer
有时候读自己三年前的答案还是挺有趣的。我想,今天我会给出不同的答案。我会说这段代码违反了 Demeter 法则,我会建议重构它以避免这种情况。 - Boris Modylevsky
1
截至今天,在原回答7年后,我会加入@Phillip Ngan并使用以下语法的C# 6:int?value = objectA?.PropertyA?.PropertyB?.PropertyC; - Boris Modylevsky

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