在对象层次结构中检查空值

7
我有一个大的C#(3.0)对象结构,来自反序列化的XML文档。我需要知道层次结构深处的变量是否为空。目前我的做法是检查下行路径上的每个父对象是否为空,但这会导致长时间重复的if语句。
我试图避免昂贵的try-catch块。
有没有更聪明的方法来做到这一点?
编辑: 例如,在将XML应用程序表单反序列化为对象层次结构后,可能会在其中找到薪资值。
applicationForm.employeeInfo.workingConditions.salary

但是为了安全起见,我需要写出类似以下的内容:

if (applicationForm.employeeInfo != null)
  if (applicationForm.employeeInfo.workingConditions != null)
    if (applicationForm.employeeInfo.workingConditions.salary != null)

因为仅仅使用后面的if语句会在其中一个父对象为空时失败。

所以我正在寻找任何更智能的方法来处理这种情况。


4
你能否提供一些示例代码,以明确你想要做什么? - Rik
反序列化XML非常耗费资源,而且.NET代码已经有很多try/catch块了,再加一个也无妨。如果“贵重”对于你的其他代码没有影响,为什么要避免它呢? - Abel
你需要知道的是特定变量是否为空,还是层次结构中的任何变量是否为空? - Jeff Sternal
1
谢谢你的更新。在我看来,方法链将对你有所帮助(请参见我的答案),因为它不依赖于固定顺序(但涉及一些高级概念,如果你是C#新手可能会有挑战)。或者,你可以使用短路||(Winston)或反射(Wim,也是高级的,但你会失去类型安全性)。我有没有忘记什么?递归方法不适合你的需求,但考虑到最初提供的信息很少,没有人能知道。 - Abel
新运算符的工作原理 考虑像这样获取父对象的孙子对象:var g1 = parent?.child?.child?.child; if (g1 != null) // TODOhttps://github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#null-conditional-operators - John Arundell
显示剩余3条评论
12个回答

7
您遇到了经典情况,即 A.B.C.D 中的每个步骤都可能返回 null。虽然这是一个常见的场景,但令人惊讶的是,除了使用大量或运算符(||)的 if 语句外,没有通用模式来解决它。
如果每个步骤可以返回不同的类,则有一种很少使用的模式可供您应用:使用通用泛型扩展方法和方法链接。
“通用”扩展方法不是固定术语,但在此处我使用它来强调该扩展方法适用于几乎所有类型的对象,因此是“通用”的。根据Bill Wagner in Effective C#,这是糟糕的设计。但在某些偏远情况下,就像您的情况一样,只要您知道自己在做什么以及为什么,就可以使用它。
“诀窍”很简单:定义一个泛型扩展方法,并将其推广到具有默认构造函数的所有类。如果测试失败(对象为 null),则该方法返回相同类型的新对象。否则,它将返回未更改的对象本身。
为什么这是您情境下的一个好方法?因为您不需要更改任何现有类,因为它易于理解并促进可读代码,因为它保持类型安全(编译时错误而不是运行时错误),并且与其他方法相比更加简洁。
// extension method:
public static class SomeExtentionMethods
{
    public static T SelfOrDefault<T>(this T elem)
        where T : class, new()     /* must be class, must have ctor */
    {
        return elem ?? new T();    /* return self or new instance of T if null */
    }
}

// your code now becomes very easily readable:
Obj someObj = getYourObjectFromDeserializing();

// this is it applied to your code:
var mySalary = applicationForm.SelfOrDefault().
    employeeInfo.SelfOrDefault().
    workingConditions.SelfOrDefault().
    salary;

// now test with one if-statement:
if(mySalary.IsEmpty())
   // something in the chain was empty
else
   // all's well that ends well :)

这种方法的优点在于它适用于所有类型的类(只要它们有构造函数),包括集合和数组。如果任何步骤是索引步骤,它仍然可以工作(根据集合,无效索引可能返回null、默认值或引发异常):
var x = 
    someObj.SelfOrDefault()
    .Collection.SelfOrDefault()
    .Items[1].SelfOrDefault()
    .Mother.SelfOrDefault()
    .Father.SelfOrDefault();

更新:进行了扩展并添加了更详细的示例
更新:NotNull重命名为SelfOrDefault,这符合LINQ的命名约定(FirstOrDefault等),并且说明了它的作用。
更新:重写和重新组织代码,使其更具适用性,希望整体上更易于理解 :)


