读取对象子属性的优雅方式
本文关键字:方式 属性 取对象 读取 | 更新日期: 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
}