C # елегантний спосіб перевірити, чи є властивість властивості нульовою


98

У C # скажіть, що ви хочете отримати значення з PropertyC у цьому прикладі, і ObjectA, PropertyA та PropertyB можуть бути нульовими.

ObjectA.PropertyA.PropertyB.PropertyC

Як я можу безпечно отримати PropertyC із найменшою кількістю коду?

Зараз я би перевірив:

if(ObjectA != null && ObjectA.PropertyA !=null && ObjectA.PropertyA.PropertyB != null)
{
    // safely pull off the value
    int value = objectA.PropertyA.PropertyB.PropertyC;
}

Було б непогано зробити щось подібне (псевдокод).

int value = ObjectA.PropertyA.PropertyB ? ObjectA.PropertyA.PropertyB : defaultVal;

Можливо, ще більше впав із оператором нульового злиття.

EDIT Спочатку я сказав, що мій другий приклад був схожий на js, але я змінив його на psuedo-код, оскільки було правильно вказано, що він не буде працювати в js.


я не бачу, як працює ваш приклад js. ви повинні отримувати помилку "очікуваний об'єкт", коли ObjectAабо PropertyAє нулем.
lincolnk

Відповіді:


118

У C # 6 ви можете використовувати нульовий умовний оператор . Тож оригінальним тестом буде:

int? value = objectA?.PropertyA?.PropertyB?.PropertyC;

2
Ви могли б пояснити, що це робить? Що valueдорівнює, якщо PropertyCнуль? або якщо PropertyBмає значення null? а якщо Object Aнуль?
Каньйон

1
Якщо БУДЬ-ЯК з цих властивостей є нульовим, тоді весь оператор повертається як null. Починається зліва направо. Без синтаксичного цукру це еквівалентно ряду тверджень if, де if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......з кінцевим останнім виразом єif(propertyZ != null) { value = propertyZ }
детектив

27

Метод короткого розширення:

public static TResult IfNotNull<TInput, TResult>(this TInput o, Func<TInput, TResult> evaluator)
  where TResult : class where TInput : class
{
  if (o == null) return null;
  return evaluator(o);
}

Використання

PropertyC value = ObjectA.IfNotNull(x => x.PropertyA).IfNotNull(x => x.PropertyB).IfNotNull(x => x.PropertyC);

Цей простий метод розширення та багато іншого ви можете знайти на http://devtalk.net/csharp/chained-null-checks-and-the-maybe-monad/

Редагувати:

Після його використання на даний момент, я думаю, що власною назвою цього методу має бути IfNotNull () замість оригінального With ().


15

Чи можете ви додати метод до свого класу? Якщо ні, ви думали про використання методів розширення? Ви можете створити метод розширення для типу об'єкта, який називається GetPropC().

Приклад:

public static class MyExtensions
{
    public static int GetPropC(this MyObjectType obj, int defaltValue)
    {
        if (obj != null && obj.PropertyA != null & obj.PropertyA.PropertyB != null)
            return obj.PropertyA.PropertyB.PropertyC;
        return defaltValue;
    }
}

Використання:

int val = ObjectA.GetPropC(0); // will return PropC value, or 0 (defaltValue)

До речі, це передбачає, що ви використовуєте .NET 3 або новішу версію.


11

Те, як ви це робите, є правильним.

Ви можете використовувати фокус, як описаний тут , використовуючи вирази Linq:

int value = ObjectA.NullSafeEval(x => x.PropertyA.PropertyB.PropertyC, 0);

Але набагато повільніше, що вручну перевіряти кожну власність ...


10

Рефактор дотримується закону Деметра


Я не вважаю графік об'єктів лише глибиною трьох рівнів, що потребує рефакторингу, коли ви лише читаєте властивості. Я б погодився, якщо операційна програма хотіла викликати метод для об'єкта, на який посилається через PropertyC, але не тоді, коли це властивість, для перевірки якої має значення null лише перед читанням. У цьому прикладі він може бути таким же простим, як Customer.Address.Country, де країна може бути еталонним типом, таким як KeyValuePair. Як би ви переформатували це, щоб запобігти необхідності перевірки нульових посилань?
Даррен Льюїс,

Приклад OP насправді має глибину 4. Моя пропозиція полягає не в тому, щоб зняти нульові перевірки посилань, а знайти їх у об'єктах, які, швидше за все, зможуть належним чином обробити їх. Як і у більшості "великих правил", є винятки, але я не впевнений, що це одне. Чи можемо ми погодитися не погодитися?
rtalbot

