更清洁的方法在C#中进行空检查?

时间:2013-07-16 09:13:38

标签: c# .net-4.0 null .net-4.5

假设我有这个界面,

interface IContact
{
    IAddress address { get; set; }
}

interface IAddress
{
    string city { get; set; }
}

class Person : IPerson
{
    public IContact contact { get; set; }
}

class test
{
    private test()
    {
        var person = new Person();
        if (person.contact.address.city != null)
        {
            //this will never work if contact is itself null?
        }
    }
}

Person.Contact.Address.City != null(这可以检查City是否为空。)

但是,如果Address或Contact或Person本身为null,则此检查失败。

目前,我能想到的一个解决方案是:

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)

{ 
    // Do some stuff here..
}

有更清洁的方法吗?

我真的不喜欢null检查(something == null)。相反,还有另一种很好的方式来做something.IsNull()方法吗?

19 个答案:

答案 0 :(得分:236)

以通用方式,您可以使用表达式树并使用扩展方法进行检查:

if (!person.IsNull(p => p.contact.address.city))
{
    //Nothing is null
}

完整代码:

public class IsNullVisitor : ExpressionVisitor
{
    public bool IsNull { get; private set; }
    public object CurrentObject { get; set; }

    protected override Expression VisitMember(MemberExpression node)
    {
        base.VisitMember(node);
        if (CheckNull())
        {
            return node;
        }

        var member = (PropertyInfo)node.Member;
        CurrentObject = member.GetValue(CurrentObject,null);
        CheckNull();
        return node;
    }

    private bool CheckNull()
    {
        if (CurrentObject == null)
        {
            IsNull = true;
        }
        return IsNull;
    }
}

public static class Helper
{
    public static bool IsNull<T>(this T root,Expression<Func<T, object>> getter)
    {
        var visitor = new IsNullVisitor();
        visitor.CurrentObject = root;
        visitor.Visit(getter);
        return visitor.IsNull;
    }
}

class Program
{
    static void Main(string[] args)
    {
        Person nullPerson = null;
        var isNull_0 = nullPerson.IsNull(p => p.contact.address.city);
        var isNull_1 = new Person().IsNull(p => p.contact.address.city);
        var isNull_2 = new Person { contact = new Contact() }.IsNull(p => p.contact.address.city);
        var isNull_3 =  new Person { contact = new Contact { address = new Address() } }.IsNull(p => p.contact.address.city);
        var notnull = new Person { contact = new Contact { address = new Address { city = "LONDON" } } }.IsNull(p => p.contact.address.city);
    }
}

答案 1 :(得分:62)

您的代码可能比需要检查空引用有更大的问题。就目前而言,您可能违反了Law of Demeter

Demeter法是其中一种启发式方法,比如“不要重复自己”,它可以帮助您编写易于维护的代码。它告诉程序员不要访问距离直接范围太远的任何东西。例如,假设我有这段代码:

public interface BusinessData {
  public decimal Money { get; set; }
}

public class BusinessCalculator : ICalculator {
  public BusinessData CalculateMoney() {
    // snip
  }
}

public BusinessController : IController {
  public void DoAnAction() {
    var businessDA = new BusinessCalculator().CalculateMoney();
    Console.WriteLine(businessDA.Money * 100d);
  }
}

DoAnAction方法违反了得墨忒耳法则。在一个函数中,它访问BusinessCalcualtorBusinessDatadecimal。这意味着如果进行以下任何更改,则必须重构该行:

  • BusinessCalculator.CalculateMoney()的返回类型更改。
  • BusinessData.Money的类型更改

考虑到现状,这些变化很可能发生。如果在整个代码库中编写这样的代码,那么进行这些更改可能会变得非常昂贵。除此之外,这意味着您的BusinessControllerBusinessCalculatorBusinessData类型相关联。

避免这种情况的一种方法是重写代码:

public class BusinessCalculator : ICalculator {
  private BusinessData CalculateMoney() {
    // snip
  }
  public decimal CalculateCents() {
    return CalculateMoney().Money * 100d;
  }
}

public BusinessController : IController {
  public void DoAnAction() {
    Console.WriteLine(new BusinessCalculator().CalculateCents());
  }
}

现在,如果您进行上述任一更改,则只需重构一段代码BusinessCalculator.CalculateCents()方法。您还消除了BusinessControllerBusinessData的依赖性。


您的代码遇到类似问题:

interface IContact
{
    IAddress address { get; set; }
}

