Reputation: 125
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?
Upvotes: 1
Views: 565
Reputation: 111870
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.
Upvotes: 2
Reputation: 646
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.
Upvotes: 1