Ответ 1
Я закончил тем, что использовал Expressions, подход, обрисованный Марком Грэвеллом, который я нашел, перейдя по ссылкам из комментариев спинона.
https://jonskeet.uk/csharp/miscutil/usage/genericoperators.html
Я пишу класс, который делает по существу тот же тип вычислений для каждого из примитивных числовых типов в С#. Хотя реальный расчет является более сложным, подумайте об этом как о методе вычисления среднего числа значений, например.
class Calc
{
public int Count { get; private set; }
public int Total { get; private set; }
public int Average { get { return Count / Total; } }
public int AddDataPoint(int data)
{
Total += data;
Count++;
}
}
Теперь, чтобы поддержать ту же операцию для double, float и, возможно, других классов, которые определяют operator + и operator/, моя первая мысль состояла в том, чтобы просто использовать generics:
class Calc<T>
{
public T Count { get; private set; }
public T Total { get; private set; }
public T Average { get { return Count / Total; } }
public T AddDataPoint(T data)
{
Total += data;
Count++;
}
}
К сожалению, С# не может определить, поддерживает ли T операторы + и /, поэтому не компилирует приведенный выше фрагмент. Моя следующая мысль заключалась в том, чтобы ограничить T типами, поддерживающими этих операторов, но мои первоначальные исследования показывают, что это невозможно.
Конечно, возможно установить каждый из типов, которые я хочу поддерживать в классе, который реализует собственный интерфейс, например. IMath и ограничить T, но этот код будет называться очень много раз, и я хочу избежать накладных расходов на бокс.
Есть ли элегантный и эффективный способ решить эту проблему без дублирования кода?
Я закончил тем, что использовал Expressions, подход, обрисованный Марком Грэвеллом, который я нашел, перейдя по ссылкам из комментариев спинона.
https://jonskeet.uk/csharp/miscutil/usage/genericoperators.html
(извините, если я опубликую его сегодня, но я искал место, где можно было бы поставить этот фрагмент кода, и этот вопрос казался идеальным)
Как расширение статьи Gravell:
public static class Add<T>
{
public static readonly Func<T, T, T> Do;
static Add()
{
var par1 = Expression.Parameter(typeof(T));
var par2 = Expression.Parameter(typeof(T));
var add = Expression.Add(par1, par2);
Do = Expression.Lambda<Func<T, T, T>>(add, par1, par2).Compile();
}
}
Вы используете его как:
int sum = Add<int>.Do(x, y);
Преимущество состоит в том, что мы используем систему типов .NET для хранения различных "вариантов" Add
и при необходимости создаем новые. Поэтому при первом вызове Add<int>.Do(...)
будет создан Expression
, но если вы вызовете его второй раз, Add<int>
уже будет полностью инициализирован.
В некоторых простых тестах он 2x медленнее прямого добавления. Я думаю, это очень хорошо. Ах... он совместим с объектами, которые переопределяют operator+
. Очевидно, что создание других операций легко.
Дополнение от Мейриона Хьюза
Метод может быть расширен с помощью метакодирования, поэтому вы можете обрабатывать случаи операции T1
T2
. Например, здесь, если T1
- это число, тогда его нужно преобразовать в T2 == double
перед тем, как operator *
затем преобразует его обратно. Если T1
есть Foo
, а Foo
имеет оператор для умножения с T2 == double
, вы можете опустить преобразование. try
, catch
необходимо, потому что это самый простой способ проверить наличие T operator *(T, double)
.
public static class Scale<T>
{
public static Func<T, double, T> Do { get; private set; }
static Scale()
{
var par1 = Expression.Parameter(typeof(T));
var par2 = Expression.Parameter(typeof(double));
try
{
Do = Expression
.Lambda<Func<T, double, T>>(
Expression.Multiply(par1, par2),
par1, par2)
.Compile();
}
catch
{
Do = Expression
.Lambda<Func<T, double, T>>(
Expression.Convert(
Expression.Multiply(
Expression.Convert(par1, typeof (double)),
par2),
typeof(T)),
par1, par2)
.Compile();
}
}
}
В С# 4.0 используется подход, использующий динамику, но он не является совершенным, но он может принести новый свет.
Подробности в этом сообщении в блоге
Я нашел еще один интересный подход, который легче кодировать и отлаживать, чем ядро из дерева решений, которое я изначально использовал:
http://www.codeproject.com/KB/cs/genericnumerics.aspx
Это решение использует общие ограничения типов интересным способом обеспечения всех необходимых операций, но без каких-либо вызовов бокса или виртуального метода.