在 C # 中执行 null 检查的更干净的方法?

假设我有这个接口,

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 是否为 null)

但是,如果 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()方法的事情?

90561 次浏览

第二个问题,

我真的不喜欢把 null 检查作为(something = = null)来执行。相反,是否有其他更好的方法来执行诸如 something. IsNull ()方法之类的操作?

可以用扩展方法解决:

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

你可以写:

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

然后:

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


}

有时候这就说得通了。但就我个人而言,我会避免这样的事情... ... 因为这不清楚为什么你可以调用一个实际上为空的对象的方法。

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

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

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

但你还是要检查 person 是否为 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
}

更新28/04/2014: Null 传播计划用于 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();
}

在我看来,相等运算符并不是一种更安全、更好的方法为参考等式。

使用 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 文章 Rel = “ nofollow”> Object. ReferenceEquals Method < a href = “ http://msdn.microsoft.com/en-us/library/system.Object.ReferenceEquals.aspx”rel = “ nofollow”> Object. ReferenceEquals Method

但这里也是我对空值的想法

  • 一般来说,如果有人试图表示没有数据,返回 null 值是最好的办法。

  • 如果对象不为 null,而为空,则表示已返回数据,而返回 null 则清楚地表明未返回任何内容。

  • 另外,如果您将返回 null,那么如果您试图访问对象中的成员,它将导致 null 异常,这对于突出显示错误代码非常有用。

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

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

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

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

有关详细信息,请参阅 MSDN 文章 返回文章页面超载等式指南(http://msdn.microsoft.com/en-us/library/ms173147% 28v = vs. 80% 29.aspx)译者:

一个完全不同的选择(我认为是未得到充分利用)是 空对象模式。很难说这在你的特殊情况下是否有意义,但它可能值得一试。简而言之,您将拥有一个 NullContact实现、一个 NullAddress实现等等,您将使用它来代替 null。这样,您就可以消除大部分的空检查,当然,代价是您必须在这些实现的设计中投入一些思想。

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

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

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

另外,null 对象可以作为单例实现(例如,查看关于空对象模式使用的一些实际指令的 给你和关于 c # 中单例的指令的 给你) ,这允许你使用经典的比较。

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

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

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

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);
}
}
try
{
// do some stuff here
}
catch (NullReferenceException e)
{
}

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

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

解决方案允许你这样写:

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

你需要 C # ,还是只需要 .NET?如果你能再调一杯的话。NET 语言,看看 氧气。这是一种令人惊奇的、非常现代的面向对象语言。NET (以及 Java 和 可可。是的。从本质上讲,它确实是一个相当神奇的工具链。)

氧气有一个结肠操作员,它完全按照你的要求做。引用他们的 杂项语言功能页:

冒号(“ :”)操作符

在 Oxygene,就像许多语言一样 操作符用于调用类的成员 或物体,例如

var x := y.SomeProperty;

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

“ :”操作符的工作方式大致相同,但不是抛出 如果对未赋值的对象发生异常,则结果将简单地为 nil。 对于来自 Objective-C 的开发人员来说,这是很熟悉的 也是 Objective-C 方法使用[]语法调用的方式。

... (剪)

其中“ :”真正发光的是当访问属性链时,在哪里 任何元素都可以是 nil

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

将运行没有错误,并且 如果链中的任何对象为 nil,则返回 nil ーー表单、 按钮或其标题。

尽管我非常喜欢 C # ,但是在直接处理对象实例时,这是 C + + 非常讨人喜欢的一点; 一些简单的 不能声明是 null,所以不需要检查 null。

在 C # 中分得一杯羹的最好方法是使用 struct(这可能有点过于重新设计了——在这种情况下,从其他答案中选择一个)。虽然您可能会发现自己处于一个结构具有未实例化的“默认”值(即0.0.0,null 字符串)的情况下,但是从来不需要检查“ if (myStruct = = null)”。

当然,我不会在不了解它们的用途的情况下转而使用它们。它们往往用于值类型,而不是真正用于大块的数据——任何时候你把一个结构从一个变量分配给另一个变量,你往往实际上是在复制数据,本质上是在创建每个原始值的副本(你可以用 ref关键字避免这种情况——再次强调,仔细阅读它,而不是仅仅使用它)。尽管如此,它可能适用于像 StreetAddress 这样的东西——我当然不会懒惰地在任何我不想使用 null 检查的东西上使用它。

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

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

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

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的类型发生变化

考虑到过去的情况,这些变化很可能发生。如果像这样的代码是在整个代码库中编写的,那么进行这些更改可能会变得非常昂贵。此外,这意味着您的 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()方法。您还消除了 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. 您非常自信将来不需要重构这些类,

忽视得墨忒耳定律是可以接受的,尤其是在处理这些类别的时候。这是因为它们表示应用程序使用的数据,所以从一个数据对象到另一个数据对象是探索程序中信息的一种方式。在我上面的例子中,违反得墨忒尔定律导致的耦合要严重得多: 我从堆栈顶部附近的控制器,通过堆栈中间的业务逻辑计算器,一直到达可能位于持久层的数据类。

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

根据使用“ city”变量的目的,一种更简单的方法是将 null 检查分离到不同的类中。这样你也不会违反得墨忒尔定律。所以不是:

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 表示代码中存在 bug,那么您可以使用代码契约。如果您在测试期间得到 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 来自其他地方,那么您需要已经对数据进行了条件化处理。如果有任何 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,就会初始化该值。

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

但是,应该注意的是,如果您发现自己处于这样一种情况,即如果属性 null 之一(即,一个 Person with a null Contact 实际上在您的域中有什么含义吗?) ,那么需要执行某些操作,那么这种方法将是一个障碍,而不是帮助。但是,如果有问题的属性 永远不会应该为 null,那么这种方法将为您提供一种非常简洁的方式来表示这个事实。

—— jtlovetteiii

这样的引用链可能会发生,例如,如果您使用 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());

然而,可以肯定地说,代码的长度并没有发生太大的变化。

您可以使用反射,以避免在每个类中强制实现接口和额外的代码。只是一个具有静态方法的 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");

当然,如果你在程序名中有一个打字错误,结果将是错误的(最有可能的)。