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

StackOverflow https://stackoverflow.com/questions/63694

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

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

    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/

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top