Глибока перевірка нуля, чи є кращий спосіб?


130

Примітка: Це питання було задано до введення в .?операторах в C # 6 / Visual Studio 2015 .

Ми всі були там, у нас є якась глибока властивість, наприклад, cake.frosting.berries.loader, яку нам потрібно перевірити, чи недійсна вона, щоб не було винятку. Так можна використовувати короткий замикання if

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

Це не зовсім елегантно, і, мабуть, повинен бути простіший спосіб перевірити всю ланцюжок і побачити, чи не зустрічається вона з нульовою змінною / властивістю.

Чи можливо використовувати якийсь метод розширення чи це мовна особливість, чи це просто погана ідея?


3
Я цього хотів досить часто - але всі ідеї, які я придумав, були гіршими, ніж реальна проблема.
peterchen

Дякую за всі відповіді та цікаво бачити, що інші люди мали такі самі думки. Я задумався над тим, як я хотів би, щоб це вирішив сам, і хоча рішення Еріка добре, я думаю, що я просто повинен написати щось подібне, якщо (IsNull (abc)), або якщо (IsNotNull (abc)), але можливо це просто на мій смак :)
Хомде

Коли ви призначаєте заморожування, воно має властивість ягід, тож у цей момент у вашому конструкторі ви можете просто сказати, що глазур про те, що коли він буде встановлений, створює порожні (ненулі) ягоди? і щоразу, коли ягоди модифікуються, глазур перевіряє значення ????
Дуг Чемберлен

Дещо слабко пов’язані, деякі методи, які я тут вважаю кращими для проблеми "глибоких нулів", яку я намагався обійти. stackoverflow.com/questions/818642 / ...
AaronLS

Відповіді:


223

Ми розглянули можливість додавання нової операції "?". до мови, яка має потрібну вам семантику. (І це додано зараз; див. Нижче.) Тобто, ви б сказали

cake?.frosting?.berries?.loader

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

Це не зробило планку для C # 4. Можливо, для майбутньої гіпотетичної версії мови.

Оновлення (2014 року):?. оператор тепер планується в наступній версії компілятора Рослин. Зауважимо, що існує ще певна дискусія щодо точного синтаксичного та семантичного аналізу оператора.

Оновлення (липень 2015 р.): Visual Studio 2015 випущений та постачається разом із компілятором C #, який підтримує операторів?.?[] з нульовими умовами та .


10
Без крапки вона стає синтаксично неоднозначною з умовним (A? B: C) оператором. Ми намагаємось уникати лексичних конструкцій, які вимагають від нас «дивитись вперед» довільно далеко в потоці токенів. (Хоча, на жаль, такі конструкції в C # вже є; ми краще не додавати більше.)
Ерік Ліпперт

33
@Ian: ця проблема є надзвичайно поширеною. Це одне з найчастіших запитів, які ми отримуємо.
Ерік Ліпперт

7
@Ian: Я також вважаю за краще використовувати нульовий об'єкт, коли це можливо, але більшість людей не мають розкоші працювати з об'єктними моделями, які вони самі розробили. Багато існуючих моделей об'єктів використовують нулі, і тому ми маємо жити.
Ерік Ліпперт

12
@John: Цей запит на функцію ми отримуємо майже повністю від наших найдосвідченіших програмістів. MVP просять про це постійно . Але я розумію, що думки різняться; якщо ви хочете дати конструктивну пропозицію щодо мовного дизайну на додаток до вашої критики, я радий врахувати це.
Ерік Ліпперт

28
@lazyberezovsky: Я ніколи не розумів так званого "закону" Деметра; передусім, схоже, точніше називатись «Пропозиція Деметра». По-друге, результатом прийняття "лише одного члена доступу" до його логічного завершення є "Бог об'єкти", де кожен об'єкт зобов'язаний зробити все для кожного клієнта, а не вміти роздавати об'єкти, які вміють робити те, що клієнт хоче. Я віддаю перевагу прямо протилежному закону деметера: кожен об’єкт добре вирішує невелику кількість проблем, і одним із таких рішень може бути «ось ще один об’єкт, який краще вирішує вашу проблему»
Ерік Ліпперт,

27

Мене надихнуло це запитання, щоб спробувати дізнатися, як подібну глибоку перевірку нуля можна зробити за допомогою легшого / гарнішого синтаксису за допомогою дерев виразів. Хоча я згоден з відповідями, вказуючи, що це може бути поганим дизайном, якщо вам часто потрібно звертатися до екземплярів глибоко в ієрархії, я також думаю, що в деяких випадках, таких як представлення даних, це може бути дуже корисно.

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

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

Це поверне Ягоди, якщо жодна частина виразу не має значення. Якщо виникає 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. Розгляньте це як код підтвердження концепції, і майте на увазі, що за його застосування буде застосовано покарання за ефективність (що, мабуть, не має значення у багатьох випадках, але не використовуйте його у жорсткому циклі :-))