3
Я погоджуюсь з @rtalbot (правда, справедливості заради @Daz Lewis пропонує 4-глибокий приклад, оскільки останнім елементом є KeyValuePair). Якщо щось псується з об’єктом Клієнта, то я не бачу, який бізнес він переглядає через ієрархію об’єктів Адреса. Припустимо, ви згодом вирішите, що KeyValuePair не була такою чудовою ідеєю для власності Country. У такому випадку коди кожного повинні змінити. Це не хороший дизайн.
Jeffrey L Whitledge

10

Оновлення 2014: C # 6 має новий оператор, який ?.називається `` безпечна навігація '' або `` нульове розповсюдження ''

parent?.child

Читайте http://blogs.msdn.com/b/jerrynixon/archive/2014/02/26/at-last-c-is-getting-sometimes-called-the-safe-navigation-operator.aspx подробиці

Це вже давно надзвичайно популярний запит https://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/3990187-add-operator-to-c-?tracking_code=594c10a522f8e9bc987ee4a5e2c0b38d


8

Ви, очевидно, шукаєте монаду, яка може бути припустима :

string result = new A().PropertyB.PropertyC.Value;

стає

string result = from a in new A()
                from b in a.PropertyB
                from c in b.PropertyC
                select c.Value;

Це повертається null, якщо будь-яке з властивостей, що допускають нуль, має значення null; в іншому випадку значення Value.

class A { public B PropertyB { get; set; } }
class B { public C PropertyC { get; set; } }
class C { public string Value { get; set; } }

Методи розширення LINQ:

public static class NullableExtensions
{
    public static TResult SelectMany<TOuter, TInner, TResult>(
        this TOuter source,
        Func<TOuter, TInner> innerSelector,
        Func<TOuter, TInner, TResult> resultSelector)
        where TOuter : class
        where TInner : class
        where TResult : class
    {
        if (source == null) return null;
        TInner inner = innerSelector(source);
        if (inner == null) return null;
        return resultSelector(source, inner);
    }
}

Чому тут метод розширення? Він не використовується.
Младен Михайлович

1
@MladenMihajlovic: SelectManyметод розширення використовується from ... in ... from ... in ...синтаксисом.
dtb

5

Якщо припустимо, що у вас є порожні значення типів, одним підходом було б таке:

var x = (((objectA ?? A.Empty).PropertyOfB ?? B.Empty).PropertyOfC ?? C.Empty).PropertyOfString;

Я великий фанат C #, але дуже приємною річчю в новій Java (1.7?) Є. оператор:

 var x = objectA.?PropertyOfB.?PropertyOfC.?PropertyOfString;

1
Це справді буде в Java 1.7? Його просили в C # давно, але я сумніваюся, що це коли-небудь станеться ...
Томас Левеск

На жаль, у мене немає порожніх значень. Цей синтаксис Java виглядає солодко! Я буду голосувати за це лише тому, що хочу цього синтаксису!
Джон Краг,

3
Томас: Востаннє я перевіряв tech.puredanger.com/java7, це означає, що Java отримає це. Але тепер, коли я повторно перевіряю, він говорить: «Нульова безпечна робота»: НІ. Тож я відкликаю свою заяву і замінюю її новою: вона була запропонована для Java 1.7, але не вчинила її.
Просто черговий метапрограміст

Додатковим підходом є той, який використовує monad.net
Ще один метапрограмер

1
Схоже, що. Оператор знаходиться у Visual Studio 2015 https://msdn.microsoft.com/en-us/library/dn986595.aspx
Едвард

4

Цей код - "найменша кількість коду", але не найкраща практика:

try
{
    return ObjectA.PropertyA.PropertyB.PropertyC;
}
catch(NullReferenceException)
{
     return null;
}

1
Я бачив такий код, як цей, і, не зважаючи на втрату продуктивності, найбільша проблема полягає в тому, що він ускладнює налагодження, оскільки реальний виняток тоне в мільйонах марних винятків null ref.
Просто черговий метапрограміст

Іноді забавно читати власну відповідь через 3 роки. Думаю, я б сьогодні відповів інакше. Я б сказав, що цей код порушує закон Деметра, і я б рекомендував рефакторинг, щоб він не став.
Борис Моділевський

