Предположим, я хочу описать generic-класс, выполняющий роль калькулятора, таким образом, чтобы он одинаково работал для всех числовых типов. Т.е.:
Сложность задачи заключается в том, что пусть все эти типы и объединяет наличие определения для их экземпляров арифметических операций, однако они не наследуют какого-либо общего интерфейса типа IArithmetical, INumber. А также ограничение where в C# не позволяет нам описать нечто вроде:
public static void Add<T>(T A, T B) where T : +, -, *, /, % ...
Так что компилятор не может быть уверен в том, что для всех возможных типов, эксплуатирующих метод, определены нужные операции
Это приводит к тому, что подобный код:
public class Calculator<T>
{
public T Add(T A, T B) => A + B;
public T Sub(T A, T B) => A - B;
public T Mul(T A, T B) => A * B;
public T Div(T A, T B) => A / B;
public T Mod(T A, T B) => A % B;
}
Увы, но просто невозможно скомпилировать: по описанным выше причинам будет выкинута ошибка CS0019
Так что же делать в данной ситуации? Возможно-ли вообще средствами C# описать generic-класс/метод, который бы мог работать с числами и только с ними?
Я начал этот топик, дабы рассмотреть все известные (мне) способы решения поставленной проблемы и дать максимально развернутый ответ на сей довольно таки частый вопрос
Если вдруг Вам известен метод, который я по каким-то причинам не описал в данном посте - напишите, пожалуйста, об этом в комментариях!
Итак, поехали!
#0.0: Дублирование кода и никаких generic'овБесспорно, самым банальным решением, к которому и прибегают все отчаявшиеся, является простое дублирование кода для каждого из типов:
public sbyte Add(sbyte A, sbyte B) => (sbyte)(A + B);
public byte Add(byte A, byte B) => (byte)(A + B);
public short Add(short A, short B) => (short)(A + B);
public ushort Add(ushort A, ushort B) => (ushort)(A + B);
public int Add(int A, int B) => A + B;
public uint Add(uint A, uint B) => A + B;
public long Add(long A, long B) => A + B;
public ulong Add(ulong A, ulong B) => A + B;
public float Add(float A, float B) => A + B;
public double Add(double A, double B) => A + B;
Add немного поменяет свою логику (как бы странно это ни звучало), то переписывать придется каждый из N методов! Чтобы хоть как-то упростить себе жизнь и не писать множество однотипных методов, можно прибегнуть к кодогенерации с помощью встроенного в Visual Studio T4-генератора:
Добавим в проект файл Calc.tt по шаблону Text Template (Текстовый шаблон). Запишем в него следующий код:
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ template debug="false" hostspecific="false" language="C#" #>
namespace Calc
{
public class Calculator
{
<#
// Типы, используемые в методах
string[] usingTypes = new[] { "sbyte", "byte", "short", "ushort", "int", "uint", "long", "ulong", "float", "double" };
// Экземпляры некоторых типов перед операцией кастятся к int, так что результат нужно привести обратно
HashSet<string> needCast= new HashSet<string> { "sbyte", "byte", "short", "ushort" };
foreach(string T in usingTypes)
{
#>
public <#=T#> Add(<#=T#> A, <#=T#> B) => <#=(needCast.Contains(T) ? $"({T})(A + B)" : "A + B")#>;
<#
}
#>
}
}
Выхлоп (Calc.cs) будет выглядеть так:
namespace Calc
{
public class Calculator
{
public sbyte Add(sbyte A, sbyte B) => (sbyte)(A + B);
public byte Add(byte A, byte B) => (byte)(A + B);
public short Add(short A, short B) => (short)(A + B);
public ushort Add(ushort A, ushort B) => (ushort)(A + B);
public int Add(int A, int B) => A + B;
public uint Add(uint A, uint B) => A + B;
public long Add(long A, long B) => A + B;
public ulong Add(ulong A, ulong B) => A + B;
public float Add(float A, float B) => A + B;
public double Add(double A, double B) => A + B;
}
}
Visual Studio хромает, мягко говоря. Так что о комфортном коддинге можно забыть)
Данный подход был первоначально описан в ответе от Alexander Petrov
#1.0: dynamicТак как мы решаем проблему, которая неведома языкам с динамической типизацией, то следующим очевидным решением будет использование dynamic:
Тип dynamic включает операции, в которых он применяется для обхода проверки типов во время компиляции. Такие операции разрешаются во время выполнения.
Собственно, нам это подходит!
Перепишем метод Add таким образом:
// Достаточно привести лишь один аргумент к dynamic,
// дабы обозначить динамический контекст
public T Add(T A, T B) => (T)((dynamic)A + B);
И посмотрим, что получилось:
Calculator<int> calcInt = new Calculator<int>();
int resultInt = calcInt.Add(19, 23); // 42
Calculator<sbyte> calcSbyte = new Calculator<sbyte>();
sbyte resultSbyte = calcSbyte.Add(19, 23); // 42
Кажется, все чудесно работает!
Право, все же вынужден добавить бочку дегтя в эту ложку меда:
Помимо того, что разрешение динамического контекста съедает куда больше времени, нежели разрешение статического, так мы ведь не предусмотрели следующей вещи:
Кто нам мешает написать так?
Calculator<DateTime> calcDate = new Calculator<DateTime>();
DateTime resultDate = calcDate.Add(DateTime.Now, DateTime.Now);
Как раз таки никто, так что код спокойно скомпилируется, экземпляр класса будет успешно создан, однако метод Add упадет со следующей ошибкой:
Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: Не удается применить оператор + к операндам типа System.DateTime и System.DateTime
Как уже упоминалось в самом вопросе, мы не можем ограничить generic-параметры определенным набором типов. Так что придется делать это вручную и в runtime:
public class Calculator<T>
{
// Добавим классу статический инициализатор, который будет отвечать
// за проверку валидности типа
// (Статический - дабы не проводить проверку несколько раз для одинаковых типов)
static Calculator()
{
// Если тип не является одним из доступных, то сразу же выкинем ошибку,
// дав тем самым конечному пользователю понять, что его
// действия неправомерны
if (!new[] { typeof(sbyte), typeof(byte),
typeof(short), typeof(ushort),
typeof(int), typeof(uint),
typeof(long), typeof(ulong),
typeof(float), typeof(double)}.Contains(typeof(T)))
throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
}
// Достаточно привести лишь один аргумент к dynamic,
// дабы обозначить динамический контекст
public T Add(T A, T B) => (T)((dynamic)A + B);
}
generic, чтобы для разных типов работали разные предопределенные методыИдея данного метода заключается в следующем:
Мы не можем просто так переназначить функции во время исполнения в духе:
public void A() => ...;
...
A = () => Console.WriteLine("Hello, world!");
Однако мы можем переназначать переменные (в том числе и типов делегатов)!
Мы можем создать внутреннее поле типа делегата, переназначать его в зависимости от ситуации, а уже публичный метод, будучи неизменным, как раз и будет его эксплуатировать:
public class Calculator<T>
{
static Calculator()
{
// Инициализируем _add, исходя из типа generic-параметра
if (typeof(T) == typeof(sbyte))
_add = castFrom<sbyte>((x, y) => (sbyte)(x + y));
else if (typeof(T) == typeof(byte))
_add = castFrom<byte>((x, y) => (byte)(x + y));
else if (typeof(T) == typeof(short))
_add = castFrom<short>((x, y) => (short)(x + y));
else if (typeof(T) == typeof(ushort))
_add = castFrom<ushort>((x, y) => (ushort)(x + y));
else if (typeof(T) == typeof(int))
_add = castFrom<int>((x, y) => x + y);
else if (typeof(T) == typeof(uint))
_add = castFrom<uint>((x, y) => x + y);
else if (typeof(T) == typeof(long))
_add = castFrom<long>((x, y) => x + y);
else if (typeof(T) == typeof(ulong))
_add = castFrom<ulong>((x, y) => x + y);
else if (typeof(T) == typeof(int))
_add = castFrom<float>((x, y) => x + y);
else if (typeof(T) == typeof(double))
_add = castFrom<double>((x, y) => x + y);
else
// Если тип не является ни одним из доступных, то выкинем ошибку
throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
Func<T, T, T> castFrom<U>(Func<U, U, U> f) => (Func<T, T, T>)(object)f;
}
// Инструкция внутри _add будет проинициализированна
// в зависимости от типа generic-параметра
private static readonly Func<T, T, T> _add;
// А вот инструкция в самой функции Add всегда одна - вызвать _add)
public T Add(T A, T B) => _add(A, B);
}
Этот метод стоило бы расположить в блоке про "Дублирование кода", однако для многих он все же является менее очевидным, чем dynamic, да и тему можно развить, что показано в следующем блоке)
Данный подход был первоначально описан в ответе от VladD
#2.1: Expression:С помощью класса Expression мы можем по узлам собрать нужное нам дерево выражений и скомпилировать его в делегат необходимой сигнатуры, используя при этом базовую идею предыдущего подхода:
public class Calculator<T>
{
static Calculator()
{
// Эту проверку Вы уже наблюдали)
if (!new[] { typeof(sbyte), typeof(byte),
typeof(short), typeof(ushort),
typeof(int), typeof(uint),
typeof(long), typeof(ulong),
typeof(float), typeof(double)}.Contains(typeof(T)))
throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
// Укажем параметры, испоьзуемые в функции
ParameterExpression a = Expression.Parameter(typeof(T));
ParameterExpression b = Expression.Parameter(typeof(T));
// Создадим узел сложения заданных параметров
BinaryExpression addition = Expression.Add(a, b);
// Скомпилируем полученное дерево
_add = Expression.Lambda<Func<T, T, T>>(addition, a, b).Compile();
}
// Инструкция внутри _add будет проинициализированна
// в зависимости от типа generic-параметра
private static readonly Func<T, T, T> _add;
// А вот инструкция в самой функции Add всегда одна - вызвать _add)
public T Add(T A, T B) => _add(A, B);
}
generic реализациям)runtime на создание методаДанный подход был первоначально описан в ответе от Pavel Mayorov
#3.0: ВекторизацияУ Microsoft есть следующий прекрасный пакет - System.Numerics.Vectors, описание коиего гласит:
Обеспечивает аппаратно-ускоренные числовые типы, подходящие для высокопроизводительной обработки и графических приложений.
В данном пакете нас интересует тип Vector<T>, которой способен векторизовать входные данные, после чего мы можем применять к полученным векторам нужные нам арифметические операции!
Посмотрим на примере:
public class Calculator<T> where T : struct
{
static Calculator()
{
// Эту проверку Вы уже наблюдали)
if (!new[] { typeof(sbyte), typeof(byte),
typeof(short), typeof(ushort),
typeof(int), typeof(uint),
typeof(long), typeof(ulong),
typeof(float), typeof(double)}.Contains(typeof(T)))
throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
}
// Создадим векторы на основе заданных значений,
// после чего сложим их и вернем 0-вое измерение
// результирующего вектора
public T Add(T A, T B) => (new Vector<T>(A) + new Vector<T>(B))[0];
}
nuget-пакетаДанный подход был первоначально описан в ответе от VladD
#4.0: IL позволит Вам то, чего не позволит C#!Как известно, код любого .NET-языка транслируется в 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
(Код примерный, таким он, конечно, не будет. Приведен он в таком виде для ясности происходящего)
Что же отвечает за сложение двух чисел типа int?
Стандартная инструкция 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 с описанием найдете здесь)
Добавим к проекту файл Calc.il, используя расширение ILSupport, после чего запишем туда следующий код:
.class Calc.Calculator`1<T>
{
.method public !T Add(!T, !T) cil managed
{
.maxstack 2
ldarg.0 // Кладем на стек нулевой аргумент
ldarg.1 // Кладем на стек первый аргумент
add // Складываем их
ret // Возвращаем результат
}
}
На C# же проделаем следующие манипуляции с классом:
public class Calculator<T>
{
static Calculator()
{
// Эту проверку Вы уже наблюдали)
if (!new[] { typeof(sbyte), typeof(byte),
typeof(short), typeof(ushort),
typeof(int), typeof(uint),
typeof(long), typeof(ulong),
typeof(float), typeof(double)}.Contains(typeof(T)))
throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
}
// Сообщаем, что метод реализован где-то в другом месте
[MethodImpl(MethodImplOptions.ForwardRef)]
public extern T Add(T A, T B);
}
Вот и готово! Скомпилировав проект, мы получим класс, который способен работать с любым стандартным числовым типом)
runtimeIL, решение может показаться сложнымC# и ILДанный подход был первоначально описан в ответе от Kir_Antipov
Надеюсь, один из предложенных в данном ответе методов помог решить Вам указанную задачу)
А пока у меня есть 2 большие просьбы:
Не забывайте благодарить авторов оригинальных ответов (помимо своего решения я собрал в данном ответе и идеи других участников сообщества, приведя на них ссылки)
Если у Вас есть еще идеи по решению данной задачи/по исправлению данного ответа - пишите комментарии! Буду безумно рад выслушать Ваше мнение)
Мне нравятся подходы 0.0 и 0.1 из предыдущего ответа, но дублирование кода я бы сделал по-другому принципу, я бы вынес интерфейс ICalculator<T>:
public interface ICalculator<T>
{
T Add(T a, T b);
T Sub(T a, T b);
T Mul(T a, T b);
T Div(T a, T b);
T Mod(T a, T b);
}
public class IntCalculator : ICalculator<int>
{
public int Add(int a, int b) => a + b;
public int Sub(int a, int b) => a - b;
public int Mul(int a, int b) => a * b;
public int Div(int a, int b) => a / b;
public int Mod(int a, int b) => a % b;
}
Потребуется реализовать ICalculator<T> для каждого типа с которым вы хотите работать, они не обязательно должны быть числами. Такой подход будет гораздо лучше если вы используете Dependency Injection, вы сможете передавать ICalculator<T> в класс где он требуется. Ну и напоследок - реализация может быть либо такой, либо можно создать универсальную с dynamic или IL.
Все числовые типы объединяет то, что они являются структурами и реализуют интерфейс IComparable. С этим ограничением уже можно отсечь много неподходящих типов на этапе компиляции. Не нужно использовать статические конструкторы для "валидации", они предназначены для инициализации глобального состояния, и класс, единственная задача которого - арифметические операции, вообще не должен их иметь. Проверяйте перед вычислением (или компиляцией выражения), это намного более логично.
Что касается алгоритма, есть еще один способ, который лежит на поверхности: это простой обобщенный метод с несколькими ветками в условном операторе. Может показаться, что веток будет слишком много, но на самом деле, операции сложения для многих типов по сути одинаковы и отличаются только типом, к которому приводится конечный результат. Например, операцию сложения на целом типе можно представить как операцию сложения на Decimal с последующим "сужающим" приведением к целому типу (Decimal позволяет представить все значения любых целых типов и еще оставляет некоторый запас для обработки переполнений). Аналогично, сложение на типе float можно представить как сложение на типе double с последующим преобразованием результата.
Весь набор числовых типов можно разделить на три группы:
y = x % 2 n
где n - размер типа в битах.
(Остаток от деления тут появляется, так как по умолчанию у нас unchecked-контекст, и переполнения не генерируют ошибку, а просто обрезаются по границе типа.)
y = (x + 2 n * 1.5) % 2 n - 0.5 * 2 n
На самом деле, формула может выглядеть по разному, но для отлова переполнений подходит именно такой вид.
Реализовать это можно так:
using System;
using System.Text;
namespace ConsoleApp1
{
public class Calculator<T> where T : struct,IComparable
{
static bool IsSignedInteger(Type t)
{
return (t == typeof(sbyte) || t == typeof(short) || t == typeof(int) || t == typeof(long));
}
static bool IsUnsignedInteger(Type t)
{
return (t == typeof(byte) || t == typeof(ushort) || t == typeof(uint) || t == typeof(ulong));
}
static bool IsReal(Type t)
{
return (t == typeof(float) || t == typeof(double));
}
//преобразует значение из Decimal в целевой целочисленный тип
public static T FromDecimal(decimal val)
{
//вычисляем размер типа
int size = System.Runtime.InteropServices.Marshal.SizeOf(typeof(T));
//вычисляем количество элементов в целевом множестве
decimal capacity = (size < 8) ? (1L << (size * 8)) : ((decimal)UInt64.MaxValue + 1);
//отображаем элемент на целевое множество
decimal res;
if (IsUnsignedInteger(typeof(T)))
{
res = (val) % (capacity);
return (T)Convert.ChangeType(res, typeof(T));
}
else if (IsSignedInteger(typeof(T)))
{
res = (val + capacity * 1.5M) % (capacity) - capacity * 0.5M;
return (T)Convert.ChangeType(res, typeof(T));
}
else throw new NotSupportedException(typeof(T).ToString() + " is not integer type");
}
//непосредственно сложение
public static T Add(T A, T B)
{
if (IsSignedInteger(typeof(T)) || IsUnsignedInteger(typeof(T)))
{
return FromDecimal(Convert.ToDecimal(A) + Convert.ToDecimal(B));
}
else if (IsReal(typeof(T)))
{
return (T)Convert.ChangeType(Convert.ToDouble(A) + Convert.ToDouble(B), typeof(T));
}
else throw new NotSupportedException(typeof(T).ToString() + " is not supported, because it is not numeric type");
}
}
class Program
{
static void Main(string[] args)
{
unchecked
{
//тест сложения целых чисел
Console.WriteLine("{0} {1}", Calculator<int>.Add(1000, 222), (1000 + 222));
Console.WriteLine("{0} {1}", Calculator<byte>.Add(200, 200), (byte)(200 + 200));
Console.WriteLine("{0} {1}", Calculator<sbyte>.Add(100, 100), (sbyte)(100 + 100));
Console.WriteLine("{0} {1}", Calculator<long>.Add(long.MinValue, -1), (long)(long.MinValue - 1));
//тест сложения с плавающей точкой
Console.WriteLine("{0} {1}", Calculator<float>.Add((float)Math.PI, 2.2f), (float)Math.PI + 2.2f);
Console.WriteLine("{0} {1}", Calculator<double>.Add(Math.PI, 2.2), Math.PI + 2.2);
//этот код выдаст исключение...
//Console.WriteLine("{0}", Calculator<DateTime>.Add(DateTime.Now, new DateTime(2000, 1, 1)));
//Console.WriteLine("{0}", Calculator<bool>.Add(true, true));
//а этот - не скомпилируется
//Console.WriteLine("{0}", Calculator<string>.Add("Саша", "Маша"));
}
Console.ReadKey();
}
}
}
Если наплевать на переполнения, то код можно значительно упростить.
Основные этапы разработки сайта для стоматологической клиники
Продвижение своими сайтами как стратегия роста и независимости