Мене вражають ваші навички лямбда :) Синтаксис, однак, здається трохи складнішим, ніж хотілося б, принаймні для сценарію if-statement
Хомда

Класно, але він працює як на 100 разів більше коду, ніж якщо .. &&. Це варто лише в тому випадку, якщо він все ще збирається до рівня if .. &&.
Монсьє

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);
}

Це ідея, з якої я походив від оператора зведення нуля в 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));
Роберт Гісекке

О, але ви маєте на увазі, що другий аргумент - це не заклик до Вугілля, яким, звичайно, має бути. Це просто зручна переробка. Селектор (x => x.berry) передається виклику вугілля всередині методу вугілля, який бере два аргументи.
Джон Лейдегрен

Назва coalescing або coalesce було взято з T-SQL, саме тут я вперше дійшов ідеї. IfNotNull означає, що щось відбувається, якщо не нульове, однак те, що це, не пояснюється викликом методу IfNotNull. Вугілля справді незвичне ім'я, але це в ділі дивний метод, про який варто звернути увагу.
Джон Лейдегрен

Найкращою буквальною назвою для цього було б щось на кшталт "ReturnIfNotNull" або "ReturnOrDefault"
Джон Лейдегрен

@flq +1 ... в нашому проекті він також називається IfNotNull :)
Marc Sigrist

16

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

Найчастіше це відбувається, коли потрібно замінити порожні об’єкти значеннями за замовчуванням. У цьому випадку слід розглянути можливість застосування шаблону об'єкта Null . Він виступає як резервний стан для реального об'єкта, надаючи значення за замовчуванням та методи "без дії".


ні, aim-c дозволяє надсилати повідомлення нульовим об'єктам і повертає відповідне значення за замовчуванням, якщо це необхідно. Ніяких проблем там немає.
Йоганнес Рудольф

2
Так. В тім-то й річ. В основному, ви будете імітувати поведінку ObjC за допомогою Null Object Pattern.
Мехрдад Афшарі

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) …), але це може бути оцінено приємніше з точки зору синтаксису.


2
Я не знаю, чому це було знято, я зробив внесок у рівновагу: відповідь правильна і вносить новий аспект (і чітко згадує про недоліки цього рішення ...)
MartinStettner

де "вищевказана відповідь Мехрдада Афшарі"?
Марсон Мао

1
@MarsonMao: Цю відповідь тим часом видалено. (Ви все ще можете прочитати його, якщо ваш показник SO достатньо високий.) Дякую за вказівку на мою помилку: я повинен посилатися на інші відповіді за допомогою гіперпосилання, не використовуючи слова типу "див. Вище" / "див. Нижче" (оскільки відповіді не відображаються у фіксованому порядку). Я оновив свою відповідь.
stakx - більше не вносять

5

Хоча відповідь Driis цікава, я думаю, що це занадто дороге виконання. Замість того, щоб компілювати багато делегатів, я вважаю за краще складати одну лямбда на шлях власності, кешувати її та повторно відкликати її на багато типів.

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

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

Однак ось досить пристойну альтернативу, яку я використовую: створити допоміжний метод Null-Safe-Chain. Я усвідомлюю, що це дуже схоже на відповідь @ Джона вище ( 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

Є проект Codeplex, який реалізує Можливо, або 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());

Реалізація є безладною, тому що вам потрібно змусити її працювати для типів значень, типів посилань та типів зведених значень. Ви можете знайти повну реалізацію в 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 замість нульової функції відображення):

cmd.Parameters.AddWithValue("CustomerContactEmail", GetPropValue("AccountingCustomerParty.Party.Contact.ElectronicMail.Value", eInvoiceType));

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;
    }

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
Навіщо турбуватися з a, 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); }
    }
}

І так, це, мабуть, не оптимальне рішення через наслідки спроб / лову, але це працює:>

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

var val = cake.GetValueOrDefault(x => x.frosting.berries.loader);

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);
        }
    }
}

-3

Мені подобається підхід, застосований Objective-C:

"Мова" Objective-C "використовує інший підхід до цієї проблеми і не викликає методів на нуль, а натомість повертає нуль для всіх таких викликів."

if (cake.frosting.berries != null) 
{
    var str = cake.frosting.berries...;
}

1
те, що робить інша мова (і ваша думка про неї), майже зовсім не має значення для того, щоб вона працювала в C #. Це нікому не допомагає вирішити свою проблему з C #
ADyson
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.