Глубокая проверка нуля, есть ли лучший способ? - PullRequest
128 голосов
/ 17 января 2010

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

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

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

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

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

Ответы [ 16 ]

218 голосов
/ 17 января 2010

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

cake?.frosting?.berries?.loader

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

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

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

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

27 голосов
/ 17 января 2010

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

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

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

Это вернет Ягоды, если ни одна часть выражения не будет нулевой. Если встречается нуль, возвращается ноль. Однако есть некоторые предостережения: в текущей версии он будет работать только с простым доступом к элементу и работать только в .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. Считайте, что это код для проверки концепции, и имейте в виду, что его использование приведет к снижению производительности (что, вероятно, не будет иметь большого значения во многих случаях, но не используйте его в тесном цикле :-))

23 голосов
/ 17 января 2010

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

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

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

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

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

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

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

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

16 голосов
/ 17 января 2010

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

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

10 голосов
/ 17 января 2010

Обновление: Начиная с 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
    ...
}

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

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

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

Это почти наверняка должна быть языковая функция (которая доступна в 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 && ...) ...

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


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

5 голосов
/ 14 февраля 2011

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

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

Пример:

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 голосов
/ 08 февраля 2010

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

3 голосов
/ 15 ноября 2011

Я тоже часто хотел более простой синтаксис! Это становится особенно уродливо, когда у вас есть возвращаемые значения метода, которые могут быть нулевыми, потому что тогда вам нужны дополнительные переменные (например: 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 голос
/ 24 июня 2015

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

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

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 вместо нуля в функции отражения):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));
1 голос
/ 12 декабря 2013

Как предлагается в Джон Лейдгрен ответ , один из способов обойти это - использовать методы расширения и делегаты. Их использование может выглядеть примерно так:

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

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

...