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!
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.
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), "+");
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);
}