Подтвердить что ты не робот

Элегантный способ чтения дочернего свойства объекта

Предположим, вы пытаетесь прочитать это свойство

var town = Staff.HomeAddress.Postcode.Town;

Где-то вдоль цепи может существовать нуль. Каким будет лучший способ чтения города?

Я экспериментировал с несколькими методами расширения...

public static T2 IfNotNull<T1, T2>(this T1 t, Func<T1, T2> fn) where T1 : class
{
    return t != null ? fn(t) : default(T2);
}

var town = staff.HomeAddress.IfNotNull(x => x.Postcode.IfNotNull(y=> y.Town));

или

public static T2 TryGet<T1, T2>(this T1 t, Func<T1, T2> fn) where T1 : class
{
if (t != null)
{
    try
    {
        return fn(t);
    }
    catch{ }
}
return default(T2);
}

var town = staff.TryGet(x=> x.HomeAddress.Postcode.Town);

Очевидно, что это просто абстрагирует логику и делает код (немного) более удобочитаемым.

Но есть ли лучший/более эффективный способ?

EDIT:

В моем конкретном случае объекты возвращаются из службы WCF, и я не контролирую архитектуру этих объектов.

ИЗМЕНИТЬ 2:

Существует также этот метод:

public static class Nullify
{
    public static TR Get<TF, TR>(TF t, Func<TF, TR> f) where TF : class
    {
        return t != null ? f(t) : default(TR);
    }

    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 as for the pseudocode
    ///     var r = Pharmacy?.GuildMembership?.State?.Name
    /// can be written as
    ///     var r = Nullify( Pharmacy, p => p.GuildMembership, g => g.State, s => s.Name );
    /// </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);
    }
}

из этой статьи http://qualityofdata.com/2011/01/27/nullsafe-dereference-operator-in-c/

4b9b3361

Ответ 1

Лучшим способом было бы избежать нарушения закона Деметры .

var town = Staff.GetTown();

И в Staff:

string GetTown()
{
    HomeAddress.GetTown();
}

И в HomeAddress:

string GetTown()
{
    PostCode.GetTown();
}

И в PostCode:

string GetTown()
{
    Town.GetTownName();
}

Update:

Поскольку у вас нет контроля над этим, вы можете использовать оценку короткого замыкания:

if(Staff != null 
   && Staff.HomeAddress != null
   && Staff.HomeAddress.PostCode != null
   && Staff.HomeAddress.PostCode.Town != null)
{
    var town = Staff.HomeAddress.Postcode.Town;
}

Ответ 2

Я согласен с Одедом в том, что это нарушает Закон Деметры.

Я был заинтригован вашим вопросом, хотя, поэтому я написал метод расширения "Null-Safe Evaluate" для бедных людей с деревьями выражений, просто для удовольствия. Это должно дать вам компактный синтаксис для выражения желаемой семантики.

Пожалуйста, не используйте это в производственном коде.

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

var town = Staff.NullSafeEvaluate(s => s.HomeAddress.Postcode.Town);

Это будет оцениваться последовательно:

Staff
Staff.HomeAddress
Staff.HomeAddress.Postcode
Staff.HomeAddress.Postcode.Town

(Кэширование и повторное использование значений промежуточных выражений для создания следующего)

Если он встречает ссылку null, он возвращает значение по умолчанию типа Town. В противном случае он возвращает значение полного выражения.

(Не проверенный, может быть улучшен с точки зрения производительности и не поддерживает экземпляры-методы. Только POC.)

public static TOutput NullSafeEvaluate<TInput, TOutput>
        (this TInput input, Expression<Func<TInput, TOutput>> selector)
{
    if (selector == null)
        throw new ArgumentNullException("selector");

    if (input == null)
        return default(TOutput);

    return EvaluateIterativelyOrDefault<TOutput>
            (input, GetSubExpressions(selector));
}

private static T EvaluateIterativelyOrDefault<T>
        (object rootObject, IEnumerable<MemberExpression> expressions)
{
    object currentObject = rootObject;

    foreach (var sourceMemEx in expressions)
    {
        // Produce next "nested" member-expression. 
        // Reuse the value of the last expression rather than 
        // re-evaluating from scratch.
        var currentEx = Expression.MakeMemberAccess
                      (Expression.Constant(currentObject), sourceMemEx.Member);


        // Evaluate expression.
        var method = Expression.Lambda(currentEx).Compile();
        currentObject = method.DynamicInvoke();

        // Expression evaluates to null, return default.
        if (currentObject == null)
            return default(T);
    }

    // All ok.
    return (T)currentObject;
}

