Глубокая нулевая проверка, есть ли лучший способ?

130

Примечание: Этот вопрос был задан до введения в .?операторах в C # 6 / Visual Studio 2015 .

Мы все там были, у нас есть какое-то глубокое свойство, например cake.frosting.berries.loader, которое нам нужно проверить, является ли оно нулевым, чтобы не было исключений. Чтобы сделать это, используйте короткое замыкание оператора if.

if (cake != null && cake.frosting != null && cake.frosting.berries != null) ...

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

Возможно ли использование какого-либо метода расширения, или это будет языковая функция, или это просто плохая идея?

Homde
источник
3
Я достаточно часто мечтал об этом, но все идеи, которые мне приходили, были хуже, чем настоящая проблема.
peterchen
Спасибо за все ответы и интересно видеть, что у других людей были такие же мысли. Я подумал о том, как бы я сам хотел, чтобы это было решено, и хотя решения Эрика хороши, я думаю, я бы просто написал что-то вроде этого if (IsNull (abc)) или if (IsNotNull (abc)), но, возможно, это как раз в моем вкусе :)
Homde
Когда вы инстанцируете глазурь, у него есть свойство ягод, поэтому в этот момент в вашем конструкторе вы можете просто указать замораживанию, что всякий раз, когда он создается для создания пустых (не нулевых) ягод? и всякий раз, когда ягоды видоизменяются, глазурь проверяет ценность ????
Дуг Чемберлен
В некоторой степени связанные, некоторые из этих методов, которые я нашел предпочтительными для решения проблемы «глубоких нулей», которую я пытался обойти. stackoverflow.com/questions/818642/…
AaronLS 06

Ответы:

223

Мы рассмотрели возможность добавления новой операции «?». на язык, который имеет желаемую семантику. (И это было добавлено сейчас; см. Ниже.) То есть, вы бы сказали

cake?.frosting?.berries?.loader

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

Он не стал планкой для C # 4. Возможно, для гипотетической будущей версии языка.

Обновление (2014):?. оператор теперь планируется в следующей версии компилятора Рослин. Обратите внимание, что до сих пор ведутся споры о точном синтаксическом и семантическом анализе оператора.

Обновление (июль 2015 г.): выпущена Visual Studio 2015, которая поставляется с компилятором C #, который поддерживает условные операторы NULL ?.и?[] .

