I'm here all I indulge with IL , so my decision is directly related to it)
So. Let's start with the following:
Let's write this code:
int a = 2; int b = 3; int c = a + b;
After reviewing the IL code created for this chain of expressions, we will see something like this:
ldc.i4.2 stloc a ldc.i4.3 stloc b ldloc a ldloc b add stloc c
(The code is approximate, so of course it will not. It is presented in this form for the sake of clarity)
What is responsible for adding two numbers of type System.Int32 ?
Correct: add instruction!
Rewrite the code:
double a = 2; double b = 3; double c = a + b;
Now IL will be:
ldc.r8 2 stloc a ldc.r8 3 stloc b ldloc a ldloc b add stloc c
What changed? Only the instruction l oa dc onstant, the addition instruction remained in its rightful place)
I will not continue, you already understand what I am getting at)
At the IL level , the same add instruction calmly handles the addition of instances of types sbyte , byte , short , ushort , int , uint , long , ulong , float , double )
But this is exactly what we need!
(By the way, this is also true for sub , mul , div , rem instructions. You can find a detailed list of IL instructions with a description here )
So, if you can painlessly add a .il file to a project (for example, using the ILSupport + extension, I am also in the process of writing this extension lazily ), then the same addition method can be described as follows:
.method private static !!T _Add<T>(!!T, !!T) cil managed { .maxstack 2 ldarg.0 // Кладем на стек нулевой аргумент ldarg.1 // Кладем на стек первый аргумент add // Складываем их ret // Возвращаем результат }
In the main file we describe something like this:
// Доступные для обработки типы private static HashSet<Type> AvailableTypes { get; } = new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double) }; // Метод сокроем от глаз посторонних, // так как сюда можно засунуть абсолютно что угодно, // а мы сего не хотим [MethodImpl(MethodImplOptions.ForwardRef)] private static extern T _Add<T>(TA, TB); // Обертка над нашим методом, которая проверяет, // допустим ли тип переданных объектов // На деле можно проверку и в основном методе реализовать, // но так нагляднее) public static T Add<T>(TA, TB) where T : struct { if (!AvailableTypes.Contains(typeof(T))) throw new TypeAccessException("Unsupported type!"); return _Add(A, B); }
Time to test!)
sbyte @sbyte = Add((sbyte)122, (sbyte)5); // 127 byte @byte = Add((byte)127, (byte)128); // 255 short @short = Add((short)16384, (short)16383); // 32767 ushort @ushort = Add((ushort)32767, (ushort)32768); // 65535 int @int = Add(1073741823, 1073741824); // 2147483647 uint @uint = Add(2147483647u, 2147483648u); // 4294967295 long @long = Add(4611686018427387903L, 4611686018427387904L); // 9223372036854775807 ulong @ulong = Add(9223372036854775807ul, 9223372036854775808ul); // 18446744073709551615 float @float = Add((float)Math.PI, 2f); // 5.141593 double @double = Add(Math.PI, 2); // 5.1415926535897931
As you can see, everything works great! One generic method accepts any numeric type as input and returns the correct answer!
If, due to some circumstances, you are limited only by the C# code, I can offer you a solution using MethodBuilder
Then we remove the _Add method, and in its place we put the following construction:
private static MethodInfo _Add { get { if (__add == null) { // Создаем билдеры нужных нам объектов AssemblyName asmName = new AssemblyName("asm"); AssemblyBuilder asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndCollect); ModuleBuilder moduleBuilder = asmBuilder.DefineDynamicModule(asmName.Name, $"{asmName.Name}.dll"); TypeBuilder typeBuilder = moduleBuilder.DefineType("tp", TypeAttributes.Public); // Начинаем строить метод MethodBuilder methodBuilder = typeBuilder.DefineMethod("_Add", MethodAttributes.Public | MethodAttributes.Static); // Добавляем generic-параметр GenericTypeParameterBuilder T = methodBuilder.DefineGenericParameters("T")[0]; // Указываем, что у нас 2 входных аргумента имеют generic-тип, // а также и выходное значение methodBuilder.SetParameters(T, T); methodBuilder.SetReturnType(T); // Генерируем IL, аналогичный примеру выше ILGenerator il = methodBuilder.GetILGenerator(); il.Emit(OpCodes.Ldarg_0); il.Emit(OpCodes.Ldarg_1); il.Emit(OpCodes.Add); il.Emit(OpCodes.Ret); // Задаем значение локальному полю __add = typeBuilder.CreateType().GetMethod("_Add", BindingFlags.Static | BindingFlags.Public); } return __add; } } private static MethodInfo __add;
And a bit of tinkering with the wrapper method:
public static T Add<T>(TA, TB) where T : struct { if (!AvailableTypes.Contains(typeof(T))) throw new TypeAccessException("Unsupported type!"); return (T)_Add.MakeGenericMethod(typeof(T)).Invoke(null, new object[] { A, B }); }
The rest of the code remained the same and did not lose in working capacity.
The only difference is that in the zero case we obtained the necessary method at the compilation stage, and here - during the program execution)
I hope my solution seemed to you at least a little, but interesting, and also brought some new ideas for the realization of your thoughts!)
Numerics.Vectors. - Alexander Petrov