#c_sharp
This question already has answers here: Как написать метод/класс, который бы одинаково работал со всеми числовыми типами? (3 ответа) Закрыт 1 год назад. Например класс для работы с комплексными числами. Я хочу чтобы конструктор и некоторые методы могли принимать как int так и double. Внутри методов только математические операции которые выполнимы для всех числовых типов. Более расширенная версия задания, иметь возможность передавать в методы любые класы которые поддерживают математические операции. Подскажите каким образом лучше всего это реализовать, мне на ум приходит только создание интерфейса который бы определял все операции а потом нужные класы наследовать от него, но тогда как быть с int и double?
Ответы
Ответ 1
Можно сделать кодогенерацию на встроенном в Visual Studio генераторе кода T4. Добавляем в проект Text Template (с расширением .tt). Пишем в нём, например, такой код: <#@ template debug="false" hostspecific="false" language="C#" #> <#@ output extension=".cs" #> namespace MyApp // заменить на нужное { public class Calculator { <# // Добавить/удалить типы var types = new string[] { "int", "long", "float", "double" }; foreach(var T in types) { #> public <#=T#> Add(<#=T#> a, <#=T#> b) { return a + b; } public <#=T#> Subtract(<#=T#> a, <#=T#> b) { return a - b; } <# } #> } } Несложно заметить, что это очень похоже на Razor (веб-разработчики хорошо с ним знакомы). output extension нужно заменить на .cs - нам нужен на выходе код C#. Компилируем проект. Получаем класс с набором перегрузок методов. Для удобной работы с T4 в Студию можно установить какое-нибудь расширение, например, tangible T4.Ответ 2
К сожалению, в C# нет общего типа и общего метода работы с числами произвольного типа. Невозможно выразить наличие статических операций сложения/вычитания при помощи ограничений на обобщённые типы (generic constraints).* Но вы можете определить нужные операции самостоятельно, например, так: class Operation { public static T Add(T t1, T t2) => OperationImpl .add(t1, t2); public static T Subtract (T t1, T t2) => OperationImpl .subtract(t1, t2); class OperationImpl { public static Func add, subtract; static Func ForceCast(Func f) => (Func )(object)f; static OperationImpl() { if (typeof(T) == typeof(int)) { add = ForceCast ((x, y) => x + y); subtract = ForceCast ((x, y) => x - y); } else if (typeof(T) == typeof(double)) { add = ForceCast ((x, y) => x + y); subtract = ForceCast ((x, y) => x - y); } else if (typeof(T) == typeof(Complex)) { add = ForceCast ((x, y) => x + y); subtract = ForceCast ((x, y) => x - y); } else { throw new NotSupportedException( $"Operations on type {typeof(T).Name} are not supported"); } } } } Проверка: http://ideone.com/XOea6Z Альтернативные решения (использование Expression.Compile или System.Numerics.Vectors) описаны здесь. *Но работа в этом направлении ведётся, рассматривается возможность введения typeclass'ов à la Haskell. Ответ 3
Я тут все балуюсь с IL, так что мое решение напрямую связано с ним) Итак. Давайте начнем вот с чего: Напишем такой вот код: int a = 2; int b = 3; int c = a + b; Просмотрев IL-код, созданный для данной цепочки выражений, мы увидим нечто такое: ldc.i4.2 stloc a ldc.i4.3 stloc b ldloc a ldloc b add stloc c (Код примерный, таким он, конечно, не будет. Приведен он в таком виде для ясности происходящего) Что же отвечает за сложение двух чисел типа System.Int32? Правильно: инструкция add! Перепишем код: double a = 2; double b = 3; double c = a + b; Теперь IL будет таковым: ldc.r8 2 stloc a ldc.r8 3 stloc b ldloc a ldloc b add stloc c Что изменилось? Только инструкция loadconstant, инструкция же сложения так и осталось на своем законном месте) Я не буду продолжать, Вы уже поняли, к чему я клоню) На уровне IL одна и та же инструкция add спокойненько обрабатывает сложение экземпляров типов sbyte, byte, short, ushort, int, uint, long, ulong, float, double) А ведь это именно то, что нам нужно! (К слову, это верно и для инструкций sub, mul, div, rem. Подробный лист инструкций IL с описанием найдете здесь) Итак, если Вы можете безболезненно добавить к проекту .il файл (к примеру, с помощью расширения ILSupport + я также нахожусь в процессе ленивого написания подобного расширения), то метод того же сложения мы можем описать так: .method private static !!T _Add(!!T, !!T) cil managed { .maxstack 2 ldarg.0 // Кладем на стек нулевой аргумент ldarg.1 // Кладем на стек первый аргумент add // Складываем их ret // Возвращаем результат } В основном файле опишем нечто такое: // Доступные для обработки типы private static HashSet AvailableTypes { get; } = new HashSet { 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 A, T B); // Обертка над нашим методом, которая проверяет, // допустим ли тип переданных объектов // На деле можно проверку и в основном методе реализовать, // но так нагляднее) public static T Add (T A, T B) where T : struct { if (!AvailableTypes.Contains(typeof(T))) throw new TypeAccessException("Unsupported type!"); return _Add(A, B); } Время тестировать!) 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 Как видите, все прекрасно работает! Один generic-метод принимает на вход любой числовой тип и возвращает корректный ответ! Если же Вы в силу каких-то обстоятельств ограничены только C#-кодом, то могу предложить Вам решение с помощью MethodBuilder Тогда убираем метод _Add, а на его месте ставим такую конструкцию: 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; И чуток подправим метод-обертку: public static T Add (T A, T B) 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 }); } Весь остальной код остался тем же и ничуть не потерял в работоспособности Разница лишь в том, что в нулевом случае мы получили нужный метод на этапе компиляции, а здесь - во время выполнения программы) Надеюсь, мое решение показалось Вам хоть немножечко, но интересным, а также привнесло какие-то новые идеи для реализации в строй Ваших мыслей!) Ответ 4
Если в методе (в т.ч. конструкторе) принимается аргументы double, можно свободно передавать туда int: действует автоматическое приведение типов. Чтобы передавать любые объекты, уже придётся использовать Generic.
Комментариев нет:
Отправить комментарий