是的,我被教导尽量避免方法链式调用。 - James
然而,尽管每个 Fluent 接口都是纯方法链接(例如 FluentNHibernate、许多 Config API),但皱眉的原因来自于一个基本上可以在每个类或类型上使用的扩展方法:除了一些罕见的情况外,这并不好。 - Abel
有没有一种方法可以使其在不需要默认构造函数的情况下工作?这似乎是一个很大的限制。 - Dax Fohl
@DaxFohl,你应该返回一个类的(默认)实例,以防止返回null。如果没有new(),则无法使用泛型完成此操作。另一种选择是使用静态访问器,例如MyClass.Create,但与Java不同,.NET不允许静态成员成为接口的一部分,因此除非您想通过反射来解决此问题,否则没有通用的方法可以做到这一点。简单的方法是为您想要以这种方式处理的每个类都有一个默认构造函数。 - Abel
@James,另一种选择是使用一个接口,比如说INullSafe,它可以是一个空接口(也称为标记接口),可以在上面的通用方法的where子句中使用。这样你就只限制了那些实现了INullSafe的类,但这意味着你将无法与任何内置的BCL类一起使用它。 - Abel

5
首先,如果您在多个地方重复使用同一逻辑,请封装到一个方法中。
其次,您不需要大量的if语句,只需要一个带有许多OR条件的语句即可:
if(parent==null || 
   parent.Child == null || 
   parent.Child.GrandChild == null ...

第三点,“避免昂贵的try/catch块”可能是一种过早的优化,这取决于您的情况。您是否尝试过并对其进行了剖析,它确实会导致大量开销?


1
“避免”try/catch的目的是为了避免使用catch来识别值为空的情况,如果确实是这种情况,那么它并不算是过早的解决问题,因为这是一种昂贵的解决问题的方式。 - Murph
这段代码相对于适当的if语句的成本来说是昂贵的,但它是否足够昂贵以至于有所影响呢?例如,如果这段代码只执行一次,比如在显示窗口时,那么它可能不会有任何区别,但如果它在渲染大型集合中的每个列表项时都被执行,那么这可能是一个糟糕的想法。所以这可能确实是过早的判断——只有原帖作者才能回答这个问题。 - Winston Smith
请记住,OP提到的是层次结构,而不是树形结构。因此,它可能类似于CreateAccountRequest.Data.Account.Facilities.Statement.Format。 - Winston Smith
你的结构不一定需要是树形结构才能有递归解决方案。 - James
我相信如果发帖者(lox,请?)花点时间帮助我们所有人并更新他的问题,这将会很有帮助…… - Abel
显示剩余4条评论

4

你可以嵌套三元运算符。虽然仍然有些麻烦,但不像嵌套if语句那么糟糕。

string salary = (applicationForm.employeeInfo == null) ? null :
                (applicationForm.employeeInfo.workingConditions == null) ? null :
                applicationForm.employeeInfo.workingConditions.salary;

如果你只想知道它是否为空:

bool hasSalary = (applicationForm.employeeInfo == null) ? false :
                 (applicationForm.employeeInfo.workingConditions == null) ? false :
                 (applicationForm.employeeInfo.workingConditions.salary != null);

4

以下是我的简单解决方案:

if (applicationForm?.employeeInfo?.workingConditions?.salary != null)

任何这些对象(applicationForm, employeeInfo, workingConditions, salary)都可以为null,且不会出现错误。

2

你不能迭代吗?

for (SomeObject obj = someInstance; obj != null; obj = obj.Parent) {
    //do something
}

这个解决方案假设起点是潜在缺失的对象,因此 obj != null 会产生相同的效果。问题在于首先要到达 obj - GraemeF
应该反过来,我猜:最后一部分可能是 obj.Child,但这假设有一个访问器可以到达子级,并且链中的所有对象共享相同的类型。 - Abel

1

我喜欢Pontus Bremdahl的答案,但为了我的需要添加了更多细节。

代码:

    /// <summary>
    /// Get a member in an object hierarchy that might contain null references.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="source">Base object to get member from.</param>
    /// <param name="getResult">Member path.</param>
    /// <param name="defaultResult">Returned object if object hierarchy is null.</param>
    /// <returns>Default of requested member type.</returns>
    public TResult SafeGet<TSource, TResult>(TSource source, Func<TSource, TResult> getResult, TResult defaultResult)
    {
        // Use EqualityComparer because TSource could by a primitive type.
        if (EqualityComparer<TSource>.Default.Equals(source, default(TSource)))
            return defaultResult;
        try
        {
            return getResult(source);
        }
        catch
        {
            return defaultResult;
        }
    }
    /// <summary>
    /// Get a member in an object hierarchy that might contain null references.
    /// </summary>
    /// <typeparam name="TSource"></typeparam>
    /// <typeparam name="TResult"></typeparam>
    /// <param name="source">Base object to get member from.</param>
    /// <param name="getResult">Member path.</param>
    /// <returns>Default of requested member type.</returns>
    public TResult SafeGet<TSource, TResult>(TSource source, Func<TSource, TResult> getResult)
    {
        // Use EqualityComparer because TSource could by a primitive type.
        if (EqualityComparer<TSource>.Default.Equals(source, default(TSource)))
            return default(TResult);
        try
        {
            return getResult(source);
        }
        catch
        {
            return default(TResult);
        }
    }

使用方法:

// Only authenticated users can run this code
if (!HttpContext.Current.SafeGet(s => s.User.Identity.IsAuthenticated))
        return;

// Get count limit from app.config
var countLimit = int.Parse(ConfigurationManager.AppSettings.SafeGet(
    s => s.Get("countLimit"),
      "100" // Default 100 if no value is present
    ));

// Is int 6 a class? Always no, but just to show primitive type usage.
var is6AClass = 6.SafeGet(i => i.GetType().IsClass);

更新

CSharp 6 版本现在已经内置此功能。 https://github.com/dotnet/roslyn/wiki/New-Language-Features-in-C%23-6#null-conditional-operators

空值条件运算符

有时代码会淹没在 null 检查中。空值条件运算符允许您仅在接收器不为 null 时访问成员和元素,否则提供 null 结果:

int? length = customers?.Length; // null if customers is null
Customer first = customers?[0];  // null if customers is null

空值条件运算符通常与空值合并运算符 ?? 一起使用:
int length = customers?.Length ?? 0; // 0 if customers is null

空值条件运算符表现出短路行为,即仅当原始接收器不为 null 时,后续成员访问、元素访问和调用链才会被执行:

int? first = customers?[0].Orders.Count();

这个例子基本等同于:
int? first = (customers != null) ? customers[0].Orders.Count() : null;

除了 customers 只会被计算一次。在 ? 之后立即执行的成员访问、元素访问和调用都不会被执行,除非 customers 具有非空值。
当然,如果需要在链中多次检查 null 值,则可以链接使用 null-conditional 运算符:
int? first = customers?[0].Orders?.Count();

注意,调用(括号中的参数列表)不能紧跟在 ? 运算符后面 - 这会导致太多的语法歧义。因此,直接调用委托只有在它存在时才能起作用的简单方法不起作用。但是,您可以通过委托上的 Invoke 方法来实现。
if (predicate?.Invoke(e) ?? false) { … }

我们预计这种模式的一个非常常见的用途是触发事件:

PropertyChanged?.Invoke(this, args);

这是一种易于实现且线程安全的方式,在触发事件之前检查null。它之所以线程安全是因为该特性只评估左侧一次,并将其保存在临时变量中。


1

由于您没有提供太多细节,我不得不填补很多空白。以下是一个类似伪代码的例子,展示了如何通过递归来实现此操作。

    public bool doesHierarchyContainANull(MyObject ParentObject)
    {

        if (ParentObject.getMemberToCheckForNull() == null)
            return true;
        else if (ParentObject.isLastInHierarchy())
            return false;

        return doesHierarchyContainANull(ParentObject.getNextInHierarchy());

    }

1

使用反射。

创建一个辅助方法,该方法接受一个父对象和一个用点符号表示的属性名称的层次结构字符串。然后使用 PropertyInfo 和递归来一次处理一个属性,每次检查是否为 null,并在是的情况下返回 null,否则继续向下处理层次结构。


是的,但这并不需要比if语句少的代码,并且会慢上一个数量级。 - Winston Smith
好的,这是一种通用且可重复使用的方式,您可以在任何地方应用它。这就是我认为 OP 追求的东西。至于“数量级”慢的问题,用户不会注意到。反射被低估了。;-) - Wim
2
反射对于这种任务非常出色。我建议,不要使用点表示法字符串,而是迭代所有公共属性怎么样?如果您一次性获取所有方法或属性,然后对它们进行迭代(而不是尝试按名称绑定每个名称),那么反射会相当快。 - Abel
已经完成了;-) 现在我们终于有了问题的更新,我担心反射不再是最美观或类型安全的解决方案。看看我的方法链接示例。目前,我想不出更好的办法(当然,这是我的想法!)。 - Abel

0

使用 Null monad。它可以在同一文件或不同文件中,只要您在代码中 using 它。

public static class NullMonad {
    public static TResult SelectMany<TIn, TOut, TResult>(this TIn @in, Func<TIn, TOut> remainder, Func<TIn, TOut, TResult> resultSelector)
        where TIn : class
        where TOut : class
        where TResult : class {
        var @out = @in != null ? remainder(@in) : null;
        return @out != null ? resultSelector(@in, @out) : null;
    }
}

然后您可以使用LINQ:

var salary = from form in applicationForm
             from info in form.employeeInfo
             from cond in info.workingConditions
             select cond.salary

如果存在薪水,返回薪水;如果之前任何语句的结果为null,则返回null,而不会抛出异常。

这样做真的会好很多吗?并没有太大的改进,只是避免了重复的代码,但看起来很酷。它还避免了在接受的答案中创建所有未使用的“OrDefault”对象所带来的开销。


0
CheckForNull(MyType element)
{
    if(element.ChildElement != null)
    {
        CheckForNull(element.ChildElement);
    }
    else
    {
        Console.WriteLine("Null Found");
    }
}

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