阅读对象的子属性的优雅方式

时间:2011-04-21 13:16:49

标签: c# .net properties logic

假设您正在尝试阅读此属性

var town = Staff.HomeAddress.Postcode.Town;

沿链的某处可能存在null。 阅读城镇的最佳方式是什么?

我一直在试验几种扩展方法......

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 个答案:

答案 0 :(得分:19)

最好的方法是避免违反law of Demeter

var town = Staff.GetTown();

Staff

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

HomeAddress

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

PostCode

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

更新

由于您无法控制此问题,因此您可以使用short circuit evaluation

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

答案 1 :(得分:10)

我同意Oded的观点,这违反了得墨忒耳法。

我对你的问题很感兴趣,所以我写了一个穷人的“无安全评估”#34;带表达式树的扩展方法,只是为了好玩。这应该为您提供紧凑的语法来表达所需的语义。

请不要在生产代码中使用它。

用法:

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

答案 2 :(得分:9)

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

答案 3 :(得分:1)

根据封装,在返回它们之前,对它的字段(和属性)进行适当的验证(即空值检查)始终是一个类的义务。因此每个对象都负责其字段,您可以选择返回null,空字符串或引发异常并在链中处理它一级。试图解决这个问题就像尝试解决封装一样。

答案 4 :(得分:1)

这是一个使用空合并运算符的解决方案,我把它放在一起以获得乐趣(其他答案更好)。如果除了这个作为答案,我将不得不追捕你,呃,拿走你的键盘! : - )

基本上,如果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#的编译器和优化器,它可能没有必要这样做(对此有一些确认会很好)。

答案 5 :(得分:1)

前段时间我提出了与Ani's相同的解决方案,详见this blog post。优雅,但效率很低......

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

更好的解决方案恕我直言是this CodeProject article中建议的那个:

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

我唯一不喜欢这个解决方案的是扩展方法的名称,但它很容易改变......

答案 6 :(得分:1)

@ Oded&#39;和其他人&#39;答案在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.");
    }
}

答案 7 :(得分:0)

您多久会看到一次null?如果(并且仅当)它很少,我会使用

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
}

答案 8 :(得分:0)

另一个去:

声明一个辅助方法

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

答案 9 :(得分:0)

您还可以考虑使用Maybe monad并使用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);

我不会深入讨论实现细节(除非有人问),但代码看起来像这样:

{{1}}
根据你的观点,这是非常干净或非常丑陋

答案 10 :(得分:-1)

现在无法测试,但不会有这样的工作吗?

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