开发者

Elegant way of reading a child property of an object

Say you are trying to read this property

var town = Staff.HomeAddress.Postcode.Town;

Somewhere along the chain a null could exist. What would be the be开发者_如何学Gost way of reading Town?

I have been experimenting with a couple of extension methods...

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

or

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

Obviously these are just abstracting away the logic and making the code (a little) more readable.

But is there a better/ more efficient way?

EDIT:

In my particular case the objects are being returned from a WCF service and I have no control over the architecture of those objects.

EDIT 2:

There is also this method:

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

from this article http://qualityofdata.com/2011/01/27/nullsafe-dereference-operator-in-c/


The best way would be to avoid violating the law of Demeter.

var town = Staff.GetTown();

And in Staff:

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

And in HomeAddress:

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

And in PostCode:

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

Update:

Since you don't have control over this, you can use short circuit evaluation:

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


I agree with Oded that this violates the Law of Demeter.

I was intrigued by your question though, so I wrote up a a poor man's "Null-Safe Evaluate" extension-method with expression-trees, just for fun. This should give you compact syntax to express the desired semantics.

Please don't use this in production code.

Usage:

var town = Staff.NullSafeEvaluate(s => s.HomeAddress.Postcode.Town);

This will evaluate in succession:

Staff
Staff.HomeAddress
Staff.HomeAddress.Postcode
Staff.HomeAddress.Postcode.Town

(Caching and reusing the values of the intermediate expressions to produce the next one)

If it encounters a null reference, it returns the default value of the type of Town. Otherwise, it returns the value of the full expression.

(Not throughly tested, can be improved in terms of performance and doesn't support instance-methods. POC only.)

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


As per encapsulation, it is always the duty of a class to make proper validation (i.e. null-checks) for it's fields (and properties) before returning them. So each object is responsible for its fields, you can choose to return null, empty string, or raise an exception and handle it one level up in the chain. Trying to work around this is like trying to work around encapsulation.


Here a solution using null coalescing operators that I put together for fun (other answers are better). If you except this as the answer I'll have to hunt you down and uh, take your keyboard away! :-)

Basically, if any object in Staff is null its default will be used instead.

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

Disclaimer, I'm a javascript guy and we javascripters know that accessing an object's properties can get expensive - so we tend to cache just above everything, which is what the code above accomplishes (each property is only looked up once). With C#'s compilers and optimizers it probably isn't necessary to do it (some confirmation on this would be nice).


I came up with the same solution as Ani's some time ago, see this blog post for details. While elegant, it's very inefficient...

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

A better solution IMHO is the one suggested in this CodeProject article:

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

The only thing I don't like with this solution is the name of the extension method, but it can easily be changed...


@Oded's and others' answers still hold true in 2016 but c# 6 introduced the null-conditional operator which provides the elegance you are after.

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


How often do you expect a null? If (and only if) it will be infrequent, I would use

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
}


Another go:

Declare a helper method

bool HasNull(params object[] objects)
{
    foreach (object o in objects) { if (o == null) return true; }
    return false;
}

Then use it like this:

if (!HasNull(Staff, Staff.HomeAdress, Staff.HomeAddress.Postcode, Staff.HomeAddress.Postcode.Town))
{
    town = Staff.HomeAddress.Postcode.Town;
}


You could also consider using the Maybe monad and having an extension method like ToMaybe() that gives you a Just a if the object is not null, a Nothing if it is.

I won't go into the implementation details (unless someone asks) but the code would look like this:

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

which is really clean or really ugly depending on you point of view


Can't test right now, but wouldn't something like this work?

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

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