Более чистый способ сделать нулевую проверку в 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.

в настоящее время одно решение, которое я мог придумать, было следующим:

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)

{ 
    // Do some stuff here..
}

есть ли более чистый способ сделать это?

мне действительно не нравится null проверка выполняется как (something == null). Вместо этого, есть еще один хороший способ сделать что-то вроде something.IsNull() способ?

19 ответов


В общем виде вы можете использовать дерево выражений и проверить с помощью метода расширения:

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, a BusinessData и decimal. Это означает, что если какой-либо из следующих изменений, линии должны быть подвергнуты рефакторингу:

  • тип возвращаемого BusinessCalculator.CalculateMoney() изменения.
  • типа BusinessData.Money изменения

учитывая ситуацию в had, эти изменения, скорее всего, произойдет. Если такой код написан по всей кодовой базе, внесение этих изменений может стать очень дорогим. Кроме того, это означает, что ваш 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() метод. Вы также исключили на 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 изменения

я думаю, вам следует рассмотреть более глубокий рефакторинг вашего кода, чем просто переписывание нулевой проверки.


тем не менее, я думаю, что есть времена, когда следование закону Деметры несоответствующий. (В конце концов, это эвристическое, а не жесткое правило, хотя оно и называется "законом".")

в частности, я думаю, что если:

  1. у вас есть некоторые классы, которые представляют записи, хранящиеся на уровне персистентности вашей программы, и
  2. вы уверены, что вам не потребуется рефакторинг этих классов в будущем,

игнорирование закона Деметры приемлемо, когда речь идет конкретно о этот класс. Это связано с тем, что они представляют данные, с которыми работает ваше приложение, поэтому переход от одного объекта данных к другому-это способ изучения информации в вашей программе. В моем примере выше соединение, вызванное нарушением закона Деметры, было намного более серьезным: я достигал всего пути от контроллера в верхней части моего стека через калькулятор бизнес-логики в середине стека в класс данных, вероятно, в слое персистентности.

я об этом потенциальное исключение из закона Деметры, потому что с такими именами, как Person, Contact и Address, ваши классы выглядят так, как будто они могут быть POCOs уровня данных. Если это так, и вы чрезвычайно уверены, что вам никогда не понадобится рефакторировать их в будущем, вы можете уйти, игнорируя закон Деметры в своей конкретной ситуации.


в вашем случае вы можете создать свойство для человека

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

но вы все равно должны проверить, является ли человек 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
}

совершенно другой вариант (который, я думаю, используется недостаточно) - это null шаблон объекта. Трудно сказать, имеет ли это смысл в вашей конкретной ситуации, но, возможно, стоит попробовать. Короче говоря, у вас будет NullContact реализация, a NullAddress реализация и так далее, что вы используете вместо null. Таким образом, вы можете избавиться от большинства нулевых проверок, конечно, за счет какой-то мысли, которую вы должны поместить в дизайн этих реализации.

как отметил Адам в своем комментарии, это позволяет вам писать

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

в случаях, когда это действительно необходимо. Конечно, это имеет смысл только в том случае, если город действительно является нетривиальным объектом...

кроме того, нулевой объект может быть реализован как синглтон (напр.,здесь для некоторых практических инструкций, касающихся использования шаблона нулевого объекта и здесь указаний о синглтонов в C#), который позволяет использовать классическое сравнение.

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

лично я предпочитаю этот подход, потому что я думаю, что легче читать для людей, не знакомых с шаблоном.


обновление 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();
    }

второй вопрос

Мне действительно не нравится, что проверка null выполняется как (something == null). Вместо этого, есть еще один хороший способ сделать что-то вроде того.Функция isnull() метод?

может быть решена с помощью метода расширения:

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

Если по какой-то причине вы не против пойти с одним из более "сверху" решений, вы можете проверить решение, описанное в my блоге. Он использует дерево выражений, чтобы узнать, является ли значение 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())
{

}

