【问题标题】:Elegant way of reading a child property of an object读取对象子属性的优雅方式
【发布时间】:2011-08-10 07:41:12
【问题描述】:

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

var town = Staff.HomeAddress.Postcode.Town;

在链的某处可能存在空值。 阅读 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/

【问题讨论】:

标签: c# .net properties logic


【解决方案1】:

最好的办法是避免违反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;
}

【讨论】:

  • @Oded:同意,但在这种情况下,我无法控制。
  • 最好的办法是把它改成 Staff.Town
  • @Dve 如果它超出您的控制范围,您可以将 Staff 封装在您的一个对象中。
  • @Dve - 我的朋友,您在问题中没有提及的微小细节。现在添加怎么样?
  • 您的 GetTown 链如何解决这个问题,除非您在其中放置一些空检查?
【解决方案2】:

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

不过,我对你的问题很感兴趣,所以我用表达式树写了一个穷人的“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;
    }

【讨论】:

  • 这是否比使用 TryGet 方法更有效?
  • 这可能不会为您在计算机科学课堂中的优雅/创造力赢得任何分数。但是,如果我“在野外”遇到这段代码,我会立即知道它在做什么以及为什么。有时简单/实用主义是最好的方法。
  • 是的。在这种情况下,Try/gets 会消耗更多
  • @mikemann:我同意这是可以立即理解的。但是staff.TryGet(x=> x.HomeAddress.Postcode.Town) 有那么糟糕吗?
  • @Dve - 它 更糟,因为您依赖于抛出异常(如果它被抛出,它更贵)。
【解决方案4】:

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

【讨论】:

    【解决方案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 人,我们 javascripters 知道访问对象的属性会很昂贵 - 所以我们倾向于缓存所有内容,这就是上面的代码完成的(每个属性只被查找一次)。使用 C# 的编译器和优化器可能没有必要这样做(对此进行一些确认会很好)。

    【讨论】:

    • 我会将 Town 设为空字符串。
    【解决方案6】:

    前段时间我想出了与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);
    

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

    【讨论】:

      【解决方案7】:

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

      【讨论】:

        【解决方案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 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);
            

            哪个是真正干净或非常丑陋,取决于您的观点

            【讨论】:

              【解决方案11】:

              现在无法测试,但这样的东西不可行吗?

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

              【讨论】:

              • Staff ?? Staff.HomeAddress 运算符两边的类型不同,所以这种用法不会编译。此外,如果Staff 为空,则?? 运算符将调用Staff.HomeAddress,从而导致空引用异常。
              • 昨天很晚才发的(当时是在泰国),后来才意识到所有的缺点。所以,不,它不会工作......
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-10-30
              • 2012-11-06
              • 1970-01-01
              • 2011-10-15
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多