Эрик Липперт
источник
10
Без точки это становится синтаксически неоднозначным с условным оператором (A? B: C). Мы стараемся избегать лексических конструкций, которые требуют от нас произвольно «заглядывать вперед» в потоке токенов. (Хотя, к сожалению, такие конструкции уже есть в C #; мы бы предпочли не добавлять больше.)
Эрик Липперт
33
@ Ян: эта проблема встречается очень часто. Это один из самых частых запросов, которые мы получаем.
Эрик Липперт
7
@ Ян: Я также предпочитаю использовать шаблон нулевого объекта, когда это возможно, но большинство людей не могут позволить себе роскошь работать с объектными моделями, которые они сами разработали. Многие существующие объектные модели используют нули, и это мир, в котором мы должны жить.
Эрик Липперт
12
@John: Мы получаем этот запрос функции почти полностью от наших самых опытных программистов. MVP все время просят об этом . Но я понимаю, что мнения расходятся; если вы хотите дать конструктивное предложение по дизайну языка в дополнение к вашей критике, я буду рад его рассмотреть.
Эрик Липперт,
28
@lazyberezovsky: я никогда не понимал так называемого «закона» Деметры; во-первых, это, кажется, более точно будет называться «Предложение Деметры». И, во-вторых, результатом доведения «только одного члена доступа» до его логического завершения являются «объекты Бога», где каждый объект должен делать все для каждого клиента, а не иметь возможность раздавать объекты, которые знают, как делать то, что клиент хочет. Я предпочитаю полную противоположность закона Деметры: каждый объект хорошо решает небольшое количество проблем, и одним из этих решений может быть «вот еще один объект, который решает вашу проблему лучше»
Эрик Липперт
27

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

Итак, я создал метод расширения, который позволит вам писать:

var berries = cake.IfNotNull(c => c.Frosting.Berries);

Это вернет Berries, если ни одна часть выражения не является нулевой. Если встречается значение null, возвращается значение null. Однако есть некоторые предостережения: в текущей версии он будет работать только с простым доступом к членам, и он работает только в .NET Framework 4, потому что он использует метод MemberExpression.Update, который является новым в v4. Это код для метода расширения IfNotNull:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace dr.IfNotNullOperator.PoC
{
    public static class ObjectExtensions
    {
        public static TResult IfNotNull<TArg,TResult>(this TArg arg, Expression<Func<TArg,TResult>> expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            if (ReferenceEquals(arg, null))
                return default(TResult);

            var stack = new Stack<MemberExpression>();
            var expr = expression.Body as MemberExpression;
            while(expr != null)
            {
                stack.Push(expr);
                expr = expr.Expression as MemberExpression;
            } 

            if (stack.Count == 0 || !(stack.Peek().Expression is ParameterExpression))
                throw new ApplicationException(String.Format("The expression '{0}' contains unsupported constructs.",
                                                             expression));

            object a = arg;
            while(stack.Count > 0)
            {
                expr = stack.Pop();
                var p = expr.Expression as ParameterExpression;
                if (p == null)
                {
                    p = Expression.Parameter(a.GetType(), "x");
                    expr = expr.Update(p);
                }
                var lambda = Expression.Lambda(expr, p);
                Delegate t = lambda.Compile();                
                a = t.DynamicInvoke(a);
                if (ReferenceEquals(a, null))
                    return default(TResult);
            }

            return (TResult)a;            
        }
    }
}

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

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

driis
источник
Я впечатлен вашими лямбда-навыками :) синтаксис, однако, кажется немного сложнее, чем хотелось бы, по крайней мере, для сценария if-statement
Homde
Круто, но он выполняет в 100 раз больше кода, чем if .. &&. Это имеет смысл только в том случае, если он компилируется до if .. &&.
Monstieur 08
1
Ну а потом я увидел DynamicInvokeтам. Я неукоснительно избегаю этого :)
nawfal
24

Я нашел это расширение весьма полезным для сценариев глубокого вложения.

public static R Coal<T, R>(this T obj, Func<T, R> f)
    where T : class
{
    return obj != null ? f(obj) : default(R);
}

Эту идею я заимствовал из оператора объединения NULL в C # и T-SQL. Приятно то, что возвращаемый тип всегда является возвращаемым типом внутреннего свойства.

Таким образом вы можете сделать это:

var berries = cake.Coal(x => x.frosting).Coal(x => x.berries);

... или небольшое изменение вышеперечисленного:

var berries = cake.Coal(x => x.frosting, x => x.berries);

Это не лучший синтаксис, который я знаю, но он работает.

Джон Лейдегрен
источник
Почему «Уголь», выглядит крайне жутко. ;) Однако ваш образец не прошел бы, если бы инея была нулевой. Должно было выглядеть так: var Berries = cake.NullSafe (c => c.Frosting.NullSafe (f => f.Berries));
Роберт Гизеке
О, но вы подразумеваете, что второй аргумент не является призывом к Coal, как это, конечно, должно быть. Просто удобная переделка. Селектор (x => x.berries) передается вызову Coal внутри метода Coal, который принимает два аргумента.
Джон Лейдегрен,
Название coalescing или coalesce было взято из T-SQL, отсюда у меня впервые появилась идея. IfNotNull подразумевает, что что-то имеет место, если не null, однако то, что это такое, не объясняется вызовом метода IfNotNull. Уголь - действительно странное название, но на самом деле это странный метод, на который стоит обратить внимание.
Джон Лейдегрен,
Лучшее буквальное название для этого - что-то вроде «ReturnIfNotNull» или «ReturnOrDefault»
Джон Лейдегрен
@flq +1 ... в нашем проекте его еще называют IfNotNull :)
Марк Сигрист
16

Помимо нарушения Закона Деметры, как уже указал Мехрдад Афшари, мне кажется, что для логики принятия решений вам нужна «глубокая проверка нуля».

