在 C 中使用泛型创建数学库#

Creating a Math library using Generics in C#

提问人:Sklivvz 提问时间:9/15/2008 最后编辑:QuonuxSklivvz 更新时间:11/17/2022 访问量:34081

问:

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

换句话说,假设我想编写一个 Fraction 类。分数可以用两个整数或两个双精度或其他方式表示。重要的是,基本的四个算术运算是明确定义的。所以,我希望能够写和/或.Fraction<int> frac = new Fraction<int>(1,2)Fraction<double> frac = new Fraction<double>(0.1, 1.0)

遗憾的是,没有表示四个基本操作(+,-,*,/)的接口。有没有人找到一种可行的方法来实现这一点?

C# 泛型 接口 数学

评论

0赞 ToolmakerSteve 10/25/2023
stackoverflow.com/a/74796391/199364C# 11 / .NET 7 为我们提供了这个:“......其中 T : INumber<T>“ ...

答:

2赞 user1228 9/15/2008 #1

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

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

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

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

评论

1赞 Sklivvz 9/15/2008
问题是我什至不能做这样的求和,因为结构没有定义加法运算符。
0赞 9/16/2008
msdn.microsoft.com/en-us/library/aa691324(VS.71).aspx“可以通过在类和结构中包含运算符声明来引入用户定义的实现”
7赞 Peter 9/15/2008 #2

我相信这回答了你的问题:

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

评论

1赞 Sklivvz 9/15/2008
该解决方案和其他可用的解决方案(例如使用 Emit)根本不干净,所以这不是我想要的。但无论如何都要感谢:)
34赞 fryguybob 9/15/2008 #3

这是一种相对轻松的抽象运算符的方法。

    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.
    }

如果无法实现所使用的类型,则会在运行时而不是在编译时失败(这很糟糕)。实现的定义总是相同的(也很糟糕)。我建议您避免在 C# 中执行此操作,而使用 F# 或其他更适合此抽象级别的语言。MathProvider<T>

编辑:修复了 的加法和减法的定义。 另一个有趣且简单的事情是实现一个在抽象语法树上运行的 MathProvider。这个想法立即指向了自动微分之类的事情:http://conal.net/papers/beautiful-differentiation/Fraction<T>

评论

1赞 dalle 12/6/2008
按照一般的方式,我认为MathProvider应该做成一个接口,减法做成一个普通的接口方法,或者它可以作为一个扩展方法实现。另一方面,这将不允许覆盖它。
0赞 AK_ 7/2/2013
我想知道您的解决方案的性能......只有当一切都内联时,它才能很好地工作......
1赞 AK_ 7/2/2013
此外,您还需要完全重新实现 System.Math
0赞 Tobias Knauss 7/18/2018
根据下面 John D. Cook 的回答(“int 除以 int 将是一个整数除法并产生错误的结果”),您可能需要添加一个public abstract double DivideToDouble (T a, T b)
0赞 johnny 5 2/26/2021
@fryguybob您能否举例说明如何使用它
4赞 John D. Cook 12/9/2010 #4

这是泛型类型带来的一个微妙问题。假设一个算法涉及除法,比如用高斯消元法来求解方程组。如果你传入整数,你会得到一个错误的答案,因为你将进行整数除法。但是,如果你传入恰好有整数值的双参数,你会得到正确的答案。

平方根也会发生同样的事情,就像在 Cholesky 因式分解中一样。因式分解整数矩阵会出错,而对恰好具有整数值的双精度矩阵进行因式分解会很好。

评论

0赞 ToolmakerSteve 10/25/2023
可以说,对于这种算法来说,整数本质上是错误的。“正确”的解决方案不太可能是整数值。当结果不是整数时,你会看到“碰巧有整数值的双精度参数”。通常,中间计算应始终使用实数(浮点数、双精度数)类型进行。但我同意这是值得注意的。
1赞 Mike Marynowski 12/2/2020 #5

此处的其他方法将起作用,但它们对原始运算符的性能影响很大。我想我会在这里发布这个,供需要最快而不是最漂亮方法的人使用。

如果你想在不付出性能损失的情况下进行通用数学运算,那么不幸的是,这是这样做的方法:

[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

2赞 Guru Stron 11/17/2022 #6

.NET 7 引入了一项新功能 - 泛型数学(在此处此处阅读更多内容),它基于添加静态抽象接口方法。此功能引入了许多接口,这些接口允许对数字类型和/或数学运算进行一般抽象:

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);
}