假设我有这个界面,
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本身为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()
方法吗?
答案 0 :(得分:236)
以通用方式,您可以使用表达式树并使用扩展方法进行检查:
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);
}
}
答案 1 :(得分:62)
您的代码可能比需要检查空引用有更大的问题。就目前而言,您可能违反了Law of Demeter。
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);
}
}
DoAnAction
方法违反了得墨忒耳法则。在一个函数中,它访问BusinessCalcualtor
,BusinessData
和decimal
。这意味着如果进行以下任何更改,则必须重构该行:
BusinessCalculator.CalculateMoney()
的返回类型更改。BusinessData.Money
的类型更改考虑到现状,这些变化很可能发生。如果在整个代码库中编写这样的代码,那么进行这些更改可能会变得非常昂贵。除此之外,这意味着您的BusinessController
与BusinessCalculator
和BusinessData
类型相关联。
避免这种情况的一种方法是重写代码:
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
的类型更改IContact.address
的类型更改IAddress.city
的类型更改我认为你应该考虑对代码进行更深入的重构,而不仅仅是重写一次空检查。
那就是说,我认为有时候遵循得墨忒耳法是不合适的。 (毕竟,它是一种启发式的,而不是一种强硬的规则,即使它被称为“法律”。)
特别是,我认为如果:
我将此可能的例外情况引入Demeter法则,因为Person
,Contact
和Address
这样的名称,您的类看起来可能是数据层POCO。如果是这种情况,并且您非常有信心将来永远不需要重构它们,那么您可能会在特定情况下无视Demeter法则。
答案 2 :(得分:48)
在您的情况下,您可以为人创建属性
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
}
答案 3 :(得分:37)
完全不同的选项(我认为未充分利用)是null object pattern。在你的特定情况下很难判断它是否有意义,但它可能值得一试。简而言之,您将使用NullContact
实施,NullAddress
实施等等,而不是使用null
。这样,你可以摆脱大多数的空检查,当然这是以牺牲你必须考虑到这些实现的设计为代价的。
正如亚当在评论中指出的那样,这可以让你写出
if (person.Contact.Address.City is NullCity)
在真的有必要的情况下。当然,这只有在城市确实是一个非平凡的对象时才有意义......
或者,可以将null对象实现为单例(例如,查看here以获取有关空对象模式的使用的一些实用指令,并将here实现为有关C#中单例的指令,允许你要使用古典比较。
if (person.Contact.Address.City == NullCity.Instance)
就个人而言,我更喜欢这种方法,因为我觉得对不熟悉模式的人来说更容易阅读。
答案 4 :(得分:26)
更新28/04/2014: Null propagation is planned for 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();
}
答案 5 :(得分:14)
第二个问题,
我真的不喜欢将null检查作为(某些事情== null)。相反,还有另一种很好的方式来做像something.IsNull()方法吗?
可以使用扩展方法解决:
public static class Extensions
{
public static bool IsNull<T>(this T source) where T : class
{
return source == null;
}
}
答案 6 :(得分:10)
如果由于某种原因您不介意使用其中一个“超过顶级”的解决方案,您可能需要查看我的blog post中描述的解决方案。在评估表达式之前,它使用表达式树来确定值是否为null。但为了保持性能可接受,它会创建并缓存IL代码。
该解决方案允许您写下这个:
string city = person.NullSafeGet(n => n.Contact.Address.City);
答案 7 :(得分:7)
你可以写:
public static class Extensions
{
public static bool IsNull(this object obj)
{
return obj == null;
}
}
然后:
string s = null;
if(s.IsNull())
{
}
有时这是有道理的。但我个人会避免这样的事情...因为这不清楚为什么你可以调用一个实际为null的对象的方法。
答案 8 :(得分: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;
}
答案 9 :(得分:4)
您需要C#,还是只需要.NET?如果您可以混合使用另一种.NET语言,请查看Oxygene。它是一种令人惊叹的,非常现代的OO语言,它以.NET(以及Java和Cocoa为目标。是的。在本机上,它确实是一个非常棒的工具链。)
Oxygene有一个冒号运算符,它完全符合你的要求。引用他们的miscellaneous language features page:
冒号(“:”)运算符
在Oxygene中,就像许多语言一样 受到“。”的影响。运算符用于调用类的成员 或对象,例如
var x := y.SomeProperty;
这“取消引用”中包含的对象 “y”,调用(在本例中)属性getter并返回其值。 如果“y”恰好是未分配的(即“nil”),则抛出异常。
“:”运算符的工作方式大致相同,但不是抛出 未分配对象的异常,结果将只是零。 对于来自Objective-C的开发人员来说,这将是熟悉的 是Objective-C方法使用[]语法工作的方式。
...(剪辑)
其中“:”真正闪耀的是访问链中的属性,其中 任何元素都可能是nil。例如,以下代码:
var y := MyForm:OkButton:Caption:Length;
将无误地运行,并且 如果链中的任何对象都是零,则返回nil - 形式, 按钮或其标题。
答案 10 :(得分:3)
try
{
// do some stuff here
}
catch (NullReferenceException e)
{
}
实际上不要这样做。执行空检查,找出最适合的格式。
答案 11 :(得分:3)
我有一个可能对此有用的扩展名; 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
}
}
}
答案 12 :(得分:3)
例如,如果您使用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());
然而,人们可以定义论代码长度没有太大变化。
答案 13 :(得分:2)
在我看来,等于运算符不是更安全和更好的方式用于引用相等。
使用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则表明没有返回任何内容。
同样是IMO,如果你将返回null,如果你试图访问对象中的成员,它将导致一个空异常,这对于突出显示错误的代码很有用。
在C#中,有两种不同的等式:
当一个类型是不可变的时,重载operator ==来比较值的相等而不是引用相等可能是有用的。
不建议在非不可变类型中覆盖operator ==。
有关详细信息,请参阅MSDN文章 Guidelines for Overloading Equals() and Operator == (C# Programming Guide) 。
答案 14 :(得分:1)
尽管我喜欢C#,但在直接使用对象实例时,这是一件对C ++有点讨厌的事情;一些声明只是不能为空,所以不需要检查null。
你可以在C#中找到这个馅饼的最佳方式(这可能有点过于重新设计 - 在这种情况下,请选择其他答案)是struct
的。虽然您可能发现自己处于结构体具有未实例化的“默认”值(即0,0.0,空字符串)的情况下,但从不需要检查“if(myStruct == null)”。
当然,如果不了解它们的使用,我就不会切换到它们。它们倾向于用于值类型,而不是用于大块数据 - 无论何时将结构从一个变量分配到另一个变量,您往往实际上是复制数据,实际上是创建每个原始值的副本(您可以使用ref
关键字避免这种情况 - 再次阅读,而不是仅仅使用它。尽管如此,它可能适合像StreetAddress这样的东西 - 我当然不会懒得在任何我不想做的事情上使用它。
答案 15 :(得分:1)
根据使用“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
}
}
同样,这取决于该计划的目的。
答案 16 :(得分:1)
在什么情况下这些东西可以为空?如果nulls表示代码中存在错误,那么您可以使用代码契约。如果在测试过程中出现空值,他们会选择它,然后在生产版本中消失。像这样:
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作为合同的一部分,你需要测试它们并适当地处理它们。
答案 17 :(得分: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,则初始化该值。
您可以将这种完全相同的方法应用于所有类型中可能为null的所有属性。这种方法的好处在于它1)防止你必须在线进行空检查,它2)使你的代码更具可读性,并且不易出现复制粘贴错误。
但是,应该注意的是,如果您发现自己处于需要执行某些操作的情况,如果其中一个属性 为null(即,具有null联系人的人实际上是指您域中的某些内容?),那么这种方法将成为一种障碍,而不是一种帮助。但是,如果有问题的属性从不为null,那么这种方法将为您提供一种非常干净的方式来表示这一事实。
- jtlovetteiii
答案 18 :(得分: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");
如果你的名字中有拼写错误,结果将是错误的(最有可能)..