简体   繁体   中英

Expression trees and lazy evaluation in C#

I have this little bit of code where I take a ParameterExpression array of strings and convert a particular index to a target type. I do this either by calling Parse (if the type is primitive) or by attempting a raw conversion (hopefully to a string or implicit string).

static Expression ParseOrConvert(ParameterExpression strArray, ConstantExpression index, ParameterInfo paramInfo)
{
    Type paramType = paramInfo.ParameterType;

    Expression paramValue = Expression.ArrayIndex(strArray, index);

    if (paramType.IsPrimitive) {
        MethodInfo parseMethod = paramType.GetMethod("Parse", new[] { typeof(string) });
        // Fetch Int32.Parse(), etc.

        // Parse paramValue (a string) to target type
        paramValue = Expression.Call(parseMethod, paramValue);
    }
    else {
        // Else attempt a raw conversion
        paramValue = Expression.Convert(paramValue, paramType);
    }

    return paramValue;
}

This works, but I'm trying to rewrite the conditional as such.

paramValue = Expression.Condition(
    Expression.Constant(paramType.IsPrimitive),
    Expression.Call(parseMethod, paramValue),
    Expression.Convert(paramValue, paramType)
);

This always results in System.InvalidOperationException , presumably because it attempts both conversions. I find the second style more intuitive to write in so this is unfortunate.

Can I write this in a way that defers evaluation to when the values are actually needed?

Often debugging is like journalism... In journalism there are the five Ws : who , what , where , when , why (plus how ).. and in programming it is similar. who throws the exception (that is the what )? Let's make the code more easy to debug:

static Expression ParseOrConvert(ParameterExpression strArray, ConstantExpression index, ParameterInfo paramInfo)
{
    Type paramType = paramInfo.ParameterType;

    Expression paramValue = Expression.ArrayIndex(strArray, index);
    MethodInfo parseMethod = paramType.GetMethod("Parse", new[] { typeof(string) });

    var isPrimitive = Expression.Constant(paramType.IsPrimitive);
    var call = Expression.Call(parseMethod, paramValue);
    var convert = Expression.Convert(paramValue, paramType);

    var paramValue2 = Expression.Condition(
        isPrimitive,
        call,
        convert
    );

    return paramValue2;
}

And then call it like:

public static void MyMethod(int par1)
{
}

and then

ParameterExpression strArray = Expression.Parameter(typeof(string[]));

// paramType int
var paramInfo = typeof(Program).GetMethod("MyMethod").GetParameters()[0];

var result = ParseOrConvert(strArray, Expression.Constant(0), paramInfo);

Now... who throws the exception? Expression.Convert(paramValue, paramType) throws an exception... And why ? Because you are trying to do a:

string paramValue = ...;
convert = (int)paramValue;

That is surely illegal! Even "dead" code (code that can't be reached) must be "compilable" in .NET (in its IL language). So your error is trying to introduce some illegal dead code in your expression, that would be:

string paramValue = ...;
isPrimitive = true ? int.Parse(paramValue) : (int)paramValue;

This wouldn't compile in C# and probably can't even be written in IL code. And the Expression classes throw on it.

Expressions represent code as data, the true and false branches are not being evaluated here; it is not "attempting both conversions". Instead, it is trying to build an Expression Tree that represents each conversion. The condition however, being a Constant , is being baked into the conditional eagerly based on the type.

You are building an expression with the following structure:

var result = true // <`true` or `false` based on the type T> 
    ? T.Parse(val)
    : (T) val;

When T is int (and thus the "Test" is the constant true ) this does not compile because there is no valid cast from string to int , even though at runtime it would always evaluate/execute int.Parse(val) .

When T is Foo , this would compile when Foo has both a static Parse(string val) method, and an explicit cast operator

public class Foo
{
    public static Foo Parse(string fooStr)
    {
        return default(Foo);
    }

    public static explicit operator Foo(string fooStr)
    {
        return default(Foo);
    }
}

Even though it would only ever execute the explicit cast operator because Foo is not primitive.

Your original code actually already builds an Expression that will use the "correct" conversion strategy based on T without trying to compile/evaluate the other one. If this isn't working for you already, I suspect it's because the types involved don't have an explicit cast defined from string .

As an aside, I would discourage reusing paramValue as both the original (unconverted) and converted value, it makes debugging much harder than it needs to be, among other things.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM