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

本文关键字:方式 属性 取对象 读取 | 更新日期: 2023-09-27 17:52:48

如果您正在尝试读取此属性

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/

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

最好的办法是避免违反得墨忒耳定律。

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

我同意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;
}
    var town = "DefaultCity";
    if (Staff != null &&
        Staff.HomeAddress != null &&
        Staff.HomeAddress.Postcode != null &&
        Staff.HomeAddress.Postcode.Town != null)
    {
        town = Staff.HomeAddress.Postcode.Town;
    }

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

这里有一个使用空合并运算符的解决方案,我把它放在一起是为了好玩(其他答案更好)。如果你把这个当成答案,我就得找到你,把你的键盘拿走!: -)

基本上,如果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知道访问对象的属性可能会很昂贵-所以我们倾向于在所有东西之上缓存,这就是上面的代码所完成的(每个属性只查找一次)。对于c#的编译器和优化器,可能没有必要这样做(对此进行一些确认会很好)。

我在前段时间提出了与Ani相同的解决方案,详见这篇博客文章。虽然很优雅,但效率很低…

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

一个更好的解决方案是这篇CodeProject文章中建议的:

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

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

@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.");
    }
}

你多久会看到一个空值?当(且仅当)不经常出现时,我会使用

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
}

再试一次:

声明一个helper方法

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

您还可以考虑使用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
}