Чаще всего это происходит, когда вы хотите заменить пустые объекты значениями по умолчанию. В этом случае вам следует рассмотреть возможность реализации шаблона нулевого объекта . Он действует как заменитель реального объекта, предоставляя значения по умолчанию и методы «бездействия».

Иоганнес Рудольф
источник
нет, objective-c позволяет отправлять сообщения нулевым объектам и при необходимости возвращает соответствующее значение по умолчанию. Никаких проблем нет.
Йоханнес Рудольф
2
Да. В этом-то и дело. По сути, вы будете имитировать поведение ObjC с помощью шаблона Null Object.
Mehrdad Afshari
10

Обновление: начиная с Visual Studio 2015, компилятор C # (языковая версия 6) теперь распознает ?.оператор, что упрощает «глубокую нулевую проверку». См. Этот ответ для подробностей.

Помимо перепроектирования вашего кода, как предлагается в этом удаленном ответе , другим (хотя и ужасным) вариантом было бы использование try…catchблока, чтобы увидеть, NullReferenceExceptionпроисходит ли когда- нибудь во время этого глубокого поиска свойств.

try
{
    var x = cake.frosting.berries.loader;
    ...
}
catch (NullReferenceException ex)
{
    // either one of cake, frosting, or berries was null
    ...
}

Я лично не стал бы этого делать по следующим причинам:

  • Это не выглядит красиво.
  • Он использует обработку исключений, которая должна быть нацелена на исключительные ситуации, а не на то, что, как вы ожидаете, часто будет происходить в нормальном ходе работы.
  • NullReferenceExceptions, вероятно, никогда не следует перехватывать явно. (См. Этот вопрос .)

Возможно ли использование какого-либо метода расширения или это будет языковая функция, [...]

