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.