Есть ли какой-либо реальный способ использования дженериков для создания математической библиотеки, которая не зависит от базового типа, выбранного для хранения данных?
Другими словами, предположим, что я хочу написать класс Fraction. Дробь может быть представлена двумя целыми или двумя двойными числами или чем-то еще. Важно то, что основные четыре арифметических операции четко определены. Итак, я хотел бы иметь возможность писать Fraction<int> frac = new Fraction<int>(1,2) и / или Fraction<double> frac = new Fraction<double>(0.1, 1.0).
К сожалению, нет интерфейса, представляющего четыре основные операции (+, -, *, /). Кто-нибудь нашел работоспособный, осуществимый способ реализовать это?





Во-первых, ваш класс должен ограничить общий параметр примитивами (открытый класс Fraction, где T: struct, new ()).
Во-вторых, вам, вероятно, потребуется создать неявные перегрузки приведения, чтобы вы могли обрабатывать преобразование из одного типа в другой без крика компилятора.
В-третьих, вы также можете перегрузить четыре основных оператора, чтобы сделать интерфейс более гибким при объединении дробей разных типов.
Наконец, вы должны учитывать, как вы справляетесь с арифметическими переполнениями и потерями. Хорошая библиотека будет предельно ясна в том, как она обрабатывает переполнения; в противном случае нельзя доверять результату операций с разными типами дробей.
Я считаю, что это ответ на ваш вопрос:
http://www.codeproject.com/KB/cs/genericnumerics.aspx
Это и другие доступные решения (например, с использованием Emit) на самом деле не совсем чисты, так что это не то, что я искал. Но все равно спасибо :)
Вот способ абстрагироваться от операторов, который относительно безболезнен.
abstract class MathProvider<T>
{
public abstract T Divide(T a, T b);
public abstract T Multiply(T a, T b);
public abstract T Add(T a, T b);
public abstract T Negate(T a);
public virtual T Subtract(T a, T b)
{
return Add(a, Negate(b));
}
}
class DoubleMathProvider : MathProvider<double>
{
public override double Divide(double a, double b)
{
return a / b;
}
public override double Multiply(double a, double b)
{
return a * b;
}
public override double Add(double a, double b)
{
return a + b;
}
public override double Negate(double a)
{
return -a;
}
}
class IntMathProvider : MathProvider<int>
{
public override int Divide(int a, int b)
{
return a / b;
}
public override int Multiply(int a, int b)
{
return a * b;
}
public override int Add(int a, int b)
{
return a + b;
}
public override int Negate(int a)
{
return -a;
}
}
class Fraction<T>
{
static MathProvider<T> _math;
// Notice this is a type constructor. It gets run the first time a
// variable of a specific type is declared for use.
// Having _math static reduces overhead.
static Fraction()
{
// This part of the code might be cleaner by once
// using reflection and finding all the implementors of
// MathProvider and assigning the instance by the one that
// matches T.
if (typeof(T) == typeof(double))
_math = new DoubleMathProvider() as MathProvider<T>;
else if (typeof(T) == typeof(int))
_math = new IntMathProvider() as MathProvider<T>;
// ... assign other options here.
if (_math == null)
throw new InvalidOperationException(
"Type " + typeof(T).ToString() + " is not supported by Fraction.");
}
// Immutable impementations are better.
public T Numerator { get; private set; }
public T Denominator { get; private set; }
public Fraction(T numerator, T denominator)
{
// We would want this to be reduced to simpilest terms.
// For that we would need GCD, abs, and remainder operations
// defined for each math provider.
Numerator = numerator;
Denominator = denominator;
}
public static Fraction<T> operator +(Fraction<T> a, Fraction<T> b)
{
return new Fraction<T>(
_math.Add(
_math.Multiply(a.Numerator, b.Denominator),
_math.Multiply(b.Numerator, a.Denominator)),
_math.Multiply(a.Denominator, b.Denominator));
}
public static Fraction<T> operator -(Fraction<T> a, Fraction<T> b)
{
return new Fraction<T>(
_math.Subtract(
_math.Multiply(a.Numerator, b.Denominator),
_math.Multiply(b.Numerator, a.Denominator)),
_math.Multiply(a.Denominator, b.Denominator));
}
public static Fraction<T> operator /(Fraction<T> a, Fraction<T> b)
{
return new Fraction<T>(
_math.Multiply(a.Numerator, b.Denominator),
_math.Multiply(a.Denominator, b.Numerator));
}
// ... other operators would follow.
}
Если вам не удастся реализовать тип, который вы используете, вы получите ошибку во время выполнения, а не во время компиляции (это плохо). Определение реализаций MathProvider<T> всегда будет одинаковым (тоже плохим). Я бы посоветовал вам просто избегать этого на C# и использовать F# или другой язык, более подходящий для этого уровня абстракции.
Редактировать: Исправлены определения сложения и вычитания для Fraction<T>.
Еще одна интересная и простая вещь - реализовать MathProvider, работающий с абстрактным синтаксическим деревом. Эта идея сразу же указывает на такие вещи, как автоматическое дифференцирование: http://conal.net/papers/beautiful-differentiation/
В общем, я думаю, что MathProvider должен быть преобразован в интерфейс, а Subtract - в обычный метод интерфейса, или он может быть реализован как метод расширения. Это, с другой стороны, не позволило бы его переопределить.
Мне интересно узнать о производительности вашего решения ... Он отлично работает, только если все встроено ...
также вам нужно полностью заново реализовать System.Math
Основываясь на приведенном ниже ответе Джона Д. Кука («деление int на int будет целочисленным делением и даст неправильный результат»), вы можете добавить public abstract double DivideToDouble (T a, T b)
@fryguybob, можешь ли ты показать пример того, как его использовать?
Вот тонкая проблема, которая возникает с универсальными типами. Предположим, алгоритм включает деление, скажем, исключение Гаусса для решения системы уравнений. Если вы передадите целые числа, вы получите неправильный ответ, потому что вы будете выполнять деление целое число. Но если вы передадите двойные аргументы, которые имеют целочисленные значения, вы получите правильный ответ.
С квадратными корнями происходит то же самое, что и в факторизации Холецкого. Факторинг целочисленной матрицы будет неправильным, тогда как факторинг матрицы двойных чисел, которые имеют целочисленные значения, будет прекрасным.
Другие подходы здесь будут работать, но они имеют большое влияние на производительность по сравнению с необработанными операторами. Я решил разместить это здесь для тех, кому нужен самый быстрый, а не самый красивый подход.
Если вы хотите выполнить общую математику без потери производительности, то, к сожалению, это способ сделать это:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T IncrementToMax(T value)
{
if (typeof(T) == typeof(char))
return (char)(object)value! < char.MaxValue ? (T)(object)(char)((char)(object)value + 1) : value;
if (typeof(T) == typeof(byte))
return (byte)(object)value! < byte.MaxValue ? (T)(object)(byte)((byte)(object)value + 1) : value;
// ...rest of the types
}
Я знаю, это выглядит ужасно, но при использовании этого метода код будет работать максимально быстро. JIT оптимизирует все преобразования и условные переходы.
Вы можете прочитать объяснение и некоторые дополнительные важные детали здесь: http://www.singulink.com/codeindex/post/generic-math-at-raw-operator-speed
Проблема в том, что я даже не могу делать такие суммы, потому что в структурах не определен оператор сложения.