Expression trees - unnecessary conversion to int32

2019-04-04 16:48发布

问题:

Expression trees seem to build an unnecessary conversion when working with bytes and shorts, they convert both sides (in binary expressions for instance) to int32.

This is an issue in some Linq providers that I've seen, each has to peel this redundant layer to get to the original expression. (NHibernate doesn't remove this layer and creates an awful CAST in the SQL query).

// no conversion
Console.WriteLine((Expression<Func<int, int, bool>>) ((s, s1) => s == s1));
// converts to int32
Console.WriteLine((Expression<Func<short, short, bool>>) ((s, s1) => s == s1));
// converts to int32
Console.WriteLine((Expression<Func<byte, byte, bool>>) ((s, s1) => s == s1));

If you try to build an expression that makes this exact comparison (without the conversion), you'll succeed.

So the question is, what is the reason for this behavior?

EDIT .net 4.0 64bit, the same applies to 4.5 64bit

回答1:

That's really interesting; unfortunately, the rules of the expression-tree compiler are not formally specified - there is a brief "are elsewhere" in the specification, but : they aren't really.

If it is causing a problem, you could try to spot and remove it - something like below, which is 100% untested and use-at-own-risk, etc:

static void Main()
{
    Console.WriteLine(((Expression<Func<short, short, bool>>)((s, s1) => s == s1)).Unmunge());
    Console.WriteLine(((Expression<Func<byte, byte, bool>>)((s, s1) => s == s1)).Unmunge());  
}
static Expression<T> Unmunge<T>(this Expression<T> expression)
{
    return (Expression<T>)RedundantConversionVisitor.Default.Visit(expression);
}
class RedundantConversionVisitor : ExpressionVisitor
{
    private RedundantConversionVisitor() { }
    public static readonly RedundantConversionVisitor Default = new RedundantConversionVisitor();
    protected override Expression VisitBinary(BinaryExpression node)
    {
        if(node.Type == typeof(bool) && node.Method == null
            && node.Left.NodeType == ExpressionType.Convert && node.Right.NodeType == ExpressionType.Convert
            && node.Left.Type == node.Right.Type)
        {
            UnaryExpression lhs = (UnaryExpression)node.Left, rhs = (UnaryExpression)node.Right;
            if (lhs.Method == null && rhs.Method == null && lhs.Operand.Type == rhs.Operand.Type)
            {
                // work directly on the inner values
                return Expression.MakeBinary(node.NodeType, lhs.Operand, rhs.Operand, node.IsLiftedToNull, node.Method);
            }
        }
        return base.VisitBinary(node);
    }
}

Output before:

(s, s1) => (Convert(s) == Convert(s1))
(s, s1) => (Convert(s) == Convert(s1))

Output after:

(s, s1) => (s == s1)
(s, s1) => (s == s1)


回答2:

To answer your question:

Why Expression trees seem to build an unnecessary conversion when working with bytes and shorts... So the question is, what is the reason for this behavior?

The answer is hidden in the fact, that C# types short, ushort, byte and sbyte lack the arithmetic, comparison... operators:

Extract: 4.1.5 Integral types

For the binary +, –, *, /, %, &, ^, |, ==, !=, >, <, >=, and <= operators, the operands are converted to type T, where T is the first of int, uint, long, and ulong that can fully represent all possible values of both operands. The operation is then performed using the precision of type T, and the type of the result is T (or bool for the relational operators). It is not permitted for one operand to be of type long and the other to be of type ulong with the binary operators.

The 7.9.1 Integer comparison operators describes available operators and their operands

bool operator ==(int x, int y);
bool operator ==(uint x, uint y);
bool operator ==(long x, long y);
bool operator ==(ulong x, ulong y);
... // other operators, only for int, uint, long, ulong

The conversion is done for you by Compiler (the reason why you succeed to build that without explicit conversion)

Because there are no operators working with short... the conversion must be applied. And of course, it later depends on the LINQ provider, how to convert such "expression" into SQL.