1
На сьогоднішній день, через 7 років після оригінальної відповіді, я приєднався б до @Phillip Ngan і використав би C # 6 із таким синтаксисом: int? значення = objectA? .PropertyA? .PropertyB? .PropertyC;
Борис Модилевський

4

Коли мені потрібно зациклювати дзвінки таким чином, я покладаюся на допоміжний метод, який я створив, TryGet ():

    public static U TryGet<T, U>(this T obj, Func<T, U> func)
    {
        return obj.TryGet(func, default(U));
    }

    public static U TryGet<T, U>(this T obj, Func<T, U> func, U whenNull)
    {
        return obj == null ? whenNull : func(obj);
    }

У вашому випадку ви б використовували його так:

    int value = ObjectA
        .TryGet(p => p.PropertyA)
        .TryGet(p => p.PropertyB)
        .TryGet(p => p.PropertyC, defaultVal);

Я не думаю, що цей код працює. Який тип defaultVal? var p = new Person (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Кіт,

Приклад, який я написав, слід читати як такий: ObjectA.PropertyA.PropertyB.PropertyC. Схоже, ваш код намагається завантажити властивість під назвою "Прізвище" з "Ім'я", яка не є передбачуваним використанням. Більш правильним прикладом може бути щось на зразок: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); До речі, мій допоміжний метод TryGet () дуже схожий на нову функцію в C # 6.0 - нульовий умовний оператор. Його використання буде приблизно таким: var postcode = person? .Address? .Postcode; msdn.microsoft.com/en-us/magazine/dn802602.aspx
Емануель

3

Я бачив щось у новому C # 6.0, це за допомогою '?' замість перевірки

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

if (Person != null && Person.Contact!=null && Person.Contact.Address!= null && Person.Contact.Address.City != null)
{ 
  var city = person.contact.address.city;
}

ви просто використовуєте

var city = person?.contact?.address?.city;

Сподіваюся, це комусь допомогло.


ОНОВЛЕННЯ:

Ви могли б зробити це зараз

 var city = (Person != null)? 
           ((Person.Contact!=null)? 
              ((Person.Contact.Address!= null)?
                      ((Person.Contact.Address.City!=null)? 
                                 Person.Contact.Address.City : null )
                       :null)
               :null)
            : null;

1

Ви можете зробити це:

class ObjectAType
{
    public int PropertyC
    {
        get
        {
            if (PropertyA == null)
                return 0;
            if (PropertyA.PropertyB == null)
                return 0;
            return PropertyA.PropertyB.PropertyC;
        }
    }
}



if (ObjectA != null)
{
    int value = ObjectA.PropertyC;
    ...
}

Або ще краще може бути таке:

private static int GetPropertyC(ObjectAType objectA)
{
    if (objectA == null)
        return 0;
    if (objectA.PropertyA == null)
        return 0;
    if (objectA.PropertyA.PropertyB == null)
        return 0;
    return objectA.PropertyA.PropertyB.PropertyC;
}


int value = GetPropertyC(ObjectA);

1