Это почти наверняка должны быть функцию языка (которая доступна в C # 6 в форме .?и ?[]операторов), если C # уже не было более сложные ленивые вычисления, или если вы не хотите использовать отражение (который , вероятно , также не является хорошая идея по соображениям производительности и безопасности типов).

Поскольку нет способа просто перейти cake.frosting.berries.loaderк функции (она будет оценена и вызовет исключение с нулевой ссылкой), вам придется реализовать общий метод поиска следующим образом: он принимает объекты и имена свойств для уважать:

static object LookupProperty( object startingPoint, params string[] lookupChain )
{
    // 1. if 'startingPoint' is null, return null, or throw an exception.
    // 2. recursively look up one property/field after the other from 'lookupChain',
    //    using reflection.
    // 3. if one lookup is not possible, return null, or throw an exception.
    // 3. return the last property/field's value.
}

...

var x = LookupProperty( cake, "frosting", "berries", "loader" );

(Примечание: код отредактирован.)

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

[...], или это просто плохая идея?

Я бы либо остался с:

if (cake != null && cake.frosting != null && ...) ...

или воспользуйтесь приведенным выше ответом Мехрдада Афшари.


PS: Когда я писал этот ответ, я, очевидно, не рассматривал деревья выражений для лямбда-функций; см., например, ответ @driis для решения в этом направлении. Он также основан на некотором отражении и, следовательно, может работать не так хорошо, как более простое решение ( if (… != null & … != null) …), но с точки зрения синтаксиса его можно считать лучше.

stakx - больше не участвует
источник
2
Я не знаю, почему это было отклонено, я проголосовал за баланс: ответ правильный и вносит новый аспект (и явно упоминает недостатки этого решения ...)
MartinStettner
где «вышеприведенный ответ Мехрдада Афшари»?
Марсон Мао
1
@MarsonMao: Тем временем этот ответ был удален. (Вы все еще можете прочитать его, если ваш рейтинг SO достаточно высок.) Спасибо, что указали на мою ошибку: я должен ссылаться на другие ответы, используя гиперссылку, а не слова типа «см. Выше» / «см. Ниже» (поскольку ответы не появляются в фиксированном порядке). Я обновил свой ответ.
stakx - больше не вносит свой вклад
5

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

NullCoalesce ниже делает именно это, он возвращает новое лямбда-выражение с нулевыми проверками и возврат значения по умолчанию (TResult) в случае, если какой-либо путь равен нулю.

Пример:

NullCoalesce((Process p) => p.StartInfo.FileName)

Вернет выражение

(Process p) => (p != null && p.StartInfo != null ? p.StartInfo.FileName : default(string));

Код:

    static void Main(string[] args)
    {
        var converted = NullCoalesce((MethodInfo p) => p.DeclaringType.Assembly.Evidence.Locked);
        var converted2 = NullCoalesce((string[] s) => s.Length);
    }

    private static Expression<Func<TSource, TResult>> NullCoalesce<TSource, TResult>(Expression<Func<TSource, TResult>> lambdaExpression)
    {
        var test = GetTest(lambdaExpression.Body);
        if (test != null)
        {
            return Expression.Lambda<Func<TSource, TResult>>(
                Expression.Condition(
                    test,
                    lambdaExpression.Body,
                    Expression.Default(
                        typeof(TResult)
                    )
                ),
                lambdaExpression.Parameters
            );
        }
        return lambdaExpression;
    }

    private static Expression GetTest(Expression expression)
    {
        Expression container;
        switch (expression.NodeType)
        {
            case ExpressionType.ArrayLength:
                container = ((UnaryExpression)expression).Operand;
                break;
            case ExpressionType.MemberAccess:
                if ((container = ((MemberExpression)expression).Expression) == null)
                {
                    return null;
                }
                break;
            default:
                return null;
        }
        var baseTest = GetTest(container);
        if (!container.Type.IsValueType)
        {
            var containerNotNull = Expression.NotEqual(
                container,
                Expression.Default(
                    container.Type
                )
            );
            return (baseTest == null ?
                containerNotNull :
                Expression.AndAlso(
                    baseTest,
                    containerNotNull
                )
            );
        }
        return baseTest;
    }
Удвоить
источник
4

Один из вариантов - использовать Null Object Patten, поэтому вместо null, когда у вас нет торта, у вас есть NullCake, который возвращает NullFosting и т. Д. Извините, я не очень хорошо объясняю это, но другие люди, см.

Ян Рингроуз
источник
3

Я тоже часто мечтал о более простом синтаксисе! Это становится особенно уродливым, когда у вас есть возвращаемые значения метода, которые могут быть нулевыми, потому что тогда вам понадобятся дополнительные переменные (например:cake.frosting.flavors.FirstOrDefault().loader )

Однако вот довольно приличная альтернатива, которую я использую: создать вспомогательный метод Null-Safe-Chain. Я понимаю, что это очень похоже на ответ @ John выше (с Coalметодом расширения), но я считаю, что это более просто и требует меньше ввода. Вот как это выглядит:

var loader = NullSafe.Chain(cake, c=>c.frosting, f=>f.berries, b=>b.loader);

Вот реализация:

public static TResult Chain<TA,TB,TC,TResult>(TA a, Func<TA,TB> b, Func<TB,TC> c, Func<TC,TResult> r) 
where TA:class where TB:class where TC:class {
    if (a == null) return default(TResult);
    var B = b(a);
    if (B == null) return default(TResult);
    var C = c(B);
    if (C == null) return default(TResult);
    return r(C);
}

Я также создал несколько перегрузок (от 2 до 6 параметров), а также перегрузок, которые позволяют цепочке заканчиваться типом значения или значением по умолчанию. У меня это действительно хорошо работает!

Скотт Риппи
источник
1

Существует проект Maybe Codeplex, который реализует Maybe или IfNotNull с использованием лямбда-выражений для глубоких выражений в C #

Пример использования:

int? CityId= employee.Maybe(e=>e.Person.Address.City);

Ссылка была предложена в аналогичном вопросе Как проверить наличие нулей в глубоком лямбда-выражении?

Майкл Фрейджейм
источник
1

Как было предложено в John Leidegren «s ответа , один подход к работе вокруг этого является использование методов расширения и делегатов. Их использование может выглядеть примерно так:

int? numberOfBerries = cake
    .NullOr(c => c.Frosting)
    .NullOr(f => f.Berries)
    .NullOr(b => b.Count());

Реализация запутана, потому что вам нужно заставить ее работать для типов значений, ссылочных типов и типов значений, допускающих значение NULL. Вы можете найти полную реализацию в Timwi «s ответа на Что такое правильный способ проверить для значений нуля? ,

Сэм
источник
1

Или вы можете использовать отражение :)

