1. One problem with C# generics is the lack of member constraints. In other words, this is impossible:
    public static T Add<T>(T left, T right)
    {
        return left + right;
    }
    
    People have come up with a large number of solutions, but they all have their own problems, ranging from messy looking syntax to less-than-optimal performance (particularly due to virtual function calls). The solution I propose is a somewhat hacky one (yes that's a word) which maintains both good performance and clean looking code: write the function in IL.

    The code for the add method boils down to something like this:
    ldarg.0
    ldarg.1
    add
    ret
    
    For those not familiar with IL, the above code adds the first to arguments of the method, then returns the result.

    I found that this actually works with generic types. No funny errors, no nothing. Of course, I didn't actually compile any IL source code - I wrote a separate program to generate the assembly. The source code for it is this:
    namespace Generator
    {
        using System;
        using System.Linq;
        using System.Reflection;
        using System.Reflection.Emit;
    
        public static class Program
        {
            public static void Main()
            {
                var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("Operations"), AssemblyBuilderAccess.Save);
                var module = assemblyBuilder.DefineDynamicModule("Operations", "Operations.dll");
                var type = module.DefineType("Operator", TypeAttributes.Public | TypeAttributes.Sealed);
    
                type.DefineBinaryOperatorMethod("Add", OpCodes.Add);
                type.DefineBinaryOperatorMethod("Subtract", OpCodes.Sub);
                type.DefineBinaryOperatorMethod("Multiply", OpCodes.Mul);
                type.DefineBinaryOperatorMethod("Divide", OpCodes.Div);
                type.DefineBinaryOperatorMethod("Remainder", OpCodes.Rem);
                type.CreateType();
    
                assemblyBuilder.Save("Operations.dll");
            }
    
            private static MethodBuilder DefineBinaryOperatorMethod(this TypeBuilder type, string name, OpCode operation)
            {
                var method = type.DefineMethod(name, MethodAttributes.Public | MethodAttributes.Static);
                var genericParameter = method.DefineGenericParameters("T").First();
                genericParameter.SetBaseTypeConstraint(typeof(ValueType));
                
                method.SetReturnType(genericParameter);
                method.SetParameters(genericParameter, genericParameter);
                method.DefineParameter(1, ParameterAttributes.None, "left");
                method.DefineParameter(2, ParameterAttributes.None, "right");
    
                var ilGenerator = method.GetILGenerator();
                var continueLabel = ilGenerator.DefineLabel();
    
                ilGenerator.Emit(OpCodes.Ldtoken, genericParameter);
                ilGenerator.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle"), null);
                ilGenerator.EmitCall(OpCodes.Callvirt, typeof(Type).GetProperty("IsPrimitive").GetGetMethod(), null);
                ilGenerator.Emit(OpCodes.Brtrue_S, continueLabel);
                ilGenerator.Emit(OpCodes.Ldstr, "The specified type is not supported by this operation.");
                ilGenerator.Emit(OpCodes.Newobj, typeof(ArgumentException).GetConstructor(new[] { typeof(string) }));
                ilGenerator.Emit(OpCodes.Throw);
                ilGenerator.MarkLabel(continueLabel);
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldarg_1);
                ilGenerator.Emit(operation);
                ilGenerator.Emit(OpCodes.Ret);
    
                return method;
            }
        }
    }
    
    In case your wondering what the ldtoken/call/callvirt/brtrue.s/ldstr/newobj/throw stuff is doing there, that just checks to make sure the type argument is a primitive type (yes this only works on primitive types), and throws an ArgumentException otherwise. Before adding that, .NET threw some pretty nasty exceptions when I tried it on some non-primitive types.

    As a final note, the methods generated with indeed work on all primitive types. Even booleans. I was surprised to get a DivideByZeroException upon attempting this:
    Operator.Divide(true, false);
    Again, this only works on primitive types. Remember, string and decimal are not primitive types. Perhaps this could be extended to calling the op_* functions...

    After a couple small performance tests, I found that this method was nearly as fast as doing simple addition (x + y). Then again, I know nothing about microbenchmarks. Try it for yourself :D

    Enjoy
    1

    View comments

Blog Archive
Active Projects
Active Projects
Total Pageviews
Total Pageviews
10816
Loading