Элегантный способ чтения дочернего свойства объекта
Скажем, вы пытаетесь прочитать это свойство
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);
Очевидно, они просто абстрагируют логику и делают код (немного) более читабельным.
Но есть ли лучший / более эффективный способ?
РЕДАКТИРОВАТЬ:
В моем конкретном случае объекты возвращаются из службы 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/
11 ответов
Лучшим способом было бы избежать нарушения закона Деметры.
var town = Staff.GetTown();
И в Staff
:
string GetTown()
{
HomeAddress.GetTown();
}
И в HomeAddress
:
string GetTown()
{
PostCode.GetTown();
}
И в PostCode
:
string GetTown()
{
Town.GetTownName();
}
Обновить:
Поскольку вы не можете контролировать это, вы можете использовать оценку короткого замыкания:
if(Staff != null
&& Staff.HomeAddress != null
&& Staff.HomeAddress.PostCode != null
&& Staff.HomeAddress.PostCode.Town != null)
{
var town = Staff.HomeAddress.Postcode.Town;
}
Я согласен с Одедом, что это нарушает закон Деметры.
Я был заинтригован вашим вопросом, поэтому я написал метод расширения 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;
}
var town = "DefaultCity";
if (Staff != null &&
Staff.HomeAddress != null &&
Staff.HomeAddress.Postcode != null &&
Staff.HomeAddress.Postcode.Town != null)
{
town = Staff.HomeAddress.Postcode.Town;
}
Здесь решение с использованием нулевых операторов объединения, которые я собрал для забавы (другие ответы лучше). Если вы ответите, кроме этого, мне придется выследить вас и убрать клавиатуру!:-)
В принципе, если какой-либо объект в 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, и мы, javascripters, знаем, что доступ к свойствам объекта может быть дорогим - поэтому мы склонны кэшировать чуть выше всего, что и выполняет код выше (каждое свойство просматривается только один раз). С компиляторами и оптимизаторами C# это, вероятно, не требуется (некоторое подтверждение этого было бы неплохо).
Я придумал то же решение, что и Ани некоторое время назад, подробности смотрите в этом блоге. Хотя элегантно, но очень неэффективно...
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);
Единственное, что мне не нравится в этом решении - это имя метода расширения, но его можно легко изменить...
В соответствии с инкапсуляцией, класс всегда обязан правильно проверять (т.е. проверять на ноль) свои поля (и свойства) перед их возвратом. Таким образом, каждый объект отвечает за свои поля, вы можете вернуть пустую строку или вызвать исключение и обработать его на один уровень вверх в цепочке. Попытка обойти это все равно что попытаться обойти инкапсуляцию.
Ответы @ Одеда и других по-прежнему верны в 2016 году, но в C# 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.");
}
}
Еще один ход:
Объявите вспомогательный метод
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;
}
Как часто вы ожидаете ноль? Если (и только если) это будет нечасто, я бы использовал
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
}
Вы также можете рассмотреть возможность использования Maybe
монада и имеющий метод расширения как ToMaybe()
это дает вам Just 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);
который действительно чистый или действительно уродливый в зависимости от вашей точки зрения
Не можете сейчас проверить, но разве это не сработает?
if (Staff??Staff.HomeAdress??Staff.HomeAddress.Postcode??Staff.HomeAddress.Postcode.Town != null)
{
var town = Staff.HomeAddress.Postcode.Town
}