Подтвердить что ты не робот

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

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

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

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

4b9b3361

Ответ 1

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

    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> всегда будет одинаковым (также плохо). Я бы предположил, что вы просто не делаете этого в С# и используете F # или какой-либо другой язык, который лучше подходит для этого уровня абстракции.

Изменить: Исправлены определения добавления и вычитания для Fraction<T>. Еще одна интересная и простая задача - реализовать MathProvider, который работает с абстрактным синтаксическим деревом. Эта идея сразу же указывает на такие вещи, как автоматическая дифференциация: http://conal.net/papers/beautiful-differentiation/

Ответ 3

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

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

Ответ 4

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

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

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

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