C#: Find out result type of arithmetic operations

2019-05-31 09:12发布

问题:

I'm trying to come up with a function to determine the result type of arithmetic operations, say for the case of addition:

Type TypeOfAddition(Type leftType, Type rightType)
{
  // ???
}

Type TypeOfMultiplication(Type leftType, Type rightType)
{
  // ???
}

// ... same for subtraction and division

These desired result of these functions is probably clear; Essentially, my goal is do to the same (at runtime) that Visual Studio does when inferring types to "var"-type variables when doing arithmetic operations.

For example,

public class MyClass
{
    public static string operator +(MyClass left, double right)
    {
        // ...
    }
}

TypeOfAddition(typeof(int), typeof(double)); // Should return typeof(double)
TypeOfAddition(typeof(string), typeof(int)); // Should return typeof(string)
TypeOfAddition(typeof(MyClass), typeof(double));  // Should return typeof(string)

My base idea was an implementation like, conceptually

Type TypeOfAddition(Type leftType, Type rightType)
{
  return leftType.GetMethods().Single(x =>
    x.Name == "op_Addition" &&
    x.GetParamters().Count == 2 &&
    x.GetParameters().Last().ParameterType == rightType);
}

but

A) This won't work for base types like int, double etc., which don't seem to explicitly define operator overloads, and

B) The above linq clause won't catch all cases yet (e.g. inheritance)

I could hard-code the base types and try to come up with a smart solution for B) as well, but that seems relatively .. unelegant.

Is there any smarter / easier / good solution to solving this? Mind you, I only want to get the theoretical type of the result of such an operation, without actually executing an arithmetic operation explicitly.

Thanks!

回答1:

It certainly isn't pretty, and definitely isn't fast, but it appears to work with the basic tests I've ran it through.

Note that you'll need to have Microsoft.CSharp.dll referenced.

Type TypeOfAddition<TLeft, TRight>()
{
    object GetDefault<T>()
    {
        if (typeof(T).IsValueType)
        {
            return default(T);
        }

        if (typeof(T) == typeof(string))
        {
            return string.Empty;
        }

        return (T)FormatterServices.GetUninitializedObject(typeof(T));
    }

    var binder = Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation(
        CSharpBinderFlags.None,
        ExpressionType.Add,
        null,
        new CSharpArgumentInfo[] {
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null),
            CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null)
        }
    );

    var left = Expression.Parameter(typeof(TLeft));
    var right = Expression.Parameter(typeof(TRight));

    var func = Expression.Lambda(
        Expression.Dynamic(binder, typeof(object), left, right),
        new[] { left, right }
    ).Compile();

    return func
        .DynamicInvoke(GetDefault<TLeft>(), GetDefault<TRight>())
        ?.GetType() ?? typeof(object);
}

Example outputs:

public class MyClass
{
    public static string operator +(MyClass left, double right)
    {
        return "";
    }
}

TypeOfAddition<string, int>().Dump();     // System.String
TypeOfAddition<int, double>().Dump();     // System.Double
TypeOfAddition<float, double>().Dump();   // System.Double
TypeOfAddition<MyClass, double>().Dump(); // System.String

This uses what Jeroen mentioned in the comments (RuntimeBinder) to create a addition binder. It then builds a dynamic expression tree to add the default values of both TLeft and TRight. I had to add a small function called GetDefault to resolve string to an empty string, since I'm assuming you want to see string when trying to add "" + 0 instead of null. If you do want to see nulls, just replace the GetDefault calls with default(TLeft) and default(TRight).

It doesn't call constructors (due to using GetUninitializedObject) contains a special case for strings.

There are likely many possible improvements, and I am all ears.



回答2:

Using Roslyn, I now came up with the following. So far from what I tested, it seems to work fine - let me know what you think.

Downsides (no major issue in my case though) from what I see are

  • Somewhat slow, at least on first call
  • Must explicitly reference assemblies. Not sure if this triggers these to be re-loaded into whatever AppDomain/context Roslyn scripts use, if so, might slow this down a bit further for many/large assemblies
  • Obviously need to use/deploy a lot of Roslyn assemblies with my app just for this

    static async Task<Type> GetOperationResultTypeAsync(Type left, Type right, string operatorSymbol)
    {
        // Reference all assemblies that are loaded in the current AppDomain (plugins?)
        var options = ScriptOptions.Default.AddReferences(AppDomain.CurrentDomain.GetAssemblies());
        var script = CSharpScript.Create($"var instance = default({left.FullName}) {operatorSymbol} default({right.FullName});", options: options);
    
        var compilation = script.GetCompilation();
        var syntaxTree = compilation.SyntaxTrees.Single();
        var semanticModel = compilation.GetSemanticModel(syntaxTree);
    
        var variableDeclaration = (await syntaxTree.GetRootAsync())
            .DescendantNodes()
            .OfType<VariableDeclarationSyntax>()
            .Single();
    
        var symbolInfo = semanticModel.GetSymbolInfo(variableDeclaration.Type);
        var typeSymbol = (ITypeSymbol)symbolInfo.Symbol; // will be null on error (eg operation not possible/defined/allowed)
    
        if (typeSymbol == null)
            return null;
    
        var symbolDisplayFormat = new SymbolDisplayFormat(typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces);
        string fullyQualifiedName = typeSymbol.ToDisplayString(symbolDisplayFormat);
    
        Type type = Type.GetType(fullyQualifiedName, throwOnError: true);
        return type;
    }
    

with usage just being

Type t1 = await GetOperationResultTypeAsync(typeof(MyClass), typeof(double), "+");
Type t2 = await GetOperationResultTypeAsync(typeof(int), typeof(int), "+");
Type t3 = await GetOperationResultTypeAsync(typeof(int), typeof(double), "+");


回答3:

Maybe you could try Generics in your methods. Something like

Type TypeOfAddition<T, T2>(T leftNum, T2 rightNum){
    var result = leftNum + rightNum;
    return typeof(result);
}

Type TypeOfMultiplication<T, T2>(T leftNum, T2 rightNum){
    var result = leftNum * rightNum;
    return typeof(result);
}


标签: c# reflection