interface IAddress
{
    string city { get; set; }
}

class Person : IPerson
{
    public IContact contact { get; set; }
}

class Test {
  public void Main() {
    var contact = new Person().contact;
    var address = contact.address;
    var city = address.city;
    Console.WriteLine(city);
  }
}

如果进行了以下任何更改,您将需要重构我编写的主要方法或您写的空检查:

  • IPerson.contact的类型更改
  • IContact.address的类型更改
  • IAddress.city的类型更改

我认为你应该考虑对代码进行更深入的重构,而不仅仅是重写一次空检查。


那就是说,我认为有时候遵循得墨忒耳法是不合适的。 (毕竟,它是一种启发式的,而不是一种强硬的规则,即使它被称为“法律”。)

特别是,我认为如果:

  1. 您有一些类代表存储在程序的持久层中的记录,AND
  2. 您非常有信心将来不需要重构这些课程,
  3. 在专门处理这些类时,忽略Demeter法则是可以接受的。这是因为它们代表了应用程序使用的数据,因此从一个数据对象到另一个数据对象是一种探索程序中信息的方法。在我上面的例子中,违反Demeter法则引起的耦合要严重得多:我从堆栈顶部附近的控制器一直到达堆栈中间的业务逻辑计算器到可能的数据类在持久层中。

    我将此可能的例外情况引入Demeter法则,因为PersonContactAddress这样的名称,您的类看起来可能是数据层POCO。如果是这种情况,并且您非常有信心将来永远不需要重构它们,那么您可能会在特定情况下无视Demeter法则。

答案 2 :(得分:48)

在您的情况下,您可以为人创建属性

public bool HasCity
{
   get 
   { 
     return (this.Contact!=null && this.Contact.Address!= null && this.Contact.Address.City != null); 
   }     
}

但你还是要检查人是否为空

if (person != null && person.HasCity)
{

}

对于你的另一个问题,对于字符串,你也可以用这种方式检查是否为null:

string s = string.Empty;
if (!string.IsNullOrEmpty(s))
{
   // string is not null and not empty
}
if (!string.IsNullOrWhiteSpace(s))
{
   // string is not null, not empty and not contains only white spaces
}

答案 3 :(得分:37)

完全不同的选项(我认为未充分利用)是null object pattern。在你的特定情况下很难判断它是否有意义,但它可能值得一试。简而言之,您将使用NullContact实施,NullAddress实施等等,而不是使用null。这样,你可以摆脱大多数的空检查,当然这是以牺牲你必须考虑到这些实现的设计为代价的。

正如亚当在评论中指出的那样,这可以让你写出

if (person.Contact.Address.City is NullCity)

在真的有必要的情况下。当然,这只有在城市确实是一个非平凡的对象时才有意义......

