2

Is this a bug in MiscUtils or am I missing something?

decimal a = (1M/30);
int b = 59;
Assert.AreEqual(a*b, Operator.MultiplyAlternative(a, b));
Assert.AreEqual(b*a, Operator.MultiplyAlternative(b, a));

Fails on the last line:

expected: <1.9666666666666666666666666647>
 but was: <0>

Update

As Petr pointed out there is some coercion going on in the CreateExpression method which is causing the problem. When using the compiler we don't see such issues because the parameters are lifted to the type with the highest precision, which also becomes the return type.

The second 'Assert' should have failed anyway, because if consistent with normal C# behaviour, I would expect it to lift the first parameter (b) to a decimal and perform the operation. Normally, if we wanted to store the result to a variable of a type with lower precision we would need to do an explicit cast. However, since the method we are calling has a return type that may be of lower precision and the caller has explicitly invoked the method that returns a lower precision result - it seems justifiable to automatically perform a potentially truncating cast as part of the operation. In other words the expected result from the second expression would be 1.

So, we can change CreateExpression to reflect that behaviour as follows:

if (castArgsToResultOnFailure && !(         // if we show retry                                                        
        typeof(TArg1) == typeof(TResult) &&  // and the args aren't
        typeof(TArg2) == typeof(TResult)))
{ // already "TValue, TValue, TValue"...
    var ltc = Type.GetTypeCode(lhs.Type);
    var rtc = Type.GetTypeCode(rhs.Type);
    // Use the higher precision element
    if (ltc > rtc)
    {
        // TArg1/TResult is higher precision than TArg2. Simply lift rhs
        var castRhs = Expression.Convert(rhs, lhs.Type);
        return
            Expression.Lambda<Func<TArg1, TArg2, TResult>>(body(lhs, castRhs), lhs, rhs).Compile();
    }
    // TArg2 is higher precision than TArg1/TResult. Lift lhs and Cast result
    var castLhs = Expression.Convert(lhs, rhs.Type);
    var castResult = Expression.Convert(body(castLhs, rhs), lhs.Type);
    return Expression.Lambda<Func<TArg1, TArg2, TResult>>(castResult, lhs, rhs).Compile();
}

The second assertion therefore needs to be rewritten:

Assert.AreEqual((int)(b*a), Operator.MultiplyAlternative(b, a));

Now both assertions succeed. As before, depending on the order of the parameters, different results will be returned, but now the second invocation produces a result that is logically correct.

4

1 回答 1

3

According to source code, method signature is as follows:

public static TArg1 MultiplyAlternative<TArg1, TArg2>(TArg1 value1, TArg2 value2)

and return type is same as first argument. Thus in second case it uses int as return type, and, most probably, converts second argument to that type too, so you have 59*0 which is zero.

Concerning your comment, this part of source code provide details:

/// <summary>
/// Create a function delegate representing a binary operation
/// </summary>
/// <param name="castArgsToResultOnFailure">
/// If no matching operation is possible, attempt to convert
/// TArg1 and TArg2 to TResult for a match? For example, there is no
/// "decimal operator /(decimal, int)", but by converting TArg2 (int) to
/// TResult (decimal) a match is found.
/// </param>
/// <typeparam name="TArg1">The first parameter type</typeparam>
/// <typeparam name="TArg2">The second parameter type</typeparam>
/// <typeparam name="TResult">The return type</typeparam>
/// <param name="body">Body factory</param>
/// <returns>Compiled function delegate</returns>
public static Func<TArg1, TArg2, TResult> CreateExpression<TArg1, TArg2, TResult>(
        Func<Expression, Expression, BinaryExpression> body, bool castArgsToResultOnFailure)
{
    ParameterExpression lhs = Expression.Parameter(typeof(TArg1), "lhs");
    ParameterExpression rhs = Expression.Parameter(typeof(TArg2), "rhs");
    try
    {
        try
        {
            return Expression.Lambda<Func<TArg1, TArg2, TResult>>(body(lhs, rhs), lhs, rhs).Compile();
        }
        catch (InvalidOperationException)
        {
            if (castArgsToResultOnFailure && !(         // if we show retry
                            typeof(TArg1) == typeof(TResult) &&  // and the args aren't
                            typeof(TArg2) == typeof(TResult)))
            { // already "TValue, TValue, TValue"...
                // convert both lhs and rhs to TResult (as appropriate)
                Expression castLhs = typeof(TArg1) == typeof(TResult) ?
                                (Expression)lhs :
                                (Expression)Expression.Convert(lhs, typeof(TResult));
                Expression castRhs = typeof(TArg2) == typeof(TResult) ?
                                (Expression)rhs :
                                (Expression)Expression.Convert(rhs, typeof(TResult));

                return Expression.Lambda<Func<TArg1, TArg2, TResult>>(
                        body(castLhs, castRhs), lhs, rhs).Compile();
            }
            else throw;
        }
    }
    catch (Exception ex)
    {
        string msg = ex.Message; // avoid capture of ex itself
        return delegate { throw new InvalidOperationException(msg); };
    }
}

you see, Expression.Lambda<Func<TArg1, TArg2, TResult>>(body(lhs, rhs), lhs, rhs).Compile(); is failing for any combination of TArg1 and TArg2 of int and decimal (it actually have same behaviour if you change decimal to double for example, so it's not only decimal affected), then catch block is trying to cast both arguments to TResult type, which, in turn, depends on parameter order. So you get all decimal cast in first case and all int cast in second.

It's beyond my knowledge unfortunatelly to answer why actually this lambda compile is failing and is it a bug or a language limitation.

于 2013-11-22T06:17:33.450 回答