182

假设,我有这个界面,

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 本身为空,则此检查失败。

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

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

{ 
    // Do some stuff here..
}

有更清洁的方法吗?

我真的不喜欢做null检查(something == null)。相反,是否有另一种好方法来做类似该something.IsNull()方法的事情?

4

19 回答 19

235

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

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);
    }
}
于 2013-07-16T09:55:16.977 回答
64

您的代码可能存在比需要检查空引用更大的问题。就目前而言,您可能违反了得墨忒耳法则

得墨忒耳法则是启发式方法之一,例如不要重复自己,它可以帮助您编写易于维护的代码。它告诉程序员不要访问任何离直接作用域太远的东西。例如,假设我有以下代码:

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方法违反了得墨忒耳定律。在一个函数中,它访问 a BusinessCalcualtor、 aBusinessData和 a decimal。这意味着如果进行以下任何更改,则必须重构该行:

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

考虑到had的情况,这些变化很有可能发生。如果在整个代码库中编写这样的代码,进行这些更改可能会变得非常昂贵。除此之外,这意味着您与 the和类型BusinessController都耦合。BusinessCalculatorBusinessData

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

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()方法。您还消除BusinessController了对BusinessData.


您的代码遇到类似的问题:

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

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

  • IPerson.contact变化的类型
  • IContact.address变化的类型
  • IAddress.city变化的类型

我认为您应该考虑对代码进行更深入的重构,而不是简单地重写空检查。


也就是说,我认为有时遵循得墨忒耳法则是不合适的。(毕竟,这是一个启发式的规则,而不是一成不变的规则,尽管它被称为“法律”。)

特别是,我认为如果:

  1. 您有一些表示存储在程序持久层中的记录的类,并且
  2. 您非常有信心将来不需要重构这些类,

在专门处理这些类时,忽略得墨忒耳法则是可以接受的。这是因为它们代表了您的应用程序使用的数据,因此从一个数据对象到另一个数据对象是探索程序中信息的一种方式。在我上面的例子中,违反得墨忒耳定律造成的耦合要严重得多:我从靠近堆栈顶部的控制器通过堆栈中间的业务逻辑计算器一直到达可能的数据类在持久层。

我将这个潜在的例外带到了得墨忒耳法则中,因为使用、 和之类的名称Person,您的类看起来可能是数据层 POCO。如果是这种情况,并且您非常有信心将来永远不需要重构它们,那么在您的特定情况下,您可能可以忽略 Demeter 法则。ContactAddress

于 2013-07-16T17:17:51.503 回答
48

在您的情况下,您可以为 person 创建一个属性

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

但你仍然需要检查 person 是否为空

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
}
于 2013-07-16T09:25:01.893 回答
37

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

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

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

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

或者,可以将空对象实现为单例(例如,在此处查找有关使用空对象模式的一些实用说明,并在此处查找有关 C# 中的单例的说明),这允许您使用经典比较。

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

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

于 2013-07-16T09:34:16.507 回答
26

28/04/2014 更新: 计划为 C# vNext 进行 Null 传播


有比传播空检查更大的问题。瞄准其他开发人员可以理解的可读代码,虽然它很罗嗦 - 你的例子很好。

如果是经常进行的检查,请考虑将其封装在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();
    }
于 2013-07-16T09:26:27.477 回答
15

第二个问题,

我真的不喜欢将 null 检查作为 (something == null)。相反,是否有另一种好方法来做类似 something.IsNull() 方法的事情?

可以使用扩展方法解决:

public static class Extensions
{
    public static bool IsNull<T>(this T source) where T : class
    {
        return source == null;
    }
}
于 2013-07-16T09:20:42.843 回答
10

如果出于某种原因您不介意使用更“顶级”的解决方案之一,您可能需要查看我的博客文章中描述的解决方案。它在计算表达式之前使用表达式树来确定值是否为空。但为了保持性能可接受,它会创建并缓存 IL 代码。

该解决方案允许您这样做:

string city = person.NullSafeGet(n => n.Contact.Address.City);
于 2013-07-16T11:05:51.290 回答
8

你可以写:

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

接着:

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

}

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

于 2013-07-16T09:21:54.347 回答
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;
}
于 2013-07-16T09:24:04.170 回答
4

您需要 C#,还是只需要.NET?如果您可以混合使用另一种 .NET 语言,请查看Oxygene。这是一种了不起的、非常现代的面向 .NET 的面向对象语言(还有 Java 和Cocoa。是的。从本质上讲,它确实是一个非常了不起的工具链。)