Ви можете використовувати наступне розширення, і я думаю, що це дійсно добре:

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<TF, TR>(TF t, Func<TF, TR> f)
    where TF : class
{
    return t != null ? f(t) : default(TR);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, TR>(T1 p1, Func<T1, T2> p2, Func<T2, TR> p3)
    where T1 : class
    where T2 : class
{
    return Get(Get(p1, p2), p3);
}

/// <summary>
/// Simplifies null checking
/// </summary>
public static TR Get<T1, T2, T3, TR>(T1 p1, Func<T1, T2> p2, Func<T2, T3> p3, Func<T3, TR> p4)
    where T1 : class
    where T2 : class
    where T3 : class
{
    return Get(Get(Get(p1, p2), p3), p4);
}

І використовується так:

int value = Nulify.Get(objectA, x=>x.PropertyA, x=>x.PropertyB, x=>x.PropertyC);

0

Я записав би свій власний метод у тип PropertyA (або метод розширення, якщо це не ваш тип), використовуючи аналогічний зразок типу Nullable.

class PropertyAType
{
   public PropertyBType PropertyB {get; set; }

   public PropertyBType GetPropertyBOrDefault()
   {
       return PropertyB != null ? PropertyB : defaultValue;
   }
}

Ну, в такому випадку, очевидно, PropertyB ніколи не може мати значення null.
рекурсивна

0

Щойно натрапив на цей пост.

Деякий час тому я зробив пропозицію на Visual Studio Connect щодо додавання нового ???оператора.

http://visualstudio.uservoice.com/forums/121579-visual-studio/suggestions/4104392-add-as-an-recursive-null-reference-check-opera

Це зажадає певної роботи від фреймворкової команди, але не потрібно змінювати мову, а просто робити магію компілятора. Ідея полягала в тому, що компілятор повинен змінити цей код (синтаксис atm не дозволений)

string product_name = Order.OrderDetails[0].Product.Name ??? "no product defined";

в цей код

Func<string> _get_default = () => "no product defined"; 
string product_name = Order == null 
    ? _get_default.Invoke() 
    : Order.OrderDetails[0] == null 
        ? _get_default.Invoke() 
        : Order.OrderDetails[0].Product == null 
            ? _get_default.Invoke() 
            : Order.OrderDetails[0].Product.Name ?? _get_default.Invoke()

Для нульової перевірки це може виглядати так

bool isNull = (Order.OrderDetails[0].Product ??? null) == null;

0

Я написав метод, який приймає значення за замовчуванням, ось як його використовувати:

var teacher = new Teacher();
return teacher.GetProperty(t => t.Name);
return teacher.GetProperty(t => t.Name, "Default name");

Ось код:

public static class Helper
{
    /// <summary>
    /// Gets a property if the object is not null.
    /// var teacher = new Teacher();
    /// return teacher.GetProperty(t => t.Name);
    /// return teacher.GetProperty(t => t.Name, "Default name");
    /// </summary>
    public static TSecond GetProperty<TFirst, TSecond>(this TFirst item1,
        Func<TFirst, TSecond> getItem2, TSecond defaultValue = default(TSecond))
    {
        if (item1 == null)
        {
            return defaultValue;
        }

        return getItem2(item1);
    }
}

1
Це рішення вже подано в інших відповідях (неодноразово). Немає жодної причини публікувати його знову .
Серві

Я не побачив жодного, що приймає значення за замовчуванням.
Акіра Ямамото

Я рахую 6 інших, які використовують визначене значення за замовчуванням. Мабуть, ви не так сильно виглядали.
Серві

0

Це неможливо.
ObjectA.PropertyA.PropertyBвийде з ладу, якщо ObjectAє null через нульову перенаправлення, що є помилкою.

if(ObjectA != null && ObjectA.PropertyA... працює через коротке замикання, тобто ObjectA.PropertyAніколи не буде перевірено, якщо ObjectAє null.

Перший спосіб, який ви пропонуєте, є найкращим і найбільш зрозумілим з намірами. У будь-якому випадку ви можете спробувати переробити дизайн, не маючи покластися на стільки нулів.


-1

Цей підхід досить прямолінійний, як тільки ви переходите через лямбда-гобла:

public static TProperty GetPropertyOrDefault<TObject, TProperty>(this TObject model, Func<TObject, TProperty> valueFunc)  
                                                        where TObject : class
    {
        try
        {
            return valueFunc.Invoke(model);
        }
        catch (NullReferenceException nex)
        {
            return default(TProperty);
        }
    }

З використанням, яке може виглядати так:

ObjectA objectA = null;

Assert.AreEqual(0,objectA.GetPropertyOrDefault(prop=>prop.ObjectB.ObjectB.ObjectC.ID));

Assert.IsNull(objectA.GetPropertyOrDefault(prop => prop.ObjectB));

Цікаво, чому хтось не звернувся до бюджету через 8 років після того, як я надіслав відповідь (що було роками до того, як нульове згуртування C # 6 було річчю).
BlackjacketMack

-3
var result = nullableproperty ?? defaultvalue;

Оператор ??(null-coalescing) означає, якщо перший аргумент є null, поверніть замість нього другий.


2
Ця відповідь не вирішує проблему ОП. Як би ви застосували рішення за допомогою ?? Оператор до ObjectA.PropertyA.PropertyB, коли всі частини виразів (ObjectA, PropertyA та PropertiesB) можуть бути нульовими?
Artemix

Правда, я думаю, що взагалі навіть не читав запитання. У будь-якому разі, неможливо нічого, просто не робіть цього: P static void Main (string [] args) {a ca = new a (); var default_value = new a () {b = new object ()}; значення var = (ca ?? default_value) .b ?? default_value.b; } клас a {загальнодоступний об’єкт b = null; }
Aridane Álamo

(ObjectA ?? DefaultMockedAtNull) .PropertyA! = Null? ObjectA.PropertyA.PropertyB: null
Aridane Álamo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.