22

短いバージョン (TL;DR):

メンバー アクセス演算子の単なるチェーンである式があるとします。

Expression<Func<Tx, Tbaz>> e = x => x.foo.bar.baz;

この式は、それぞれが 1 つのメンバー アクセス操作で構成される部分式の合成と考えることができます。

Expression<Func<Tx, Tfoo>>   e1 = (Tx x) => x.foo;
Expression<Func<Tfoo, Tbar>> e2 = (Tfoo foo) => foo.bar;
Expression<Func<Tbar, Tbaz>> e3 = (Tbar bar) => bar.baz;

私がやりたいのはe、これらのコンポーネントのサブ式に分解して、個別に作業できるようにすることです。

さらに短いバージョン:

もし私がその表現を持っているならx => x.foo.bar、私はすでに別れ方を知っていますx => x.foo。他の部分式を引き出すにはどうすればよいfoo => foo.barですか?

私がこれをしている理由:

CoffeeScriptの存在アクセス演算子の?.ように、C#でメンバーアクセス演算子を「持ち上げる」ことをシミュレートしようとしています。Eric Lippert は、同様の演算子が C# で検討されていると述べていますが、それを実装する予算はありませんでした。

そのような演算子が C# に存在する場合、次のようなことができます。

value = target?.foo?.bar?.baz;

チェーンの一部target.foo.bar.bazが null であることが判明した場合、この全体が null と評価されるため、NullReferenceException が回避されます。

Liftこの種のことをシミュレートできる拡張メソッドが必要です。

value = target.Lift(x => x.foo.bar.baz); //returns target.foo.bar.baz or null

私が試したこと:

私はコンパイルできるものを持っていますが、それはある程度機能します。ただし、メンバーアクセス式の左側を保持する方法しか知らないため、不完全です。x => x.foo.bar.bazに変身できますがx => x.foo.bar、維持する方法がわかりませんbar => bar.baz

したがって、最終的には次のようなことになります(疑似コード):

return (x => x)(target) == null ? null
       : (x => x.foo)(target) == null ? null
       : (x => x.foo.bar)(target) == null ? null
       : (x => x.foo.bar.baz)(target);

これは、式の左端のステップが何度も評価されることを意味します。それらが POCO オブジェクトの単なるプロパティである場合は大したことではないかもしれませんが、それらをメソッド呼び出しに変えると、非効率性 (および潜在的な副作用) がより明白になります。

//still pseudocode
return (x => x())(target) == null ? null
       : (x => x().foo())(target) == null ? null
       : (x => x().foo().bar())(target) == null ? null
       : (x => x().foo().bar().baz())(target);

コード:

static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    //omitted: if target can be null && target == null, just return null

    var memberExpression = exp.Body as MemberExpression;
    if (memberExpression != null)
    {
        //if memberExpression is {x.foo.bar}, then innerExpression is {x.foo}
        var innerExpression = memberExpression.Expression;
        var innerLambda = Expression.Lambda<Func<T, object>>(
                              innerExpression, 
                              exp.Parameters
                          );  

        if (target.Lift(innerLambda) == null)
        {
            return null;
        }
        else
        {
            ////This is the part I'm stuck on. Possible pseudocode:
            //var member = memberExpression.Member;              
            //return GetValueOfMember(target.Lift(innerLambda), member);
        }
    }

    //For now, I'm stuck with this:
    return exp.Compile()(target);
}

これは、この回答に大まかに触発されました。


Lift メソッドの代替手段と、それらを使用できない理由:

Maybe モナド

value = x.ToMaybe()
         .Bind(y => y.foo)
         .Bind(f => f.bar)
         .Bind(b => b.baz)
         .Value;
長所:
  1. 関数型プログラミングで一般的な既存のパターンを使用
  2. 解除されたメンバー アクセス以外の用途があります
短所:
  1. 冗長すぎます。いくつかのメンバーをドリルダウンするたびに、関数呼び出しの大規模なチェーンは必要ありません。クエリ構文を実装SelectManyして使用したとしても、私見はより乱雑に見えますが、それ以下ではありません。
  2. 個々のコンポーネントとして手動で書き直す必要がx.foo.bar.bazあります。つまり、コンパイル時にそれらが何であるかを知る必要があります。のような変数から式を使用することはできませんresult = Lift(expr, obj);
  3. 私がやろうとしていることのために実際に設計されたわけではなく、完璧にフィットしているようにも感じません.

式ビジター