иногда это имеет смысл. Но лично я бы избегал таких вещей... поскольку это не ясно, почему вы можете вызвать метод объекта, который на самом деле является нулевым.


в отдельную 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, посмотрите на кислород. Это удивительный, очень современный язык OO, который нацелен на .NET (а также Java и какао как хорошо. Да. Все изначально, это действительно довольно удивительная цепочка инструментов.)

Oxygene имеет оператор двоеточия, который делает именно то, что вы просите. Процитировать их страница функций разных языков:

Оператор Двоеточия (":")

в Oxygene, как в много из языков оно был под влиянием"."оператор используется для вызова членов класса или объект, например

var x := y.SomeProperty;

Это "разыменования" объекта, содержащегося в "y", вызывает (в данном случае) свойство getter и возвращает его значение. Если "y "оказывается неназначенным (т. е." nil"), возникает исключение.

оператор ":" работает примерно так же, но вместо того, чтобы бросать исключение для неназначенного объекта, результат будет просто равен нулю. Для разработчиков, исходящих из Objective-C, это будет знакомо, так как как Objective-C вызывает метод, используя синтаксис [], тоже.

... (щелчок)

где": "действительно светит при доступе к свойствам в цепочке, где любой элемент может быть равен нулю. например, следующий код:

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

будет работать без ошибок, и возвращает nil, если любой из объектов в цепочке равны нулю - форма, кнопка или ее заголовок.


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

на самом деле не делают этого. сделайте нулевые проверки и выясните, с каким форматированием вы можете лучше всего жить.


у меня есть расширение, которое может быть полезно для этого; ValueOrDefault(). Он принимает лямбда-оператор и оценивает его, возвращая либо оцененное значение, либо значение по умолчанию, если какие-либо ожидаемые исключения (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", который проверяет, является ли объект, на котором он вызывается, нулевым, а если нет, возвращает одно из его запрошенных свойств или выполняет с ним некоторые методы. Это работает конечно только для ссылочных типов, поэтому у меня соответствующее родовое ограничение.

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


насколько я люблю C#, это одна вещь, которая нравится в C++ при работе непосредственно с экземплярами объектов; некоторые объявления просто не может быть null, поэтому нет необходимости проверять на null.

лучший способ получить кусок этого пирога в C# (что может быть слишком много редизайна с вашей стороны - в этом случае, возьмите свой выбор из других ответов) - это с struct. Хотя вы можете оказаться в ситуации, когда структура имеет удаленные значения " по умолчанию "(т. е. 0, 0.0, null string) никогда не нужно проверять"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? Если nulls укажет на ошибку в коде, вы можете использовать контракты кода. Они подберут его, если вы получите nulls во время тестирования, а затем уйдут в производственную версию. Что-то вроде этого:--2-->

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. 
        }
    }
}

конечно, если возможные нули приходят откуда-то еще, вам нужно будет уже обусловить данные. И если какой-либо из нулей действителен, вы не должны делать ненулевое частью контракта, вы нужно их проверить и обработать соответствующим образом.


один из способов удаления проверки на практике, чтобы инкапсулировать функциональность в другом месте. Один из способов сделать это через геттеры и сеттеры. Например, вместо этого:

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

затем, когда вы называете "человеком.contact", код в методе " get " будет запущен, тем самым инициализируя значение, если оно равно null.

вы можете применить эту же методологию ко всем свойствам, которые могут быть null во всех ваших типы. Преимущества этого подхода заключаются в том, что он 1) предотвращает необходимость выполнения нулевых проверок в строке и 2) делает ваш код более читаемым и менее подверженным ошибкам копирования и вставки.

следует отметить, однако, что если вы оказались в ситуации, когда вам нужно выполнить какое-то действие, если одно из свойств is null (т. е. действительно ли человек с нулевым контактом означает что-то в вашем домене?), то этот подход будет скорее помехой, чем помощью. Однако, если свойства, о которых идет речь, должны никогда быть 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");

Offcourse если у вас есть опечатка в именах, результат будет неправильным (скорее всего)..