清洁的方式在C#中执行空检查?
假设我有这个接口,
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,则此检查将失败。
目前,我能想到的一个解决scheme是:
if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null) { // Do some stuff here.. }
有没有一个更干净的方式来做到这一点?
我真的不喜欢作为(something == null)
完成null
检查。 相反,有没有另外一个很好的方法来做类似something.IsNull()
方法?
以通用的方式,您可以使用expression式树并使用扩展方法进行检查:
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); } }
您的代码可能比需要检查空引用更大的问题。 就目前而言,你可能违反了德米特法 。
得墨忒耳定律是其中的一种启发式algorithm,如不要重复自己,帮助你编写易于维护的代码。 它告诉程序员不要从任何距离太远的地方访问任何东西。 例如,假设我有这样的代码:
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
方法违反了德米特法。 在一个function中,它访问BusinessCalcualtor
, BusinessData
和一个decimal
。 这意味着如果进行了以下任何更改,则该行将不得不被重构:
-
BusinessCalculator.CalculateMoney()
的返回types更改。 -
BusinessData.Money
的types更改
考虑到现在的情况,这些变化很可能发生。 如果这样的代码写在整个代码库中,做这些更改可能会变得非常昂贵。 除此之外,这意味着您的BusinessController
与BusinessCalculator
和BusinessData
types都是耦合的。
避免这种情况的一种方法是像这样重写代码:
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); } }
如果进行了以下任何更改,则需要重构我写的主要方法或写入的空检查:
-
IPerson.contact
的types发生变化 -
IContact.address
的types改变 -
IAddress.city
的types更改
我认为你应该考虑对代码进行更深层的重构,而不是简单地重写一个空检查。
这就是说,我认为有些时候遵循得墨忒耳定律是不合适的。 (毕竟,这是一个启发式的,不是一个硬性的规则,即使它被称为“法律”。)
特别是,我认为如果:
- 您有一些类代表存储在程序的持久层中的logging,AND
- 你非常自信,你将来不需要重构这些类,
在处理这些类时,忽视德米特定律是可以接受的。 这是因为它们代表了您的应用程序所使用的数据,因此从一个数据对象到另一个数据对象是探索程序中信息的一种方式。 在我上面的例子中,由于违反了得墨忒耳定律而引起的耦合要严重得多:我通过堆栈中间的商业逻辑计算器,从堆栈顶部的控制器一直到达可能的数据类在持久层。
我把这个潜在的例外带到了Demeter法则上,因为像Person
, Contact
和Address
这样的名字,你们的类似乎可能是数据层的POCO。 如果是这样的话,而且你非常有把握,以后你永远都不需要重构它们,那么在你的具体情况下,你可能会忽视德米特定律。
在你的情况下,你可以创build一个人的财产
public bool HasCity { get { return (this.Contact!=null && this.Contact.Address!= null && this.Contact.Address.City != null); } }
但是你还是要检查一下是否为空
if (person != null && person.HasCity) { }
到你的其他问题,对于string,你也可以检查是否为空或以这种方式清空:
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
。 这样,你就可以摆脱大部分的空检查,当然这样做的代价是你必须把这些实现放在devise中。
正如亚当在他的评论中指出的,这允许你写
if (person.Contact.Address.City is NullCity)
在真正有必要的情况下。 当然,这只有在城市确实是一个不平凡的对象时才有意义。
或者,空对象可以实现为一个单例(例如, 在这里查看关于空对象模式的使用的一些实际指令, 这里是关于C#中单例的指令),这允许您使用经典比较。
if (person.Contact.Address.City == NullCity.Instance)
就我个人而言,我更喜欢这种方法,因为我认为对于不熟悉这种模式的人来说,读起来更容易一些。
更新28/04/2014: C#vNext计划为空传播
有比传播空检查更大的问题。 旨在为其他开发人员可以理解的可读代码,虽然它罗嗦 – 你的例子是好的。
如果是频繁执行的检查,请考虑将其封装在Person
类中作为属性或方法调用。
这就是说,免费的Func
和仿制药!
我永远不会这样做,但这是另一种select:
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(); }
第二个问题,
我真的不喜欢作为(东西==空)完成空检查。 相反,有没有另外一个很好的方法来做类似something.IsNull()的方法?
可以使用扩展方法解决:
public static class Extensions { public static bool IsNull<T>(this T source) where T : class { return source == null; } }
如果由于某种原因,您不介意使用“顶级”解决scheme之一,您可能需要查看我的博客文章中描述的解决scheme。 在评估expression式之前,它使用expression式树来确定值是否为空。 但为了保持性能可接受,它创build并cachingIL代码。
该解决scheme允许你写这个:
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 。 这是一个了不起的,非常现代的面向.NET(也是Java和Cocoa)的面向对象语言(OO),它本质上是一个非常了不起的工具链。
氧气有一个冒号操作符,正是你所要求的。 引用他们的杂项语言特征页面 :
冒号(“:”)运算符
在Oxygene中,就像它受到影响的许多语言一样,“。” 运算符用于调用类或对象上的成员,如
var x := y.SomeProperty;
这个“取消引用”“y”中包含的对象,调用(在这种情况下)属性getter并返回它的值。 如果“y”恰好是未赋值(即“nil”),则抛出exception。
“:”运算符的工作方式大致相同,但不是在未分配的对象上抛出exception,结果将为零。 对于来自Objective-C的开发人员来说,这将是熟悉的,因为Objective-C方法使用[]语法也是如此。
…(剪辑)
其中“:”实际上是在访问链中的属性时发生的,任何元素可能都是零。 例如,下面的代码:
var y := MyForm:OkButton:Caption:Length;
将运行没有错误,并返回零如果任何链中的对象是零 – forms,button或其标题。
try { // do some stuff here } catch (NullReferenceException e) { }
不要这样做。 做空检查,找出你最适合的格式。
我有一个可能对此有用的扩展; ValueOrDefault()。 它接受一个lambda语句并对其进行求值,如果有任何预期的exception(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)); }
没有采用特定默认值的重载将为任何引用types返回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,如果不是,返回它所请求的属性之一,或者执行一些方法。 这当然只适用于引用types,这就是为什么我有相应的通用约束。
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); }
与手动解决scheme(无reflection,无expression式树)相比,这些方法几乎不会增加开销,并且可以使用它们(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方法 。
但这里也是我对空值的想法
-
一般来说,如果有人试图指出没有数据,那么返回空值是最好的办法。
-
如果对象不是空的,而是空的,则意味着数据已经被返回,而返回null清楚地表明没有返回任何东西。
-
另外,IMO,如果你将返回null,如果你试图访问对象中的成员,这将导致一个空的exception,这可能有助于突出显示错误的代码。
在C#中,有两种不同的等式:
- 参考平等和
- 价值平等。
当一个types是不可变的,重载operator ==来比较值相等而不是引用相等可能是有用的。
不推荐在非不可变types中覆盖运算符==。
有关更多详细信息,请参阅MSDN文章“重载Equals()和运算符==(C#编程指南)指南” 。
就像我喜欢C#一样,直接使用对象实例时,这是C ++的一种可爱的东西; 有些声明不能为空,所以不需要检查null。
最好的方法是在C#中获得这个饼图的一部分(这可能有点太重新devise了 – 在这种情况下,select其他答案)是用struct
的。 虽然你可能发现自己处于一个结构没有“默认”值(即0,0.0,空string)的情况下,所以从不需要检查“if(myStruct == null)”。
当然,如果不了解它们的用途,我就不会转向它们。 它们倾向于用于值types,而不适用于大块数据 – 只要您将结构从一个variables分配给另一个variables,您就倾向于实际上复制数据,实质上创build每个原始值的副本(你可以用ref
关键字来避免这个问题 – 再一次读取它,而不是仅仅使用它)。 不过,它可能适合像StreetAddress这样的东西 – 我当然不会懒惰地使用它,我不想null检查。
根据使用“城市”variables的目的,更清晰的方法可能是将空检查分为不同的类。 That way you also wouldn't be violating the Law of Demeter. 所以,而不是:
if (person != null && person.contact != null && person.contact.address != null && person.contact.address.city != null) { // do some stuff here.. }
You'd have:
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 } }
Again, it depends on the purpose of the program.
In what circumstances can those things be null? If nulls would indicate a bug in the code then you could use code contracts. They will pick it up if you get nulls during testing, then will go away in the production version. 像这样的东西:
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. } } }
Of course, if the possible nulls are coming from somewhere else then you'll need to have already conditioned the data. And if any of the nulls are valid then you shouldn't make non-null a part of the contract, you need to test for them and handle them appropriately.
One way to remove null checks in methods is to encapsulate their functionality elsewhere. One way to do this is through getters and setters. For instance, instead of doing this:
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; }
Then, whenever you call "person.contact", the code in the "get" method will run, thus initializing the value if it is null.
You could apply this exact same methodology to all of the properties that could be null across all of your types. The benefits to this approach are that it 1) prevents you from having to do null checks in-line and it 2) makes your code more readable and less prone to copy-paste errors.
It should be noted, however, that if you find yourself in a situation where you need to perform some action if one of the properties is null (ie does a Person with a null Contact actually mean something in your domain?), then this approach will be a hindrance rather than a help. However, if the properties in question should never be null, then this approach will give you a very clean way of representing that fact.
–jtlovetteiii
You could use reflection, to avoid forcing implementation of interfaces and extra code in every class. Simply a Helper class with static method(s). This might not be the most efficient way, be gentle with me, I'm a virgin (read, noob)..
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");
Offcourse if you have a typo in the propnames, the result will be wrong (most likely)..