或者,可以将null对象实现为单例(例如,查看here以获取有关空对象模式的使用的一些实用指令,并将here实现为有关C#中单例的指令,允许你要使用古典比较。

if (person.Contact.Address.City == NullCity.Instance)

就个人而言,我更喜欢这种方法,因为我觉得对不熟悉模式的人来说更容易阅读。

答案 4 :(得分:26)

更新28/04/2014: Null propagation is planned for C# vNext


传播空检查存在更大的问题。瞄准可读代码,可由其他开发人员理解,虽然它很罗嗦 - 但您的示例很好。

如果是经常进行的检查,请考虑将其封装在Person类中作为属性或方法调用。


那就是说,无偿的Func和泛型!

我永远不会这样做,但这是另一种选择:

class NullHelper
{
    public static bool ChainNotNull<TFirst, TSecond, TThird, TFourth>(TFirst item1, Func<TFirst, TSecond> getItem2, Func<TSecond, TThird> getItem3, Func<TThird, TFourth> getItem4)
    {
        if (item1 == null)
            return false;

        var item2 = getItem2(item1);

        if (item2 == null)
            return false;

        var item3 = getItem3(item2);

        if (item3 == null)
            return false;

        var item4 = getItem4(item3);

        if (item4 == null)
            return false;

        return true;
    }
}

调用:

    static void Main(string[] args)
    {
        Person person = new Person { Address = new Address { PostCode = new Postcode { Value = "" } } };

        if (NullHelper.ChainNotNull(person, p => p.Address, a => a.PostCode, p => p.Value))
        {
            Console.WriteLine("Not null");
        }
        else
        {
            Console.WriteLine("null");
        }

        Console.ReadLine();
    }

答案 5 :(得分:14)

第二个问题,

  

我真的不喜欢将null检查作为(某些事情== null)。相反,还有另一种很好的方式来做像something.IsNull()方法吗?

可以使用扩展方法解决:

public static class Extensions
{
    public static bool IsNull<T>(this T source) where T : class
    {
        return source == null;
    }
}

答案 6 :(得分:10)

如果由于某种原因您不介意使用其中一个“超过顶级”的解决方案,您可能需要查看我的blog post中描述的解决方案。在评估表达式之前,它使用表达式树来确定值是否为null。但为了保持性能可接受,它会创建并缓存IL代码。

该解决方案允许您写下这个:

string city = person.NullSafeGet(n => n.Contact.Address.City);

答案 7 :(得分:7)

你可以写:

public static class Extensions
    {
        public static bool IsNull(this object obj)
        {
            return obj == null;
        }
    }

然后:

string s = null;
if(s.IsNull())
{

}

有时这是有道理的。但我个人会避免这样的事情...因为这不清楚为什么你可以调用一个实际为null的对象的方法。

答案 8 :(得分:5)

在单独的method中执行此操作:

private test()
{
    var person = new Person();
    if (!IsNull(person))
    {
        // Proceed
              ........

IsNull method

的位置
public bool IsNull(Person person)
{
    if(Person != null && 
       Person.Contact != null && 
       Person.Contact.Address != null && 
       Person.Contact.Address.City != null)
          return false;
    return true;
}

答案 9 :(得分:4)

您需要C#,还是只需要.NET?如果您可以混合使用另一种.NET语言,请查看Oxygene。它是一种令人惊叹的,非常现代的OO语言,它以.NET(以及Java和Cocoa为目标。是的。在本机上,它确实是一个非常棒的工具链。)

Oxygene有一个冒号运算符,它完全符合你的要求。引用他们的miscellaneous language features page

  

冒号(“:”)运算符

     

在Oxygene中,就像许多语言一样   受到“。”的影响。运算符用于调用类的成员   或对象,例如

var x := y.SomeProperty;
     

这“取消引用”中包含的对象   “y”,调用(在本例中)属性getter并返回其值。   如果“y”恰好是未分配的(即“nil”),则抛出异常。

     

“:”运算符的工作方式大致相同,但不是抛出   未分配对象的异常,结果将只是零。   对于来自Objective-C的开发人员来说,这将是熟悉的   是Objective-C方法使用[]语法工作的方式。

     

...(剪辑)

     

其中“:”真正闪耀的是访问链中的属性,其中   任何元素都可能是nil。例如,以下代码:

var y := MyForm:OkButton:Caption:Length;
     

将无误地运行,并且   如果链中的任何对象都是零,则返回nil - 形式,   按钮或其标题。

答案 10 :(得分:3)

try
{
  // do some stuff here
}
catch (NullReferenceException e)
{
}

实际上不要这样做。执行空检查,找出最适合的格式。

答案 11 :(得分:3)

我有一个可能对此有用的扩展名; ValueOrDefault()。它接受lambda语句并对其进行求值,如果抛出任何预期的异常(NRE或IOE),则返回计算值或默认值。

    /// <summary>
    /// Provides a null-safe member accessor that will return either the result of the lambda or the specified default value.
    /// </summary>
    /// <typeparam name="TIn">The type of the in.</typeparam>
    /// <typeparam name="TOut">The type of the out.</typeparam>
    /// <param name="input">The input.</param>
    /// <param name="projection">A lambda specifying the value to produce.</param>
    /// <param name="defaultValue">The default value to use if the projection or any parent is null.</param>
    /// <returns>the result of the lambda, or the specified default value if any reference in the lambda is null.</returns>
    public static TOut ValueOrDefault<TIn, TOut>(this TIn input, Func<TIn, TOut> projection, TOut defaultValue)
    {
        try
        {
            var result = projection(input);
            if (result == null) result = defaultValue;
            return result;
        }
        catch (NullReferenceException) //most reference types throw this on a null instance
        {
            return defaultValue;
        }
        catch (InvalidOperationException) //Nullable<T> throws this when accessing Value
        {
            return defaultValue;
        }
    }

    /// <summary>
    /// Provides a null-safe member accessor that will return either the result of the lambda or the default value for the type.
    /// </summary>
    /// <typeparam name="TIn">The type of the in.</typeparam>
    /// <typeparam name="TOut">The type of the out.</typeparam>
    /// <param name="input">The input.</param>
    /// <param name="projection">A lambda specifying the value to produce.</param>
    /// <returns>the result of the lambda, or default(TOut) if any reference in the lambda is null.</returns>
    public static TOut ValueOrDefault<TIn, TOut>(this TIn input, Func<TIn, TOut> projection)
    {
        return input.ValueOrDefault(projection, default(TOut));
    }

未采用特定默认值的重载将为任何引用类型返回null。这应该适用于您的场景:

class test
{
    private test()
    {
        var person = new Person();
        if (person.ValueOrDefault(p=>p.contact.address.city) != null)
        {
            //the above will return null without exception if any member in the chain is null
        }
    }
}

答案 12 :(得分:3)

例如,如果您使用ORM工具,并希望尽可能保持您的类纯净,则可能会出现此类参考链。在这种情况下,我认为不能很好地避免。

我有以下扩展方法“family”,它检查被调用的对象是否为null,如果没有,则返回其中一个请求的属性,或者使用它执行某些方法。这当然只适用于引用类型,这就是为什么我有相应的泛型约束。

public static TRet NullOr<T, TRet>(this T obj, Func<T, TRet> getter) where T : class
{
    return obj != null ? getter(obj) : default(TRet);
}

public static void NullOrDo<T>(this T obj, Action<T> action) where T : class
{
    if (obj != null)
        action(obj);
}

与手动解决方案(没有反射,没有表达式树)相比,这些方法几乎不增加任何开销,并且您可以使用它们(IMO)实现更好的语法。

var city = person.NullOr(e => e.Contact).NullOr(e => e.Address).NullOr(e => e.City);
if (city != null)
    // do something...

或使用方法:

person.NullOrDo(p => p.GoToWork());

然而,人们可以定义论代码长度没有太大变化。

答案 13 :(得分:2)

在我看来,等于运算符不是更安全和更好的方式用于引用相等。

使用ReferenceEquals(obj, null)总是更好。这将始终有效。另一方面,等于运算符(==)可能会被重载,并且可能正在检查值是否相等而不是引用,所以我会说ReferenceEquals()是一种更安全,更好的方法。

class MyClass {
   static void Main() {
      object o = null;
      object p = null;
      object q = new Object();

      Console.WriteLine(Object.ReferenceEquals(o, p));
      p = q;
      Console.WriteLine(Object.ReferenceEquals(p, q));
      Console.WriteLine(Object.ReferenceEquals(o, p));
   }
}

参考:MSDN文章 Object.ReferenceEquals Method

但这也是我对空值的想法

  • 通常,如果有人试图表明没有数据,则返回空值是最好的选择。

  • 如果对象不为null,但为空,则表示已返回数据,而返回null则表明没有返回任何内容。

  • 同样是IMO,如果你将返回null,如果你试图访问对象中的成员,它将导致一个空异常,这对于突出显示错误的代码很有用。

在C#中,有两种不同的等式:

  • 引用相等和
  • 价值平等。

当一个类型是不可变的时,重载operator ==来比较值的相等而不是引用相等可能是有用的。

不建议在非不可变类型中覆盖operator ==。

有关详细信息,请参阅MSDN文章 Guidelines for Overloading Equals() and Operator == (C# Programming Guide)

答案 14 :(得分:1)

尽管我喜欢C#,但在直接使用对象实例时,这是一件对C ++有点讨厌的事情;一些声明只是不能为空,所以不需要检查null。

你可以在C#中找到这个馅饼的最佳方式(这可能有点过于重新设计 - 在这种情况下,请选择其他答案)是struct的。虽然您可能发现自己处于结构体具有未实例化的“默认”值(即0,0.0,空字符串)的情况下,但从不需要检查“if(myStruct == null)”。

当然,如果不了解它们的使用,我就不会切换到它们。它们倾向于用于值类型,而不是用于大块数据 - 无论何时将结构从一个变量分配到另一个变量,您往往实际上是复制数据,实际上是创建每个原始值的副本(您可以使用ref关键字避免这种情况 - 再次阅读,而不是仅仅使用它。尽管如此,它可能适合像StreetAddress这样的东西 - 我当然不会懒得在任何我不想做的事情上使用它。

答案 15 :(得分:1)

根据使用“city”变量的目的,更简洁的方法是将空检查分成不同的类。这样你也不会违反得墨忒耳法则。所以而不是:

if (person != null && person.contact != null && person.contact.address != null && person.contact.address.city != null)
{ 
    // do some stuff here..
}

你有:

class test
{
    private test()
    {
        var person = new Person();
        if (person != null)
        {
            person.doSomething();
        }
    }
}

...

/* Person class */
doSomething() 
{
    if (contact != null)
    {
        contact.doSomething();
    }
}

...

/* Contact class */
doSomething()
{
    if (address != null) 
    {
        address.doSomething();
    }
}

...

/* Address class */
doSomething()
{
    if (city != null)
    {
        // do something with city
    }
}

同样,这取决于该计划的目的。

答案 16 :(得分:1)

在什么情况下这些东西可以为空?如果nulls表示代码中存在错误,那么您可以使用代码契约。如果在测试过程中出现空值,他们会选择它,然后在生产版本中消失。像这样:

using System.Diagnostics.Contracts;

[ContractClass(typeof(IContactContract))]
interface IContact
{
    IAddress address { get; set; }
}

[ContractClassFor(typeof(IContact))]
internal abstract class IContactContract: IContact
{
    IAddress address
    {
        get
        {
            Contract.Ensures(Contract.Result<IAddress>() != null);
            return default(IAddress); // dummy return
        }
    }
}

[ContractClass(typeof(IAddressContract))]
interface IAddress
{
    string city { get; set; }
}

[ContractClassFor(typeof(IAddress))]
internal abstract class IAddressContract: IAddress
{
    string city
    {
        get
        {
            Contract.Ensures(Contract.Result<string>() != null);
            return default(string); // dummy return
        }
    }
}

class Person
{
    [ContractInvariantMethod]
    protected void ObjectInvariant()
    {
        Contract.Invariant(contact != null);
    }
    public IContact contact { get; set; }
}

class test
{
    private test()
    {
        var person = new Person();
        Contract.Assert(person != null);
        if (person.contact.address.city != null)
        {
            // If you get here, person cannot be null, person.contact cannot be null
            // person.contact.address cannot be null and person.contact.address.city     cannot be null. 
        }
    }
}

当然,如果可能的空值来自其他地方,那么您需要已经调整了数据。如果任何空值有效,那么你不应该将非null作为合同的一部分,你需要测试它们并适当地处理它们。

答案 17 :(得分:0)

删除方法中的空检查的一种方法是将其功能封装在其他位置。一种方法是通过getter和setter。例如,而不是这样做:

class Person : IPerson
{
    public IContact contact { get; set; }
}

这样做:

class Person : IPerson
{
    public IContact contact 
    { 
        get
        {
            // This initializes the property if it is null. 
            // That way, anytime you access the property "contact" in your code, 
            // it will check to see if it is null and initialize if needed.
            if(_contact == null)
            {
                _contact = new Contact();
            }
            return _contact;
        } 
        set
        {
            _contact = value;
        } 
    }
    private IContact _contact;
}

然后,无论何时调用“person.contact”,“get”方法中的代码都会运行,因此如果值为null,则初始化该值。

您可以将这种完全相同的方法应用于所有类型中可能为null的所有属性。这种方法的好处在于它1)防止你必须在线进行空检查,它2)使你的代码更具可读性,并且不易出现复制粘贴错误。

但是,应该注意的是,如果您发现自己处于需要执行某些操作的情况,如果其中一个属性 为null(即,具有null联系人的人实际上是指您域中的某些内容?),那么这种方法将成为一种障碍,而不是一种帮助。但是,如果有问题的属性从不为null,那么这种方法将为您提供一种非常干净的方式来表示这一事实。

- jtlovetteiii

答案 18 :(得分:0)

您可以使用反射,以避免在每个类中强制实现接口和额外代码。只是一个带静态方法的Helper类。这可能不是最有效的方式,对我温柔,我是处女(读,菜)...

public class Helper
{
    public static bool IsNull(object o, params string[] prop)
    {
        if (o == null)
            return true;

        var v = o;
        foreach (string s in prop)
        {
            PropertyInfo pi = v.GetType().GetProperty(s); //Set flags if not only public props
            v = (pi != null)? pi.GetValue(v, null) : null;
            if (v == null)
                return true;                                
        }

        return false;
    }
}

    //In use
    isNull = Helper.IsNull(p, "ContactPerson", "TheCity");

如果你的名字中有拼写错误,结果将是错误的(最有可能)..