Создание библиотеки Math с использованием Generics в C # - PullRequest
26 голосов
/ 15 сентября 2008

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

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

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

Ответы [ 4 ]

28 голосов
/ 15 сентября 2008

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

    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/

6 голосов
/ 15 сентября 2008

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

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

3 голосов
/ 09 декабря 2010

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

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

2 голосов
/ 15 сентября 2008

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

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...