Создание математической библиотеки с использованием Generics в C#

Есть ли какой-либо реальный способ использования дженериков для создания математической библиотеки, которая не зависит от базового типа, выбранного для хранения данных?

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

К сожалению, нет интерфейса, представляющего четыре основные операции (+, -, *, /). Кто-нибудь нашел работоспособный, осуществимый способ реализовать это?

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
29
0
27 833
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

Во-первых, ваш класс должен ограничить общий параметр примитивами (открытый класс Fraction, где T: struct, new ()).

Во-вторых, вам, вероятно, потребуется создать неявные перегрузки приведения, чтобы вы могли обрабатывать преобразование из одного типа в другой без крика компилятора.

В-третьих, вы также можете перегрузить четыре основных оператора, чтобы сделать интерфейс более гибким при объединении дробей разных типов.

Наконец, вы должны учитывать, как вы справляетесь с арифметическими переполнениями и потерями. Хорошая библиотека будет предельно ясна в том, как она обрабатывает переполнения; в противном случае нельзя доверять результату операций с разными типами дробей.

Проблема в том, что я даже не могу делать такие суммы, потому что в структурах не определен оператор сложения.

Sklivvz 15.09.2008 19:21
msdn.microsoft.com/en-us/library/aa691324(VS.71).aspx "user-defined implementations can be introduced by including operator declarations in classes and structs
user1228 15.09.2008 20:46

Я считаю, что это ответ на ваш вопрос:

http://www.codeproject.com/KB/cs/genericnumerics.aspx

Это и другие доступные решения (например, с использованием Emit) на самом деле не совсем чисты, так что это не то, что я искал. Но все равно спасибо :)

Sklivvz 15.09.2008 19:31
Ответ принят как подходящий

Вот способ абстрагироваться от операторов, который относительно безболезнен.

    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 - в обычный метод интерфейса, или он может быть реализован как метод расширения. Это, с другой стороны, не позволило бы его переопределить.

dalle 06.12.2008 00:04

Мне интересно узнать о производительности вашего решения ... Он отлично работает, только если все встроено ...

AK_ 02.07.2013 11:37

также вам нужно полностью заново реализовать System.Math

AK_ 02.07.2013 11:38

Основываясь на приведенном ниже ответе Джона Д. Кука («деление int на int будет целочисленным делением и даст неправильный результат»), вы можете добавить public abstract double DivideToDouble (T a, T b)

Tobias Knauss 18.07.2018 16:40

@fryguybob, можешь ли ты показать пример того, как его использовать?

johnny 5 26.02.2021 07:02

Вот тонкая проблема, которая возникает с универсальными типами. Предположим, алгоритм включает деление, скажем, исключение Гаусса для решения системы уравнений. Если вы передадите целые числа, вы получите неправильный ответ, потому что вы будете выполнять деление целое число. Но если вы передадите двойные аргументы, которые имеют целочисленные значения, вы получите правильный ответ.

С квадратными корнями происходит то же самое, что и в факторизации Холецкого. Факторинг целочисленной матрицы будет неправильным, тогда как факторинг матрицы двойных чисел, которые имеют целочисленные значения, будет прекрасным.

Другие подходы здесь будут работать, но они имеют большое влияние на производительность по сравнению с необработанными операторами. Я решил разместить это здесь для тех, кому нужен самый быстрый, а не самый красивый подход.

Если вы хотите выполнить общую математику без потери производительности, то, к сожалению, это способ сделать это:

[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

Другие вопросы по теме