Oxygene 有一个冒号操作员,可以完全按照您的要求进行操作。引用他们的杂项语言功能页面

冒号 (":") 运算符

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

var x := y.SomeProperty;

这“取消引用”包含在“y”中的对象,调用(在这种情况下)属性 getter 并返回其值。如果“y”碰巧未赋值(即“nil”),则抛出异常。

":" 运算符的工作方式大致相同,但不是在未分配的对象上抛出异常,结果将简单地为零。 对于来自 Objective-C 的开发人员来说,这将是熟悉的,因为这也是使用 [] 语法调用 Objective-C 方法的工作原理。

...(截图)

“:”真正闪耀的地方是在访问链中的属性时,其中任何元素都可能为零。例如,下面的代码:

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

将毫无错误地运行,如果链中的任何对象为 nil(表单、按钮或其标题),则返回 nil。

于 2013-07-16T11:53:38.053 回答
3
try
{
  // do some stuff here
}
catch (NullReferenceException e)
{
}

实际上不要这样做。进行空值检查,并找出最适合您使用的格式。

于 2013-07-16T09:59:51.807 回答
3

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

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

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

但是,可以肯定地争论代码的长度并没有太大变化。

于 2013-07-19T22:07:31.607 回答
3

我有一个可能对此有用的扩展;值或默认值()。它接受一个 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
        }
    }
}
于 2013-07-16T14:28:57.833 回答
2

In my opinion, the equality operator is not a safer and better way for reference equality.

It's always better to use ReferenceEquals(obj, null). This will always work. On the other hand, the equality operator (==) could be overloaded and might be checking if the values are equal instead of the references, so I will say ReferenceEquals() is a safer and better way.

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

Reference: MSDN article Object.ReferenceEquals Method.

But also here are my thoughts for null values

  • Generally, returning null values is the best idea if anyone is trying to indicate that there is no data.

  • If the object is not null, but empty, it implies that data has been returned, whereas returning null clearly indicates that nothing has been returned.

  • Also IMO, if you will return null, it will result in a null exception if you attempt to access members in the object, which can be useful for highlighting buggy code.

In C#, there are two different kinds of equality:

  • reference equality and
  • value equality.

When a type is immutable, overloading operator == to compare value equality instead of reference equality can be useful.

Overriding operator == in non-immutable types is not recommended.

Refer to the MSDN article Guidelines for Overloading Equals() and Operator == (C# Programming Guide) for more details.

于 2013-07-16T09:29:08.470 回答
1

在什么情况下这些东西可以为空?如果空值表示代码中存在错误,那么您可以使用代码协定。如果您在测试期间获得空值,他们会选择它,然后在生产版本中消失。像这样的东西:

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. 
        }
    }
}

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

于 2013-07-16T20:46:25.900 回答
1

尽管我非常喜欢 C#,但在直接使用对象实例时,这是 C++ 的一种让人喜欢的地方。有些声明根本不能为空,因此无需检查是否为空。

您可以在 C# 中分得一杯羹的最佳方式(这对您而言可能有点过多的重新设计 - 在这种情况下,请选择其他答案)是使用struct's. 虽然您可能会发现自己处于结构具有未实例化的“默认”值(即,0、0.0、空字符串)的情况下,但永远不需要检查“if (myStruct == null)”。

当然,我不会在不了解它们的用途的情况下切换到它们。它们倾向于用于值类型,而不是真正用于大数据块 - 每当您将结构从一个变量分配给另一个变量时,您往往实际上是在复制数据,本质上是创建每个原始值的副本(您可以使用关键字来避免这种情况ref- 再次阅读它,而不仅仅是使用它)。不过,它可能适合 StreetAddress 之类的东西——我当然不会懒惰地在我不想进行空检查的任何东西上使用它。

于 2013-07-16T13:39:17.700 回答
1

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

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
    }
}

同样,这取决于程序的目的。

于 2013-07-16T20:43:12.780 回答
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,则初始化该值。

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

但是,应该注意的是,如果您发现自己需要在其中一个属性null 的情况下执行某些操作(即,具有 null Contact 的 Person 是否实际上意味着您的域中的某些内容?),那么这种方法将是障碍而不是帮助。但是,如果所讨论的属性永远不应该为空,那么这种方法将为您提供一种非常简洁的方式来表示该事实。

--jtlovetteiii

于 2013-07-18T12:32:09.067 回答
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");

如果您在道具名称中有拼写错误,结果将是错误的(很可能)..

于 2013-07-20T10:12:05.733 回答