C # элегантный способ проверить, имеет ли свойство свойство null


98

В C # скажите, что вы хотите извлечь значение из PropertyC в этом примере, а ObjectA, PropertyA и PropertyB могут иметь значение NULL.

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;

Возможно, еще больше обрушился с оператором объединения с нулем.

РЕДАКТИРОВАТЬ Изначально я сказал, что мой второй пример похож на js, но я изменил его на псевдокод, поскольку было правильно указано, что он не будет работать в js.


Я не понимаю, как работает ваш пример js. вы должны получать ошибку «ожидаемый объект» всякий раз, когда ObjectAили PropertyAравны нулю.
Линкольнк,

Ответы:


120

В C # 6 вы можете использовать условный оператор Null . Итак, исходный тест будет:

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

2
не могли бы вы объяснить, что это делает? Что valueравно, если PropertyCравно нулю? или если PropertyBнуль? что если Object Aноль?
Каньон Колоб

1
Если ЛЮБОЕ из этих свойств имеет значение null, то весь оператор возвращается как null. Он начинается слева направо. Без синтаксического сахара это эквивалентно серии операторов if, где if(propertyX == null) {value = null} else if (propertyY == null){ value = null} else if......конечным последним выражением являетсяif(propertyZ != null) { value = propertyZ }
DetectivePikachu

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

Выполните рефакторинг, чтобы соблюсти закон Деметры


Я не считаю, что граф объектов глубиной всего в три уровня нуждается в рефакторинге, когда вы только читаете свойства. Я бы согласился, если бы OP хотел вызвать метод для объекта, на который ссылается PropertyC, но не тогда, когда это свойство требует проверки только на null перед чтением. В этом примере это может быть просто Customer.Address.Country, где Country может быть ссылочным типом, например KeyValuePair. Как бы вы реорганизовали это, чтобы предотвратить необходимость проверки нулевых ссылок?
Даррен Льюис,

Пример OP на самом деле имеет глубину 4. Мое предложение состоит не в том, чтобы удалить проверки нулевых ссылок, а в том, чтобы найти их среди объектов, которые, скорее всего, смогут правильно их обработать. Как и в большинстве «практических правил», есть исключения, но я не уверен, что это одно из них. Можем ли мы согласиться не соглашаться?
rtalbot

4
Я согласен с @rtalbot (хотя, честно говоря, @Daz Lewis предлагает пример с четырьмя глубинами, поскольку последний элемент - KeyValuePair). Если что-то не так с объектом Customer, то я не понимаю, какой бизнес он имеет, просматривая иерархию объекта Address. Предположим, вы позже решите, что KeyValuePair не такая уж хорошая идея для свойства Country. В этом случае нужно изменить код каждого. Плохой дизайн.
Джеффри Л. Уитледж,

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

Очевидно, вы ищете Nullable Monad :

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, равно 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 04

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 = новый человек (); Assert.AreEqual (p.TryGet (x => x.FirstName) .TryGet (x => x.LastName) .TryGet (x => x.NickName, "foo"), "foo");
Кейт

Написанный мною пример следует читать так: ObjectA.PropertyA.PropertyB.PropertyC. Кажется, ваш код пытается загрузить свойство с именем «LastName» из «FirstName», что не является предполагаемым использованием. Более правильным примером может быть что-то вроде: var postcode = person.TryGet (p => p.Address) .TryGet (p => p.Postcode); Кстати, мой вспомогательный метод TryGet () очень похож на новую функцию в C # 6.0 - условный оператор null. Его использование будет таким: 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 никогда не может быть нулевым.
рекурсивный

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
Это решение уже было предоставлено в других ответах (неоднократно). Нет никаких причин для повторной публикации .
Servy 08

Я не видел ни одного, принимающего значение по умолчанию.
Акира Ямамото

Я насчитал 6 других, которые используют определенное значение по умолчанию. Похоже, ты не выглядел так сильно.
Servy 08

0

Это невозможно.
ObjectA.PropertyA.PropertyBзавершится ошибкой, если значение ObjectAравно нулю из-за разыменования нуля, что является ошибкой.

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, вместо того, чтобы возвратить второй.


2
Этот ответ не решает проблему OP. Как бы вы применили решение с помощью ?? оператор ObjectA.PropertyA.PropertyB, когда все части выражения (ObjectA, PropertyA и PropertyB) могут иметь значение NULL?
Artemix

Правда, думаю, я вообще вопрос не читал. В любом случае невозможное - это ничто, просто не делайте этого: P static void Main (string [] args) {a ca = new a (); var default_value = new a () {b = новый объект ()}; var value = (ca ?? default_value) .b ?? default_value.b; } class 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.