检查对象层次结构中的null

时间:2009-11-04 13:04:29

标签: c# c#-3.0

我有一个源自反序列化XML文档的大型C#(3.0)对象结构。我需要知道层次结构中的深度变量是否为空。我现在这样做的方法是在向下检查每个父对象为null,但这会导致if语句的长时间重复。

我正在努力避免使用昂贵的try-catch块。

有更聪明的方法吗?

修改 例如,在将XML应用程序表反序列化为对象层次结构后,可能会有一个工资值

applicationForm.employeeInfo.workingConditions.salary

但为了安全地找到我必须写一些类似

的东西
if (applicationForm.employeeInfo != null)
  if (applicationForm.employeeInfo.workingConditions != null)
    if (applicationForm.employeeInfo.workingConditions.salary != null)

因为如果其中一个父对象为null,那么简单地使用后者if语句当然会失败。

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

12 个答案:

答案 0 :(得分:5)

首先,如果您在多个地方重复逻辑,请将其封装在方法中。

其次,你不需要很多if语句,只需要一个有很多OR条件的语句:

if(parent==null || 
   parent.Child == null || 
   parent.Child.GrandChild == null ...

第三,“避免使用昂贵的try / catch块”可能是一种过早的优化,具体取决于您的场景。你是否真的试过这个并对它进行了分析,它是否真的会造成很大的开销?

答案 1 :(得分:5)

您遇到了A.B.C.D中每个步骤可能产生null的经典情况。虽然这是一种常见的情况,但令人惊讶的是,没有解决它的常见模式,除了使用带有大量或(||)的大型if语句。

如果每个步骤都可以返回不同的类,那么可以应用很少使用的模式:使用带有方法链接的通用扩展方法

广义扩展方法不是一个固定的术语,但我在这里使用它来强调ext。方法适用于所有类型的对象,因此 generalized 。根据{{​​3}},这是糟糕的设计。但是在某些偏远的情况下,就像你的一样,你可以使用它,只要你知道你正在做什么以及为什么。

技巧很简单:定义一个通用的扩展方法,并为具有默认构造函数的所有类进行推广。如果测试失败(对象为空),则该方法返回相同类型的新对象。否则,它将返回未更改的对象本身。

为什么这个对您的方案来说是一个好方法?因为您不需要更改任何现有的类,因为它可以理解并促进可读代码,因为它保持类型安全(编译时错误而不是运行时错误),并且与其他方法相比,它更简洁。

// 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 :)

这种方法的优点在于它适用于所有类型的类(假设它们具有ctor),包括集合和数组。如果任何步骤是索引步骤,它仍然有效(取决于集合,无效索引可以返回null,默认或引发异常):

var x = 
    someObj.SelfOrDefault()
    .Collection.SelfOrDefault()
    .Items[1].SelfOrDefault()
    .Mother.SelfOrDefault()
    .Father.SelfOrDefault();

更新:扩展了一点并添加了更详细的示例
更新: NotNull 重命名为SelfOrDefault,这意味着布尔值,遵循LINQ的命名约定(FirstOrDefault等)并隐含了什么它确实。
更新:重写并重新组织,使代码更适用,希望整体更容易理解:)

答案 2 :(得分:3)

您可以嵌套三元运算符。仍然很痛苦,但没有嵌套ifs那么糟糕。

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

答案 3 :(得分:2)

你不能迭代吗?

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

答案 4 :(得分:1)

使用反射。

创建一个辅助方法,该方法为您的属性名称提供父对象和分层点标记字符串。然后使用PropertyInfo并使用递归在每次检查null时一次关闭一个属性,如果是,则返回null,否则继续沿着层次结构。

答案 5 :(得分:1)

由于你没有提供太多细节,我不得不填写很多空白。这是一个psuo-codeish示例,说明如何通过递归实现此目的

    public bool doesHierarchyContainANull(MyObject ParentObject)
    {

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

        return doesHierarchyContainANull(ParentObject.getNextInHierarchy());

    }

答案 6 :(得分:1)

这是我的简单解决方案:

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

此处任何这些对象(applicationForm、employeeInfo、workingConditions、salary)都可以为 null,并且它会正确解析。

答案 7 :(得分:0)

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

答案 8 :(得分:0)

您需要一个递归函数来遍历结构并检查每个节点及其子节点是否为null。我正在研究一个样本,但IE崩溃了(典型!!)。稍后会发布一个。

示例

你可以做一些简单的事情(假设你只想检查结构是否有效),如下所示:

public void ValidateStructure()
{
    Node root = // get the root node
    try
    {
        ValidateChildren(root);
        Console.WriteLine("All nodes are valid");
    }
    catch (NullReferenceException)
    {
        Console.WriteLine("Structure contained a null node.");
    }

}

public void ValidateChildren(Node parent)
{
    // an NullReferenceException would be raised here since parent would be null 
    foreach (var child in parent.Children)
    {
        ValidateChildren(child);
    }
}

答案 9 :(得分:0)

我的解决方案如下:

public static TResult SafeGet<TSource, TResult>(this TSource source, Func<TSource, TResult> getResult) {
    if (source == null)
        return default(TResult);
    try {
        return getResult(source);
    }
    catch {
        return default(TResult);
    }
}

用法:

Test myTestObject = null;
var myStringOrNull = myTestObject.SafeGet(x => x.test.test.test.mySring);

答案 10 :(得分:0)

我喜欢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

空条件运算符表现出短路行为,只有当原始接收器不为空时,才会执行紧随其后的成员访问链,元素访问和调用:

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

这个例子基本上等同于:

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

除了customers仅评估一次。除非?具有非空值,否则不会执行紧跟customers之后的成员访问,元素访问和调用。

当然,无条件运算符本身可以被链接,以防需要在链中多次检查null:

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

请注意,调用(带括号的参数列表)不能紧跟?运算符 - 这会导致语法含糊不清。因此,只有当代表在那里工作时,才能直接调用代理。但是,您可以通过委托上的Invoke方法执行此操作:

if (predicate?.Invoke(e) ?? false) { … }

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

PropertyChanged?.Invoke(this, args);

这是一种在触发事件之前检查null的简单且线程安全的方法。它是线程安全的原因是该功能仅评估左侧一次,并将其保存在临时变量中。

答案 11 :(得分: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,而不抛出异常。

更好吗?不,只是为你节省了一点点重复,但看起来很酷。它还避免了创建所有未使用的&#34; OrDefault&#34;接受答案中的对象。