Sobrecarga de operador aritmético para uma class genérica em c #

Dada uma definição de class genérica como

public class ConstrainedNumber : IEquatable<ConstrainedNumber>, IEquatable, IComparable<ConstrainedNumber>, IComparable, IComparable where T:struct, IComparable, IComparable, IEquatable 

Como posso definir operadores aritméticos para isso?

O seguinte não compila, porque o operador ‘+’ não pode ser aplicado aos tipos ‘T’ e ‘T’:

 public static T operator +( ConstrainedNumber x, ConstrainedNumber y) { return x._value + y._value; } 

O tipo genérico ‘T’ é restrito com a palavra-chave ‘where’ como você pode ver, mas eu preciso de uma restrição para tipos de números que possuem operadores aritméticos (IArithmetic?).

‘T’ será um tipo de número primitivo como int, float, etc. Existe uma restrição ‘where’ para esses tipos?

    Acho que o melhor que você pode fazer é usar o IConvertible como uma restrição e fazer algo como:

      public static operator T +(T x, T y) where T: IConvertible { var type = typeof(T); if (type == typeof(String) || type == typeof(DateTime)) throw new ArgumentException(String.Format("The type {0} is not supported", type.FullName), "T"); try { return (T)(Object)(x.ToDouble(NumberFormatInfo.CurrentInfo) + y.ToDouble(NumberFormatInfo.CurrentInfo)); } catch(Exception ex) { throw new ApplicationException("The operation failed.", ex); } } 

    Isso não impedirá que alguém passe em um String ou DateTime, então talvez você queira fazer uma verificação manual – mas o IConvertible deve aproximá-lo o suficiente e permitir que você faça a operação.

    Infelizmente não há maneira de restringir um parâmetro genérico para ser um tipo integral ( Edit: Eu acho que “tipo aritmético” pode ser uma palavra melhor, pois isso não diz respeito apenas a inteiros).

    Seria bom poder fazer algo assim:

     where T : integral // or "arithmetical" depending on how pedantic you are 

    ou

     where T : IArithmetic 

    Eu sugiro que você leia Generic Operators do nosso próprio Marc Gravell e Jon Skeet. Isso explica por que esse é um problema tão difícil e o que pode ser feito para contorná-lo.

    O .NET 2.0 introduziu os genéricos no mundo do .NET, o que abriu as portas para muitas soluções elegantes para os problemas existentes. Restrições genéricas podem ser usadas para restringir os argumentos de tipos a interfaces conhecidas, etc., para garantir o access à funcionalidade – ou para simples igualdade / desigualdade testa os singletons Comparer.Default e EqualityComparer.Default implementam IComparer e IEqualityComparer respectivamente (nos permitindo classificar elementos para exemplo, sem ter que saber nada sobre o “T” em questão).

    Com tudo isso, no entanto, ainda há uma grande lacuna quando se trata de operadores. Como os operadores são declarados como methods estáticos, não há nenhuma interface IMath ou equivalente equivalente que todos os tipos numéricos implementam; e, de fato, a flexibilidade dos operadores tornaria isso muito difícil de fazer de maneira significativa. Pior: muitos dos operadores em tipos primitivos nem sequer existem como operadores; em vez disso, existem methods diretos de IL. [ênfase minha] Para tornar a situação ainda mais complexa, Nullable <> exige o conceito de “operadores levantados”, onde o “T” interno descreve os operadores aplicáveis ​​ao tipo anulável – mas isso é implementado como um recurso de linguagem e é não fornecido pelo tempo de execução (tornando a reflexão ainda mais divertida).

    No c # 4.0, você pode usar dynamic para contornar essa limitação. Eu dei uma olhada no seu código e consegui produzir uma versão funcional (embora reduzida):

      public class ConstrainedNumber where T : struct, IComparable, IComparable, IEquatable { private T _value; public ConstrainedNumber(T value) { _value = value; } public static T operator +(ConstrainedNumber x, ConstrainedNumber y) { return (dynamic)x._value + y._value; } } 

    E um pequeno programa de testes para acompanhar:

     class Program { static void Main(string[] args) { ConstrainedNumber one = new ConstrainedNumber(10); ConstrainedNumber two = new ConstrainedNumber(5); var three = one + two; Debug.Assert(three == 15); Console.ReadLine(); } } 

    Apreciar!

    Não, isso não funciona. Mas há algumas sugestões sobre como resolver o problema. Eu fiz o seguinte (usando algumas idéias de diferentes fonts na net):

     public delegate TResult BinaryOperator(TLeft left, TRight right); ///  /// Provide efficient generic access to either native or static operators for the given type combination. ///  /// The type of the left operand. /// The type of the right operand. /// The type of the result value. /// Inspired by Keith Farmer's code on CodeProject:
    http://www.codeproject.com/KB/cs/genericoperators.aspx
    public static class Operator
    { private static BinaryOperator addition; private static BinaryOperator bitwiseAnd; private static BinaryOperator bitwiseOr; private static BinaryOperator division; private static BinaryOperator exclusiveOr; private static BinaryOperator leftShift; private static BinaryOperator modulus; private static BinaryOperator multiply; private static BinaryOperator rightShift; private static BinaryOperator subtraction; /// /// Gets the addition operator + (either native or "op_Addition"). /// /// The addition operator. public static BinaryOperator Addition { get { if (addition == null) { addition = CreateOperator("op_Addition", OpCodes.Add); } return addition; } } /// /// Gets the modulus operator % (either native or "op_Modulus"). /// /// The modulus operator. public static BinaryOperator Modulus { get { if (modulus == null) { modulus = CreateOperator("op_Modulus", OpCodes.Rem); } return modulus; } } /// /// Gets the exclusive or operator ^ (either native or "op_ExclusiveOr"). /// /// The exclusive or operator. public static BinaryOperator ExclusiveOr { get { if (exclusiveOr == null) { exclusiveOr = CreateOperator("op_ExclusiveOr", OpCodes.Xor); } return exclusiveOr; } } /// /// Gets the bitwise and operator & (either native or "op_BitwiseAnd"). /// /// The bitwise and operator. public static BinaryOperator BitwiseAnd { get { if (bitwiseAnd == null) { bitwiseAnd = CreateOperator("op_BitwiseAnd", OpCodes.And); } return bitwiseAnd; } } /// /// Gets the division operator / (either native or "op_Division"). /// /// The division operator. public static BinaryOperator Division { get { if (division == null) { division = CreateOperator("op_Division", OpCodes.Div); } return division; } } /// /// Gets the multiplication operator * (either native or "op_Multiply"). /// /// The multiplication operator. public static BinaryOperator Multiply { get { if (multiply == null) { multiply = CreateOperator("op_Multiply", OpCodes.Mul); } return multiply; } } /// /// Gets the bitwise or operator | (either native or "op_BitwiseOr"). /// /// The bitwise or operator. public static BinaryOperator BitwiseOr { get { if (bitwiseOr == null) { bitwiseOr = CreateOperator("op_BitwiseOr", OpCodes.Or); } return bitwiseOr; } } /// /// Gets the left shift operator << (either native or "op_LeftShift"). /// /// The left shift operator. public static BinaryOperator LeftShift { get { if (leftShift == null) { leftShift = CreateOperator("op_LeftShift", OpCodes.Shl); } return leftShift; } } /// /// Gets the right shift operator >> (either native or "op_RightShift"). /// /// The right shift operator. public static BinaryOperator RightShift { get { if (rightShift == null) { rightShift = CreateOperator("op_RightShift", OpCodes.Shr); } return rightShift; } } /// /// Gets the subtraction operator - (either native or "op_Addition"). /// /// The subtraction operator. public static BinaryOperator Subtraction { get { if (subtraction == null) { subtraction = CreateOperator("op_Subtraction", OpCodes.Sub); } return subtraction; } } private static BinaryOperator CreateOperator(string operatorName, OpCode opCode) { if (operatorName == null) { throw new ArgumentNullException("operatorName"); } bool isPrimitive = true; bool isLeftNullable; bool isRightNullable = false; Type leftType = typeof(TLeft); Type rightType = typeof(TRight); MethodInfo operatorMethod = LookupOperatorMethod(ref leftType, operatorName, ref isPrimitive, out isLeftNullable) ?? LookupOperatorMethod(ref rightType, operatorName, ref isPrimitive, out isRightNullable); DynamicMethod method = new DynamicMethod(string.Format("{0}:{1}:{2}:{3}", operatorName, typeof(TLeft).FullName, typeof(TRight).FullName, typeof(TResult).FullName), typeof(TResult), new Type[] {typeof(TLeft), typeof(TRight)}); Debug.WriteLine(method.Name, "Generating operator method"); ILGenerator generator = method.GetILGenerator(); if (isPrimitive) { Debug.WriteLine("Primitives using opcode", "Emitting operator code"); generator.Emit(OpCodes.Ldarg_0); if (isLeftNullable) { generator.EmitCall(OpCodes.Call, typeof(TLeft).GetMethod("op_Explicit", BindingFlags.Public|BindingFlags.Static), null); } IlTypeHelper.ILType stackType = IlTypeHelper.EmitWidening(generator, IlTypeHelper.GetILType(leftType), IlTypeHelper.GetILType(rightType)); generator.Emit(OpCodes.Ldarg_1); if (isRightNullable) { generator.EmitCall(OpCodes.Call, typeof(TRight).GetMethod("op_Explicit", BindingFlags.Public | BindingFlags.Static), null); } stackType = IlTypeHelper.EmitWidening(generator, IlTypeHelper.GetILType(rightType), stackType); generator.Emit(opCode); if (typeof(TResult) == typeof(object)) { generator.Emit(OpCodes.Box, IlTypeHelper.GetPrimitiveType(stackType)); } else { Type resultType = typeof(TResult); if (IsNullable(ref resultType)) { generator.Emit(OpCodes.Newobj, typeof(TResult).GetConstructor(new Type[] {resultType})); } else { IlTypeHelper.EmitExplicit(generator, stackType, IlTypeHelper.GetILType(resultType)); } } } else if (operatorMethod != null) { Debug.WriteLine("Call to static operator method", "Emitting operator code"); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.EmitCall(OpCodes.Call, operatorMethod, null); if (typeof(TResult).IsPrimitive && operatorMethod.ReturnType.IsPrimitive) { IlTypeHelper.EmitExplicit(generator, IlTypeHelper.GetILType(operatorMethod.ReturnType), IlTypeHelper.GetILType(typeof(TResult))); } else if (!typeof(TResult).IsAssignableFrom(operatorMethod.ReturnType)) { Debug.WriteLine("Conversion to return type", "Emitting operator code"); generator.Emit(OpCodes.Ldtoken, typeof(TResult)); generator.EmitCall(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle", new Type[] {typeof(RuntimeTypeHandle)}), null); generator.EmitCall(OpCodes.Call, typeof(Convert).GetMethod("ChangeType", new Type[] {typeof(object), typeof(Type)}), null); } } else { Debug.WriteLine("Throw NotSupportedException", "Emitting operator code"); generator.ThrowException(typeof(NotSupportedException)); } generator.Emit(OpCodes.Ret); return (BinaryOperator)method.CreateDelegate(typeof(BinaryOperator)); } private static bool IsNullable(ref Type type) { if (type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(Nullable<>))) { type = type.GetGenericArguments()[0]; return true; } return false; } private static MethodInfo LookupOperatorMethod(ref Type type, string operatorName, ref bool isPrimitive, out bool isNullable) { isNullable = IsNullable(ref type); if (!type.IsPrimitive) { isPrimitive = false; foreach (MethodInfo methodInfo in type.GetMethods(BindingFlags.Static|BindingFlags.Public)) { if (methodInfo.Name == operatorName) { bool isMatch = true; foreach (ParameterInfo parameterInfo in methodInfo.GetParameters()) { switch (parameterInfo.Position) { case 0: if (parameterInfo.ParameterType != typeof(TLeft)) { isMatch = false; } break; case 1: if (parameterInfo.ParameterType != typeof(TRight)) { isMatch = false; } break; default: isMatch = false; break; } } if (isMatch) { if (typeof(TResult).IsAssignableFrom(methodInfo.ReturnType) || typeof(IConvertible).IsAssignableFrom(methodInfo.ReturnType)) { return methodInfo; // full signature match } } } } } return null; } } internal static class IlTypeHelper { [Flags] public enum ILType { None = 0, Unsigned = 1, B8 = 2, B16 = 4, B32 = 8, B64 = 16, Real = 32, I1 = B8, // 2 U1 = B8|Unsigned, // 3 I2 = B16, // 4 U2 = B16|Unsigned, // 5 I4 = B32, // 8 U4 = B32|Unsigned, // 9 I8 = B64, //16 U8 = B64|Unsigned, //17 R4 = B32|Real, //40 R8 = B64|Real //48 } public static ILType GetILType(Type type) { if (type == null) { throw new ArgumentNullException("type"); } if (!type.IsPrimitive) { throw new ArgumentException("IL native operations requires primitive types", "type"); } if (type == typeof(double)) { return ILType.R8; } if (type == typeof(float)) { return ILType.R4; } if (type == typeof(ulong)) { return ILType.U8; } if (type == typeof(long)) { return ILType.I8; } if (type == typeof(uint)) { return ILType.U4; } if (type == typeof(int)) { return ILType.I4; } if (type == typeof(short)) { return ILType.U2; } if (type == typeof(ushort)) { return ILType.I2; } if (type == typeof(byte)) { return ILType.U1; } if (type == typeof(sbyte)) { return ILType.I1; } return ILType.None; } public static Type GetPrimitiveType(ILType iLType) { switch (iLType) { case ILType.R8: return typeof(double); case ILType.R4: return typeof(float); case ILType.U8: return typeof(ulong); case ILType.I8: return typeof(long); case ILType.U4: return typeof(uint); case ILType.I4: return typeof(int); case ILType.U2: return typeof(short); case ILType.I2: return typeof(ushort); case ILType.U1: return typeof(byte); case ILType.I1: return typeof(sbyte); } throw new ArgumentOutOfRangeException("iLType"); } public static ILType EmitWidening(ILGenerator generator, ILType onStackIL, ILType otherIL) { if (generator == null) { throw new ArgumentNullException("generator"); } if (onStackIL == ILType.None) { throw new ArgumentException("Stack needs a value", "onStackIL"); } if (onStackIL < ILType.I8) { onStackIL = ILType.I8; } if ((onStackIL < otherIL) && (onStackIL != ILType.R4)) { switch (otherIL) { case ILType.R4: case ILType.R8: if ((onStackIL&ILType.Unsigned) == ILType.Unsigned) { generator.Emit(OpCodes.Conv_R_Un); } else if (onStackIL != ILType.R4) { generator.Emit(OpCodes.Conv_R8); } else { return ILType.R4; } return ILType.R8; case ILType.U8: case ILType.I8: if ((onStackIL&ILType.Unsigned) == ILType.Unsigned) { generator.Emit(OpCodes.Conv_U8); return ILType.U8; } if (onStackIL != ILType.I8) { generator.Emit(OpCodes.Conv_I8); } return ILType.I8; } } return onStackIL; } public static void EmitExplicit(ILGenerator generator, ILType onStackIL, ILType otherIL) { if (otherIL != onStackIL) { switch (otherIL) { case ILType.I1: generator.Emit(OpCodes.Conv_I1); break; case ILType.I2: generator.Emit(OpCodes.Conv_I2); break; case ILType.I4: generator.Emit(OpCodes.Conv_I4); break; case ILType.I8: generator.Emit(OpCodes.Conv_I8); break; case ILType.U1: generator.Emit(OpCodes.Conv_U1); break; case ILType.U2: generator.Emit(OpCodes.Conv_U2); break; case ILType.U4: generator.Emit(OpCodes.Conv_U4); break; case ILType.U8: generator.Emit(OpCodes.Conv_U8); break; case ILType.R4: generator.Emit(OpCodes.Conv_R4); break; case ILType.R8: generator.Emit(OpCodes.Conv_R8); break; } } } }

    Use assim: int i = Operator.Addition (3, 5);

    Não há restrições disponíveis para isso, mas há uma maneira de contornar o problema:

     public static T operator -(T foo, T bar) { return (T)System.Convert.ChangeType( System.Convert.ToDecimal(foo) - System.Convert.ToDecimal(bar), typeof(T)); } 

    Se você não está usando muitos tipos que são usados ​​como argumento genérico e quer ter verificações em tempo de compilation, então você pode usar uma solução semelhante à solução da Lucero.

    Classe base

     public class Arithmetic { protected static readonly Func OP_ADD; protected static readonly Func OP_MUL; protected static readonly Func OP_SUB; /* Define all operators you need here */ static Arithmetic() { Arithmetic.OP_ADD = (x, y) => x + y; Arithmetic.OP_MUL = (x, y) => x * y; Arithmetic.OP_SUB = (x, y) => x - y; Arithmetic.OP_ADD = (x, y) => x + y; Arithmetic.OP_MUL = (x, y) => x * y; Arithmetic.OP_SUB = (x, y) => x - y; /* This could also be generated by a tool */ } } 

    Uso

     public class Vector2 : Arithmetic { public TX; public TY; public static Vector2 operator +(Vector2 a, Vector2 b) { return new Vector2() { X = OP_ADD(aX, bX), Y = OP_ADD(aY, bY) }; } public static Vector2 operator -(Vector2 a, Vector2 b) { return new Vector2() { X = OP_SUB(aX, bX), Y = OP_SUB(aY, bY) }; } public static Vector2 operator *(Vector2 a, Vector2 b) { return new Vector2() { X = OP_MUL(aX, bX), Y = OP_MUL(aY, bY) }; } } 

    Não há suporte atual em genéricos .Net para indicar que os operadores são suportados.

    Este é um recurso frequentemente solicitado.

    Pode ser semi trabalhado ao redor (veja MiscUtils ) mas isto não lhe dará a syntax que você deseja

    Eu só fiz isso depois de olhar aqui. A class Vector4 contém 4 números / eixos do tipo T com a matemática usual do vetor. Basta adicionar 2 operações implícitas para converter de e para Decimal. Isto é provavelmente tão sem verbosidade quanto você vai conseguir, mas como você aponta, mais preciso e, portanto, mais pesado do que precisa ser. Como vocês, eu gostaria que houvesse um numérico ou algo assim!

    public static Vector4 operator +(Vector4 a, Vector4 b) { Vector4 A = a; Vector4 B = b; var result = new Vector4(AX + BX, AY + BY, AZ + BZ, AW + BW); return result; } public static implicit operator Vector4(Vector4 v) { return new Vector4( Convert.ToDecimal(vX), Convert.ToDecimal(vY), Convert.ToDecimal(vZ), Convert.ToDecimal(vW)); } public static implicit operator Vector4(Vector4 v) { return new Vector4( (T)Convert.ChangeType(vX, typeof(T)), (T)Convert.ChangeType(vY, typeof(T)), (T)Convert.ChangeType(vZ, typeof(T)), (T)Convert.ChangeType(vW, typeof(T))); }
    public static Vector4 operator +(Vector4 a, Vector4 b) { Vector4 A = a; Vector4 B = b; var result = new Vector4(AX + BX, AY + BY, AZ + BZ, AW + BW); return result; } public static implicit operator Vector4(Vector4 v) { return new Vector4( Convert.ToDecimal(vX), Convert.ToDecimal(vY), Convert.ToDecimal(vZ), Convert.ToDecimal(vW)); } public static implicit operator Vector4(Vector4 v) { return new Vector4( (T)Convert.ChangeType(vX, typeof(T)), (T)Convert.ChangeType(vY, typeof(T)), (T)Convert.ChangeType(vZ, typeof(T)), (T)Convert.ChangeType(vW, typeof(T))); } 

    Como sobre esses amigos (usando o RTTI e a class de object)

     class MyMath { public static T Add(T a, T b) where T: struct { switch (typeof(T).Name) { case "Int32": return (T) (object)((int)(object)a + (int)(object)b); case "Double": return (T)(object)((double)(object)a + (double)(object)b); default: return default(T); } } } class Program { public static int Main() { Console.WriteLine(MyMath.Add(3.6, 2.12)); return 0; } } 

    Infelizmente, isso não é possível, pois não há uma IArithmetic (como você disse) definida para inteiros. Você pode envolver esses tipos primitivos em classs que implementam essa interface.

    Eu vi algumas possíveis soluções envolvendo trees de expressão, onde a expressão do operador é criada manualmente.

    Não é perfeito porque você perde a verificação em tempo de compilation, mas pode ser útil para você.

    aqui está um artigo sobre isso.

    Se eu tivesse que fazer algo assim, eu provavelmente abordaria as linhas de

     public class ConstrainedNumber { private T Value { get; } public ConstrainedNumber(T value) { Value = value; } private static Func, ConstrainedNumber, T> _addFunc; // Cache the delegate public static ConstrainedNumber operator+(ConstrainedNumber left, ConstrainedNumber right) { var adder = _addFunc; if (adder == null) { ParameterExpression lhs = Expression.Parameter(typeof(ConstrainedNumber)); ParameterExpression rhs = Expression.Parameter(typeof(ConstrainedNumber)); _addFunc = adder = Expression.Lambda, ConstrainedNumber, T>>( Expression.Add( Expression.Property(lhs, nameof(Value)), Expression.Property(lhs, nameof(Value)) ), lhs, rhs).Compile(); } return new ConstrainedNumber(adder(left, right)); } } 

    O resultado final é um pouco como o resultado final da abordagem dynamic , que acabará internamente fazendo algo assim, mas com um pouco mais de sobrecarga, e deve funcionar para qualquer T que seja primitivo aritmético ou tenha um operador + definido para isso. O único caso em que a abordagem dynamic lidaria de maneira diferente é que ela funcionaria para string enquanto isso não funcionaria. Se isso é bom ou ruim, depende do caso de uso, mas se for necessário, a string pode ser encapsulada em especial.