Ian Griffith の LiftMemberAccessToNull メソッドを、前述のように使用できる汎用拡張メソッドに変更しました。コードは長すぎてここに含めることはできませんが、興味のある方は Gist を投稿します。

長所:
  1. result = target.Lift(x => x.foo.bar.baz)構文に従います
  2. チェーンのすべてのステップが参照型または null 非許容値型を返す場合にうまく機能します
短所:
  1. チェーン内のいずれかのメンバーが null 許容値型である場合、それは窒息します。これにより、その有用性が実際に制限されます。Nullable<DateTime>メンバーのために機能する必要があります。

トライ/キャッチ

try 
{ 
    value = x.foo.bar.baz; 
}
catch (NullReferenceException ex) 
{ 
    value = null; 
}

これは最も明白な方法であり、より洗練された方法が見つからない場合に使用します。

長所:
  1. それは簡単です。
  2. コードの目的は明らかです。
  3. エッジケースについて心配する必要はありません。
短所:
  1. それは醜くて冗長です
  2. try/catch ブロックは重要なパフォーマンス ヒットです*
  3. ステートメント ブロックなので、LINQ の式ツリーを発行することはできません。
  4. 負けを認めた感じ

うそをつくつもりはありません。「負けを認めない」ことが、私が頑固な一番の理由です。私の本能は、これを行うエレガントな方法があるに違いないと言いますが、それを見つけるのは困難でした. 式の左側には簡単にアクセスできるのに、右側にはほとんど到達できないなんて信じられません。

ここには本当に 2 つの問題があるので、どちらかを解決するものは何でも受け入れます。

  • 両側を保持し、妥当なパフォーマンスを持ち、あらゆる型で機能する式分解
  • null 伝搬メンバー アクセス

アップデート:

null 伝播メンバー アクセスは、 C# 6.0に含まれる予定です 。ただし、式の分解に対する解決策が必要です。

4

1 に答える 1

8

単純なメンバー アクセス式のチェーンである場合は、簡単な解決策があります。

public static TResult Lift<T, TResult>(this T target, Expression<Func<T, TResult>> exp)
    where TResult : class
{
    return (TResult) GetValueOfExpression(target, exp.Body);
}

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else
    {
        throw new ArgumentException("The expression must contain only member access calls.", "exp");
    }
}

編集

メソッド呼び出しのサポートを追加する場合は、次の更新されたメソッドを使用します。

private static object GetValueOfExpression<T>(T target, Expression exp)
{
    if (exp == null)
    {
        return null;
    }
    else if (exp.NodeType == ExpressionType.Parameter)
    {
        return target;
    }
    else if (exp.NodeType == ExpressionType.Constant)
    {
        return ((ConstantExpression) exp).Value;
    }
    else if (exp.NodeType == ExpressionType.Lambda)
    {
        return exp;
    }
    else if (exp.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = (MemberExpression) exp;
        var parentValue = GetValueOfExpression(target, memberExpression.Expression);

        if (parentValue == null)
        {
            return null;
        }
        else
        {
            if (memberExpression.Member is PropertyInfo)
                return ((PropertyInfo) memberExpression.Member).GetValue(parentValue, null);
            else
                return ((FieldInfo) memberExpression.Member).GetValue(parentValue);
        }
    }
    else if (exp.NodeType == ExpressionType.Call)
    {
        var methodCallExpression = (MethodCallExpression) exp;
        var parentValue = GetValueOfExpression(target, methodCallExpression.Object);

        if (parentValue == null && !methodCallExpression.Method.IsStatic)
        {
            return null;
        }
        else
        {
            var arguments = methodCallExpression.Arguments.Select(a => GetValueOfExpression(target, a)).ToArray();

            // Required for comverting expression parameters to delegate calls
            var parameters = methodCallExpression.Method.GetParameters();
            for (int i = 0; i < parameters.Length; i++)
            {
                if (typeof(Delegate).IsAssignableFrom(parameters[i].ParameterType))
                {
                    arguments[i] = ((LambdaExpression) arguments[i]).Compile();
                }
            }

            if (arguments.Length > 0 && arguments[0] == null && methodCallExpression.Method.IsStatic &&
                methodCallExpression.Method.IsDefined(typeof(ExtensionAttribute), false)) // extension method
            {
                return null;
            }
            else
            {
                return methodCallExpression.Method.Invoke(parentValue, arguments);
            }
        }
    }
    else
    {
        throw new ArgumentException(
            string.Format("Expression type '{0}' is invalid for member invoking.", exp.NodeType));
    }
}
于 2012-06-22T09:51:52.107 に答える