private static IEnumerable<MemberExpression> GetSubExpressions<TInput, TOutput>
        (Expression<Func<TInput, TOutput>> selector)
{
    var stack = new Stack<MemberExpression>();

    var parameter = selector.Parameters.Single();
    var currentSubEx = selector.Body;

    // Iterate through the nested expressions, "reversing" their order.
    // Stop when we reach the "root", which must be the sole parameter.
    while (currentSubEx != parameter)
    {
        var memEx = currentSubEx as MemberExpression;

        if (memEx != null)
        {
            // Valid member-expression, push. 
            stack.Push(memEx);
            currentSubEx = memEx.Expression;
        }

        // It isn't a member-expression, it must be the parameter.
        else if (currentSubEx != parameter)
        {

            // No, it isn't. Throw, don't support arbitrary expressions.
            throw new ArgumentException
                        ("Expression not of the expected form.", "selector");
        }
    }

    return stack;
}

Ответ 3

    var town = "DefaultCity";
    if (Staff != null &&
        Staff.HomeAddress != null &&
        Staff.HomeAddress.Postcode != null &&
        Staff.HomeAddress.Postcode.Town != null)
    {
        town = Staff.HomeAddress.Postcode.Town;
    }

Ответ 4

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

Ответ 5

Здесь решение, использующее нулевые коалесцирующие операторы, которые я собрал для удовольствия (другие ответы лучше). Если вы не считаете это ответом, мне нужно будет выследить вас, а вы отпустите свою клавиатуру!: -)

В принципе, если какой-либо объект в Staff null, вместо него будет использоваться его значение по умолчанию.

// define a defaultModel
var defaultModel = new { HomeAddress = new { PostCode = new { Town = "Default Town" } } };
// null coalesce through the chain setting defaults along the way.
var town = (((Staff ?? defaultModel)
                .HomeAddress  ?? defaultModel.HomeAddress)
                    .PostCode ?? defaultModel.HomeAddress.PostCode)
                        .Town ?? defaultModel.HomeAddress.PostCode.Town;

Отказ от ответственности, я парень из javascript, и мы, javascript, знаем, что доступ к свойствам объекта может стать дорогостоящим, поэтому мы склонны кэшировать чуть выше всего, что делает вышеописанный код (каждое свойство просматривается только один раз). С С# компиляторами и оптимизаторами это, вероятно, не нужно делать (некоторые подтверждения на это было бы хорошо).

Ответ 6

Я придумал то же самое решение, что Ani's некоторое время назад, см. этот пост в блоге для получения более подробной информации. Хотя элегантный, он очень неэффективен...

var town = Staff.NullSafeEval(s => s.HomeAddress.Postcode.Town, "(N/A)");

Лучшее решение IMHO - это предложение, предложенное в этой статье CodeProject:

string town = Staff.With(s => s.HomeAddress)
                   .With(a => a.Postcode)
                   .With(p => p.Town);

Единственное, что мне не нравится в этом решении, - это имя метода расширения, но его можно легко изменить...

Ответ 7

@Oded и другие ответы остаются в силе в 2016 году, но С# 6 представила оператор с нулевым условием, который обеспечивает элегантность, которой вы пользуетесь.

using System;

public class Program
{
    public class C {
        public C ( string town ) {Town = town;}
        public string Town { get; private set;}
    }
    public class B {
        public B( C c ) {C = c; }
        public C C {get; private set; }
    }
    public class A {
        public A( B b ) {B = b; }
        public B B {get; private set; }
    }
    public static void Main()
    {
        var a = new A(null);
        Console.WriteLine( a?.B?.C?.Town ?? "Town is null.");
    }
}

Ответ 8

Как часто вы ожидаете нуль? Если (и только если) это будет нечасто, я бы использовал

try
{
    var town = staff.HomeAddress.Postcode.Town;
    // stuff to do if we could get the town
}
catch (NullReferenceException)
{
    // stuff to do if there is a null along the way
}

Ответ 9

Еще один вариант:

Объявить вспомогательный метод

bool HasNull(params object[] objects)
{
    foreach (object o in objects) { if (o == null) return true; }
    return false;
}

Затем используйте его следующим образом:

if (!HasNull(Staff, Staff.HomeAdress, Staff.HomeAddress.Postcode, Staff.HomeAddress.Postcode.Town))
{
    town = Staff.HomeAddress.Postcode.Town;
}

Ответ 10

Вы также можете рассмотреть использование монады Maybe и иметь метод расширения, например ToMaybe(), который дает вам Just a, если объект не является нулевым, a Nothing, если он есть.

Я не буду вдаваться в детали реализации (если кто-то не спрашивает), но код будет выглядеть так:

var maybeTown = from s in staff.ToMaybe()
                from h in s.HomeAddress.ToMaybe()
                from p in h.Postcode.ToMaybe()
                from t in p.Town.ToMaybe()
                select t;
var town = maybeTown.OrElse(null);

который действительно чист или действительно уродлив в зависимости от вашей точки зрения

Ответ 11

Не удается проверить прямо сейчас, но не будет ли что-то вроде этой работы?

if (Staff??Staff.HomeAdress??Staff.HomeAddress.Postcode??Staff.HomeAddress.Postcode.Town != null)
{
    var town = Staff.HomeAddress.Postcode.Town
}