Функция отражения:

public Object GetPropValue(String name, Object obj)
    {
        foreach (String part in name.Split('.'))
        {
            if (obj == null) { return null; }

            Type type = obj.GetType();
            PropertyInfo info = type.GetProperty(part);
            if (info == null) { return null; }

            obj = info.GetValue(obj, null);
        }
        return obj;
    }

Использование:

object test1 = GetPropValue("PropertyA.PropertyB.PropertyC",obj);

Мой случай (вернуть DBNull.Value вместо null в функции отражения):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));
heybeliman
источник
1

Попробуйте этот код:

    /// <summary>
    /// check deep property
    /// </summary>
    /// <param name="obj">instance</param>
    /// <param name="property">deep property not include instance name example "A.B.C.D.E"</param>
    /// <returns>if null return true else return false</returns>
    public static bool IsNull(this object obj, string property)
    {
        if (string.IsNullOrEmpty(property) || string.IsNullOrEmpty(property.Trim())) throw new Exception("Parameter : property is empty");
        if (obj != null)
        {
            string[] deep = property.Split('.');
            object instance = obj;
            Type objType = instance.GetType();
            PropertyInfo propertyInfo;
            foreach (string p in deep)
            {
                propertyInfo = objType.GetProperty(p);
                if (propertyInfo == null) throw new Exception("No property : " + p);
                instance = propertyInfo.GetValue(instance, null);
                if (instance != null)
                    objType = instance.GetType();
                else
                    return true;
            }
            return false;
        }
        else
            return true;
    }
JKSUN
источник
0

Я опубликовал это вчера вечером, а затем друг указал мне на этот вопрос. Надеюсь, поможет. Затем вы можете сделать что-то вроде этого:

var color = Dis.OrDat<string>(() => cake.frosting.berries.color, "blue");


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

namespace DeepNullCoalescence
{
  public static class Dis
  {
    public static T OrDat<T>(Expression<Func><T>> expr, T dat)
    {
      try
      {
        var func = expr.Compile();
        var result = func.Invoke();
        return result ?? dat; //now we can coalesce
      }
      catch (NullReferenceException)
      {
        return dat;
      }
    }
  }
}

Прочтите полную запись в блоге здесь .

Тот же друг также посоветовал вам посмотреть это .

Тайлер Дженсен
источник
3
Зачем возиться с an, Expressionесли вы собираетесь просто скомпилировать и поймать? Просто используйте Func<T>.
Скотт Риппи
0

Я немного изменил код отсюда, чтобы он работал на заданный вопрос:

public static class GetValueOrDefaultExtension
{
    public static TResult GetValueOrDefault<TSource, TResult>(this TSource source, Func<TSource, TResult> selector)
    {
        try { return selector(source); }
        catch { return default(TResult); }
    }
}

И да, это, вероятно, не оптимальное решение из-за проблем с производительностью try / catch, но оно работает:>

Использование:

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);
Kaptan
источник
0

Если вам нужно этого добиться, сделайте следующее:

использование

Color color = someOrder.ComplexGet(x => x.Customer.LastOrder.Product.Color);

или

Color color = Complex.Get(() => someOrder.Customer.LastOrder.Product.Color);

Реализация вспомогательного класса

public static class Complex
{
    public static T1 ComplexGet<T1, T2>(this T2 root, Func<T2, T1> func)
    {
        return Get(() => func(root));
    }

    public static T Get<T>(Func<T> func)
    {
        try
        {
            return func();
        }
        catch (Exception)
        {
            return default(T);
        }
    }
}
kernowcode
источник
-3

Мне нравится подход Objective-C:

«Язык Objective-C использует другой подход к этой проблеме и не вызывает методы с nil, а вместо этого возвращает nil для всех таких вызовов».

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}
Шьям Вемури
источник
1
то, что делает другой язык (и ваше мнение о нем), почти не имеет никакого отношения к тому, чтобы заставить его работать на C #. Это никому не помогает решить их проблему с C #
ADyson 01