Чистый способ сделать нулевую проверку на С#?

Предположим, что у меня есть этот интерфейс,

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 нулевым или нет.)

Однако эта проверка не выполняется, если адрес или контакт или сам человек не имеют значения.

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

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

{ 
    // Do some stuff here..
}

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

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

Ответы

Ответ 1

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

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

Ответ 2

У вашего кода могут быть большие проблемы, чем для проверки нулевых ссылок. Как бы то ни было, вы, вероятно, нарушите Закон Деметры.

Закон Деметры - одна из тех эвристик, таких как Do not Repeat Yourself, которая помогает вам писать легко ремонтируемый код. Он говорит программистам не получать доступ к чему-либо слишком далеко от непосредственной области. Например, предположим, что у меня есть этот код:

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 и a 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 изменяет

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


Тем не менее, я думаю, что бывают случаи, когда соблюдение Закона Деметры неуместно. (Это, в конце концов, эвристическое, а не жесткое правило, хотя оно и называется "законом".)

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

  • У вас есть несколько классов, которые представляют записи, хранящиеся на уровне сохранения вашей программы, AND
  • Вы абсолютно уверены, что вам не нужно будет реорганизовывать эти классы в будущем,

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

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

Ответ 3

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

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

но вам все равно нужно проверить, является ли человек нулевым.

if (person != null && person.HasCity)
{

}

к вашему другому вопросу, для строк вы также можете проверить, является ли это пустым или пустым:

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
}

Ответ 4

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

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

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

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

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

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

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

Ответ 5

Обновление 28/04/2014: Нулевое распространение запланировано для С# 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();
    }

Ответ 6

Второй вопрос,

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

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

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

Ответ 7

Если по какой-то причине вы не против идти с одним из более "лучших" решений, вы можете проверить решение, описанное в моем сообщении в блоге. Он использует дерево выражений, чтобы выяснить, имеет ли значение значение null перед оценкой выражения. Но для обеспечения приемлемости производительности он создает и кэширует IL-код.

Решение позволяет вам написать это:

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

Ответ 8

Вы можете написать:

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

а затем:

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

}

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

Ответ 9

Сделайте это в отдельном 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;
}

Ответ 10

Вам нужен С#, или вы хотите только .NET? Если вы можете смешивать другой язык .NET, посмотрите Oxygene. Это потрясающий, очень современный язык OO, который нацелен на .NET(а также на Java и Cocoa. Да, все это на самом деле довольно удивительная инструментальная цепочка.)

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

Оператор Colon ( ":" )

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

var x := y.SomeProperty;

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

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

... (snip)

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

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

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

Ответ 11

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

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

Ответ 12

У меня есть расширение, которое может быть полезно для этого; 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
        }
    }
}

Ответ 13

Такая ссылочная цепочка может произойти, например, если вы используете инструмент 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());

Однако можно было бы категорически утверждать, что длина кода не слишком сильно изменилась.

Ответ 14

По моему мнению, оператор равенства не является более безопасным и лучшим способом для ссылочного равенства.

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

В С# существуют два разных типа равенства:

  • ссылочное равенство и
  • значение равенства.

Когда тип является неизменным, может оказаться полезным перегрузка оператора == для сравнения равенства значений вместо ссылочного равенства.

Не рекомендуется переопределять оператор == в неизменяемых типах.

Подробнее см. статью MSDN Рекомендации по перегрузке Equals() и Operator == (Руководство по программированию на С#).

Ответ 15

Насколько я люблю С#, это одно из того, что нравится в С++ при непосредственном взаимодействии с экземплярами объектов; некоторые объявления просто не могут быть нулевыми, поэтому нет необходимости проверять значение null.

Лучший способ получить кусочек этого пирога в С# (который может быть немного переделан с вашей стороны - в этом случае, возьмите свой выбор из других ответов) с struct. В то время как вы можете оказаться в ситуации, когда у структуры есть неиспользуемые значения "по умолчанию" (то есть 0, 0.0, пустая строка), никогда не нужно проверять "if (myStruct == null)".

Я бы не переключился на них, не понимая их использования, конечно. Они, как правило, используются для типов значений, а не для больших блоков данных - в любое время, когда вы назначаете структуру из одной переменной в другую, вы, как правило, фактически копируете данные по всему, создавая, по существу, копию каждого из исходных значений ( вы можете избежать этого с помощью ключевого слова ref - снова, прочитайте его, а не просто используйте его). Тем не менее, это может поместиться для вещей, таких как StreetAddress - я, конечно, не лениво использовал бы его во всем, что я не хотел бы проверять.

Ответ 16

В зависимости от цели использования переменной "город", более чистым способом может быть разделение нулевых проверок на разные классы. Таким образом, вы также не нарушаете Закон Деметры. Поэтому вместо:

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

Опять же, это зависит от цели программы.

Ответ 17

В каких обстоятельствах эти вещи могут быть нулевыми? Если 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. 
        }
    }
}

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

Ответ 18

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

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) препятствует вам выполнять нулевые проверки в строке и 2) делает ваш код более читабельным и менее подвержен ошибкам копирования-вставки.

Следует отметить, однако, что если вы оказались в ситуации, когда вам нужно выполнить какое-либо действие, если одно из свойств является нулевым (т.е. лицо с нулевым контактом фактически означает что-то в вашем домене?), то этот подход будет скорее помехой, чем помощью. Однако, если рассматриваемые свойства никогда не должны быть нулевыми, то этот подход даст вам очень чистый способ представления этого факта.

- jtlovetteiii

Ответ 19

Вы можете использовать отражение, чтобы не форсировать реализацию интерфейсов и дополнительный код в каждом классе. Просто класс 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");

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