it-roy-ru.com

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

Однако эта проверка завершается неудачно, если адрес, контакт или само лицо имеют значение 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()?

179

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

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

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

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

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

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


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

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

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

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

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

62
Kevin

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

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

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

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

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

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

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

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

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

37
bigge

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

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

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

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

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

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

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

string city = person.NullSafeGet(n => n.Contact.Address.City);
10
Sandor Drieënhuizen

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

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

а потом:

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

}

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

7
Vladimir Gondarev

Сделайте это в отдельном method как:

private test()
{
    var person = new Person();
    if (!IsNull(person))
    {
        // Proceed
              ........

Где ваше IsNullmethod находится

public bool IsNull(Person person)
{
    if(Person != null && 
       Person.Contact != null && 
       Person.Contact.Address != null && 
       Person.Contact.Address.City != null)
          return false;
    return true;
}
5
Ashok Damani

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

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

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

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

var x := y.SomeProperty;

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

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

... (отрывок)

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

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

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

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

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

3
jwg

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

Тем не менее, можно однозначно утверждать, что длина кода не сильно изменилась.

3
Zoltán Tamási

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

Перегрузка, не принимающая определенного значения по умолчанию, возвратит нуль для любого ссылочного типа. Это должно работать в вашем сценарии:

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

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

Всегда лучше использовать 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.

Но также здесь мои мысли для нулевых значений

  • Как правило, возврат нулевых значений - это лучшая идея, если кто-то пытается указать, что данных нет.

  • Если объект не является нулевым, но пустым, это означает, что данные были возвращены, тогда как возвращение нулевого значения ясно указывает на то, что ничего не было возвращено.

  • Также IMO, если вы вернете null, это приведет к нулевому исключению, если вы попытаетесь получить доступ к элементам в объекте, что может быть полезно для выделения ошибочного кода.

В C # есть два разных вида равенства:

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

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

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

Обратитесь к статье MSDN Рекомендации по перегрузке Equals () и Operator == (Руководство по программированию в C #) для получения более подробной информации.

2
Microtechie

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

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

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

1
Katana314

В зависимости от того, какова цель использования переменной "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
    }
}

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

1
Thomas

При каких обстоятельствах эти вещи могут быть нулевыми? Если 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 является действительным, вы не должны включать ненулевое значение в контракт, вам необходимо проверить их и соответствующим образом обработать.

1
digitig

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

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

0
TDull

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

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

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

--jtlovetteiii

0
jtlovetteiii