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

now*_*ed. 180 c# null .net-4.0 .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?
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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..
}
Run Code Online (Sandbox Code Playgroud)

这样做有更干净的方法吗?

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

Tot*_*oto 236

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

if (!person.IsNull(p => p.contact.address.city))
{
    //Nothing is null
}
Run Code Online (Sandbox Code Playgroud)

完整代码:

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这真的被认为更干净吗? (60认同)
  • 所以基本上,我得到的氛围是,"不,没有更清洁的方式!" (22认同)
  • 这需要将其纳入框架......甚至可能作为运营商. (13认同)
  • @ClassicThunder为消费者清洁,肯定. (7认同)
  • 你不能只说"不,先生,没有"?:) (2认同)

Kev*_*ica 62

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

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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

考虑到现状,这些变化很可能发生.如果在整个代码库中编写这样的代码,那么进行这些更改可能会变得非常昂贵.此外,它意味着你的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());
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您进行上述任一更改,则只需重构一段代码即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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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


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

特别是,我认为如果:

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

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

我把这种潜在的例外迪米特法则,因为有相似的名字Person,ContactAddress,你的类看起来像他们可能是数据层波苏斯.如果是这种情况,并且您非常有信心将来永远不需要重构它们,那么您可以在特定情况下忽略Demeter法则.

  • 真的需要更多的支持.与接受的答案相比,这是更好的建议imho. (6认同)
  • 这不回答这个问题.它告诉你那个人可能没有问正确的问题.但这是一个很好的例子.+1 (4认同)
  • 根据Bob叔叔的说法,Demeter法不适用于数据结构,如原始问题中描述的那样.请参阅此处有关主题的讨论:http://stackoverflow.com/questions/26021140/law-of-demeter-with-data-model-objects (3认同)

Kor*_*ryu 48

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

public bool HasCity
{
   get 
   { 
     return (this.Contact!=null && this.Contact.Address!= null && this.Contact.Address.City != null); 
   }     
}
Run Code Online (Sandbox Code Playgroud)

但你还是要检查一下这个人是否为空

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

}
Run Code Online (Sandbox Code Playgroud)

对于你的其他问题,对于字符串,你也可以检查是否为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
}
Run Code Online (Sandbox Code Playgroud)

  • 那只是为了字符串,你必须为你自己的类class.IsNull创建一个静态方法. (2认同)

big*_*gge 37

一个完全不同的选项(我认为未充分利用)是空对象模式.在你的特定情况下很难判断它是否有意义,但它可能值得一试.简而言之,您将拥有一个NullContact实现,一个NullAddress实现等等,而不是使用null.这样,您可以摆脱大多数空检查,当然牺牲一些人认为您必须将这些实现的设计付诸实践.

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

if (person.Contact.Address.City is NullCity)
Run Code Online (Sandbox Code Playgroud)

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

或者,null对象可以实现为单例(例如,在这里查看有关空对象模式的使用的一些实用指令,这里有关于C#中单例的指令),它允许您使用经典比较.

if (person.Contact.Address.City == NullCity.Instance)
Run Code Online (Sandbox Code Playgroud)

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

  • `person.Contact`没有设置为`null`,而是`NullContact` - 特殊的"null"值,其地址设置为`NullAddress`,它具有`NullCity`. (4认同)
  • 在您的示例中,如果`person.Contact`属性为`null`,此答案如何防止抛出NullReferenceException? (3认同)
  • 如果要添加一些代码,结果检查可能如下所示:`if(person.Contact.Address.City是NullCity)`.我过去曾使用过这种模式,虽然我在[特殊情况模式](http://martinfowler.com/eaaCatalog/specialCase.html)中遇到过它. (2认同)

Ada*_*rth 26

更新28/04/2014: 计划为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

所谓的:

    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();
    }
Run Code Online (Sandbox Code Playgroud)

  • 我设法获得这个徽章的事实让我觉得有点不干净. (13认同)
  • @newStackExchangeInstance这是完成任务的唯一方法! (11认同)
  • 为了记录,我正在推断您对分页符上方段落的回答. (2认同)

Mar*_*zek 14

第二个问题,

我真的不喜欢执行null检查(某些事情== null).相反,有没有另一种很好的方法来做一些像something.IsNull()方法?

可以使用扩展方法解决:

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

  • 不太相关的原来的问题,但要注意使用扩展方法是这样的动态对象 - 在一个简单的'== null`成功,他们将会失败. (5认同)

San*_*zen 10

如果出于某种原因你不介意选择其中一个"超过顶级"的解决方案,你可能想看看我博客文章中描述的解决方案.在评估表达式之前,它使用表达式树来确定值是否为null.但为了保持性能可接受,它会创建并缓存IL代码.

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

string city = person.NullSafeGet(n => n.Contact.Address.City);
Run Code Online (Sandbox Code Playgroud)


小智 7

你可以写:

public static class Extensions
    {
        public static bool IsNull(this object obj)
        {
            return obj == null;
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后:

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

}
Run Code Online (Sandbox Code Playgroud)

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

  • @VladimirGondarev:为什么不应该使用它? (2认同)
  • @nowhewhomustnotbenamed.不确定是否有真正的原因,但我不会触及任何扩展`object`的东西,除非它真的,*真的*有用. (2认同)

Ash*_*ani 5

在另外一个单独的做method:

private test()
{
    var person = new Person();
    if (!IsNull(person))
    {
        // Proceed
              ........
Run Code Online (Sandbox Code Playgroud)

你的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;
}
Run Code Online (Sandbox Code Playgroud)

  • 谢谢你的方法.但是,我真的不想做一个if(Person!= null && Person.Contact!= null && Person.Contact.Address!= null && Person.Contact.Address.City!= null)return false; (2认同)