使用C#中的泛型创建一个数学库

33

有没有使用泛型创建数学库的可行方法,使其不依赖于所选择的基础类型来存储数据?

换句话说,假设我想编写一个分数类。该分数可以由两个整数、两个双精度浮点数或其他数字表示。重要的是四则运算已定义良好。因此,我希望能够编写 Fraction<int> frac = new Fraction<int>(1,2) 和/或 Fraction<double> frac = new Fraction<double>(0.1, 1.0)

不幸的是,没有接口表示四个基本运算符(+、-、*、/)。有人找到了一种可行的实现方式吗?


C# 11 / .NET 7 给我们提供了这样的功能:"... where T : INumber<T>" ... - undefined
6个回答

34
这里提供一种相对轻松的方法来抽象化运算符。
    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/

1
一般来说,我认为MathProvider应该被制作成接口,而Subtract则应该成为普通的接口方法,或者可以实现为扩展方法。但另一方面,这将禁止对其进行覆盖。 - dalle
我对你的解决方案的性能感到疑惑... 只有当所有内容都内联时,它才能正常工作... - AK_
1
你需要完全重新实现System.Math。 - AK_
根据John D. Cook的回答(“将int除以int将产生整数除法并产生错误结果”),您可能希望添加一个public abstract double DivideToDouble(T a, T b) - Tobias Knauss
@fryguybob,你能展示一下如何使用它的例子吗? - johnny 5

7

1
那个解决方案,以及其他可用的方案(比如使用Emit等)并不是很干净,所以那不是我想要的。无论如何还是谢谢 :) - Sklivvz

4
这里有一个与通用类型相关的微妙问题。假设算法涉及除法,比如高斯消元解方程组。如果您传入整数,将会得到错误的答案,因为您将执行整数除法。但是,如果您传入具有整数值的双精度参数,则将获得正确的答案。
同样的情况也发生在平方根中,比如乔列斯基分解。对整数矩阵进行因式分解会出错,而对具有整数值的双精度矩阵进行因式分解则没有问题。

或许,整数对这样的算法来说本质上是不合适的。 “正确”的解决方案不太可能是一个整数值。当结果不是整数时,你会发现这一点,“偶然具有整数值的双精度参数”。一般来说,中间计算应该总是用实数(浮点数,双精度)类型来进行。但我同意这值得注意。 - undefined

2

首先,你的类应该将泛型参数限制为基元类型(public class Fraction where T : struct, new() )。

其次,你可能需要创建隐式转换重载,以便可以处理从一种类型到另一种类型的转换而不会让编译器报错。

第三,你还可以重载四个基本运算符,以使接口在组合不同类型的分数时更加灵活。

最后,你必须考虑如何处理算术溢出和下溢。一个好的库将非常明确地说明它如何处理溢出;否则,你不能信任不同分数类型的操作结果。


1
问题在于我甚至无法进行这样的求和,因为结构体没有定义加法运算符。 - Sklivvz
“通过在类和结构中包含运算符声明,可以引入用户定义的实现。” - user1228

2

.NET 7引入了一个新功能 - 泛型数学(在这里这里阅读更多),它基于static abstract接口方法的加法。该功能引入了许多接口,允许在数字类型和/或数学操作上进行通用抽象:

class Fraction<T> :
    IAdditionOperators<Fraction<T>, Fraction<T>, Fraction<T>>,
    ISubtractionOperators<Fraction<T>, Fraction<T>, Fraction<T>>,
    IDivisionOperators<Fraction<T>, Fraction<T>, Fraction<T>>
    where T : INumber<T>
{
    public T Numerator { get; }
    public T Denominator { get; }

    public Fraction(T numerator, T denominator)
    {
        Numerator = numerator;
        Denominator = denominator;
    }

    public static Fraction<T> operator +(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator + right.Numerator * left.Denominator,
            left.Denominator * right.Denominator);

    public static Fraction<T> operator -(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator - right.Numerator * left.Denominator,
            left.Denominator * right.Denominator);

    public static Fraction<T> operator /(Fraction<T> left, Fraction<T> right) =>
        new(left.Numerator * right.Denominator, left.Denominator * right.Numerator);
}

1
这里提供的其他方法也可以使用,但它们会对原始运算符产生高性能影响。我想在这里发布这篇文章,为需要最快速而不是最漂亮的方法的人提供帮助。
如果你想进行通用数学计算而不付出性能代价,那么不幸的是,这就是做法:
[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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接