在 C# 中执行空检查的更简洁的方法

本文关键字:简洁 方法 检查 执行 | 更新日期: 2023-09-27 18:22:25

假设,我有这个接口,

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(这用于检查城市是否为空。

但是,如果地址或联系人或人员本身为空,则此检查将失败。

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

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
    // Do some stuff here..
}

有没有更清洁的方法?

我真的不喜欢null检查按(something == null)进行。相反,有没有另一种好方法可以做类似something.IsNull()方法的事情?

在 C# 中执行空检查的更简洁的方法

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

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

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

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

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

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

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

  • IPerson.contact类型发生变化
  • IContact.address类型更改
  • IAddress.city的类型发生变化

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


也就是说,我认为有时遵循得墨忒耳定律是不合适的。(毕竟,这是一个启发式的,而不是硬性规定,尽管它被称为"法律"。

特别是,我认为如果:

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

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

我把这个潜在的例外带到得墨忒耳定律中,因为像PersonContactAddress这样的名称,你的类看起来像它们可能是数据层POCO。如果是这样的话,并且您非常有信心将来永远不需要重构它们,那么在您的特定情况下,您也许可以忽略得墨忒耳定律。

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

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
}

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

正如亚当在他的评论中指出的那样,这允许您写

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

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

或者,null 对象

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

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

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

> 更新 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;
    }
}

叫:

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

第二个问题,

我真的不喜欢空检查作为(某些东西 == 空(。相反,有没有另一种好方法可以做类似的事情。IsNull(( 方法?

可以使用扩展方法解决:

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

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

该解决方案允许您编写以下内容:

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

你可以这样写:

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

然后:

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

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

在单独的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;
}

你需要 C#,还是只需要 .NET?如果可以混合使用另一种.NET语言,请查看Oxygene。这是一种了不起的,非常现代的OO语言,面向.NET(以及Java和Cocoa(。是的。总而言之,它确实是一个非常了不起的工具链。

Oxygene有一个结肠运算符,可以完全按照您的要求进行操作。引用他们的杂项语言功能页面:

冒号 (":"( 运算符

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

var x := y.SomeProperty;

这将"取消引用"包含在 "y",调用(在本例中(属性 getter 并返回其值。 如果"y"碰巧未赋值(即"nil"(,则会抛出异常。

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

。(截图(

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

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

将运行而不会出错,并且 如果链中的任何对象为 nil,则返回 nil — 形式, 按钮或其标题。

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

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

我有一个可能对此有用的扩展;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
        }
    }
}

例如,如果您使用 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());

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

在我看来,相等运算符并不是引用相等更安全、更好的方法

使用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,而是空,则表示已返回数据,而返回 null 则明确表示未返回任何内容。

  • 同样是 IMO,如果您将返回 null,则当您尝试访问对象中的成员时,将导致 null 异常,这对于突出显示错误代码很有用。

在 C# 中,有两种不同类型的相等:

  • 参考相等和
  • 价值平等。

当类型不可变时,重载运算符 == 来比较值相等性而不是引用相等性可能很有用。

不建议在非不可变类型中重写运算符 ==。

有关详细信息,请参阅 MSDN 文章重载 Equals(( 和运算符 ==(C# 编程指南(。

尽管我非常喜欢 C#,但在直接使用对象实例时,这是C++的一件讨人喜欢的事情; 有些声明根本不能为 null,因此无需检查 null。

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

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

根据使用"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
    }
}

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

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

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 有效,那么您不应该将 non-null 作为合约的一部分,您需要测试它们并适当地处理它们。

在方法中删除 null 检查的一种方法是将它们的功能封装在其他地方。一种方法是通过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,则此方法将为您提供一种非常干净的方式来表示该事实。

-

-jtlovetteiii

您可以使用反射来避免在每个类中强制实现接口和额外的代码。 只是一个具有静态方法的帮助程序类。这可能不是最有效的方法,对我温柔一点,我是处女(阅读,菜鸟(。

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

当然,如果你的道具名称中有错别字,结果将是错误的(很可能(。