假设我想描述generic一个充当计算器的 -class,以便它对所有数字类型都以相同的方式工作。那些。:
- 字节
- 字节
- 短的
- 超短
- 整数
- 单位
- 长
- 乌龙
- 漂浮
- 双倍的
任务的复杂性在于,尽管所有这些类型都通过为其算术运算实例的定义而联合起来,但是,它们不继承任何类型的公共接口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:代码重复且没有泛型
毫无疑问,最平庸的解决方案,所有绝望的求助,只是为每种类型复制代码:
优点:
缺点:
垃圾,破坏了项目的所有优雅。Add稍微改变它的逻辑(不管听起来多么奇怪),那么N个方法中的每一个都必须重写!#0.1:代码生成
为了以某种方式简化您的生活并且不编写大量相同类型的方法,您可以使用内置的
Visual StudioT4 生成器进行代码生成:让我们
Calc.tt根据模板Text Template(Текстовый шаблон)将文件添加到项目中。让我们在其中编写以下代码:输出 (
Calc.cs) 将如下所示:优点:
缺点:
Visual Studio蹩脚的。所以你可以忘记舒适的编码)这种方法最初在Alexander Petrov 的回答中有所描述
#1.0:动态
由于我们正在使用动态类型解决一个语言不知道的问题,下一个明显的解决方案是使用dynamic:
其实很适合我们!
让我们像这样重写方法
Add:让我们看看发生了什么:
一切似乎都很好!
对,不过我得在这勺蜂蜜里加一桶焦油:
除了解析动态上下文比解析静态上下文消耗更多时间之外,我们没有预见到以下事情:
谁在阻止我们这样写?
同样,没有人,所以代码会安静地编译,会成功创建一个类的实例,但是方法
Add会失败并出现以下错误:正如问题本身所提到的,我们不能将 -parameters限制
generic为一组特定的类型。所以你必须手动并在runtime:优点:
generic中,以便不同的预定义方法适用于不同的类型缺点:
#2.0:根据类型改变函数上下文
该方法背后的思想如下:
我们不能像这样在运行时重新分配函数:
但是,我们可以重新分配变量(包括委托类型)!
我们可以创建一个委托类型的内部字段,根据情况重新分配它,而公共方法是不可变的,只会利用它:
这种方法应该放在“重复代码”的板块中,但对很多人来说仍然没有那么明显
dynamic,可以展开主题,在下一个板块中展示)优点:
缺点:
这种方法最初是在VladD 的回答中描述的
#2.1:表达式:
使用Expression类,我们可以通过节点组装我们需要的表达式树,并编译成所需签名的委托,使用上一种方法的基本思想:
优点:
generic实现)缺点:
runtime创建方法这种方法最初在Pavel Mayorov 的回答中有所描述
#3.0:矢量化
我们
Microsoft有以下漂亮的包System.Numerics.Vectors,其描述如下:在这个包中,我们对Vector<T>类型感兴趣,它能够对输入数据进行矢量化处理,然后我们可以将所需的算术运算应用于接收到的矢量!
让我们看一个例子:
优点:
缺点:
nuget-package这种方法最初是在VladD 的回答中描述的
#4.0: IL позволит Вам то, чего не позволит C#!
Как известно, код любого
.NET-языка транслируется в IL-код. Этот факт мы и будем использовать)Напишем такой вот код:
Просмотрев
IL-код, созданный для данной цепочки выражений, мы увидим нечто такое:(Код примерный, таким он, конечно, не будет. Приведен он в таком виде для ясности происходящего)
Что же отвечает за сложение двух чисел типа
int?Стандартная инструкция
add)Перепишем код:
Теперь
ILбудет таковым:Что изменилось? Только инструкция loadconstant, инструкция же сложения так и осталось на своем законном месте)
Я веду к тому, что на уровне
ILодна и та же инструкцияaddспокойненько обрабатывает сложение экземпляров типовsbyte,byte,short,ushort,int,uint,long,ulong,float,double)А ведь это именно то, что нам нужно!
(К слову, это верно и для инструкций
sub,mul,div,rem. Подробный лист инструкцийILс описанием найдете здесь)Добавим к проекту файл
Calc.il, используя расширение ILSupport, после чего запишем туда следующий код:На
C#же проделаем следующие манипуляции с классом:Вот и готово! Скомпилировав проект, мы получим класс, который способен работать с любым стандартным числовым типом)
Плюсы:
runtimeМинусы:
IL, решение может показаться сложнымC#иILДанный подход был первоначально описан в ответе от Kir_Antipov
Надеюсь, один из предложенных в данном ответе методов помог решить Вам указанную задачу)
А пока у меня есть 2 большие просьбы:
Не забывайте благодарить авторов оригинальных ответов (помимо своего решения я собрал в данном ответе и идеи других участников сообщества, приведя на них ссылки)
Если у Вас есть еще идеи по решению данной задачи/по исправлению данного ответа - пишите комментарии! Буду безумно рад выслушать Ваше мнение)
Мне нравятся подходы 0.0 и 0.1 из предыдущего ответа, но дублирование кода я бы сделал по-другому принципу, я бы вынес интерфейс
ICalculator<T>:Потребуется реализовать
ICalculator<T>для каждого типа с которым вы хотите работать, они не обязательно должны быть числами. Такой подход будет гораздо лучше если вы используете Dependency Injection, вы сможете передаватьICalculator<T>в класс где он требуется. Ну и напоследок - реализация может быть либо такой, либо можно создать универсальную с dynamic или IL.Все числовые типы объединяет то, что они являются структурами и реализуют интерфейс IComparable. С этим ограничением уже можно отсечь много неподходящих типов на этапе компиляции. Не нужно использовать статические конструкторы для "валидации", они предназначены для инициализации глобального состояния, и класс, единственная задача которого - арифметические операции, вообще не должен их иметь. Проверяйте перед вычислением (или компиляцией выражения), это намного более логично.
Что касается алгоритма, есть еще один способ, который лежит на поверхности: это простой обобщенный метод с несколькими ветками в условном операторе. Может показаться, что веток будет слишком много, но на самом деле, операции сложения для многих типов по сути одинаковы и отличаются только типом, к которому приводится конечный результат. Например, операцию сложения на целом типе можно представить как операцию сложения на Decimal с последующим "сужающим" приведением к целому типу (Decimal позволяет представить все значения любых целых типов и еще оставляет некоторый запас для обработки переполнений). Аналогично, сложение на типе float можно представить как сложение на типе double с последующим преобразованием результата.
Весь набор числовых типов можно разделить на три группы:
где n - размер типа в битах.
(Остаток от деления тут появляется, так как по умолчанию у нас unchecked-контекст, и переполнения не генерируют ошибку, а просто обрезаются по границе типа.)
На самом деле, формула может выглядеть по разному, но для отлова переполнений подходит именно такой вид.
Реализовать это можно так:
Если наплевать на переполнения, то код можно значительно упростить.