有理数 - .NET 4.0 版本(有理数计算 1)






4.97/5 (54投票s)
为 .NET 和 Silverlight 提供几乎无限精度的有理数类型。
引言
本文介绍了一种在 C# 4.0 中表示有理数的结构。在当前版本的 .NET Framework 之前,要创建一个相当有用的有理数表示将是一项重要的工作。BigInteger
的加入使得构建一个有效无限精度的有理数成为可能,这可能是一个非常有用的东西。它已与生产中使用的单元测试一起打包,代码覆盖率达到 100%。该代码可编译为标准的 .NET 程序集和 Silverlight 程序集。创建一个有用的有理数类,虽然不再像以前那样困难,但仍需要相当大的努力才能使其正确地融入框架。这并不是特别复杂,所以我写这篇文章时,是假设读者是初学者,但希望对经验丰富的开发者来说,它仍然是一个愉快的小插曲。另外,如果你打算创建任何值类型或数字类型,这应该可以作为一个有用的目录,列出你需要添加的所有元素。
路线图
这是关于有理数计算系列文章的第一篇。该系列的其他文章有:
- 有理数 - .NET 4.0 版本(本文)
- MaNet: .NET 的矩阵库
- 有理数矩阵(附带相关性能分析)... 即将推出
- 有理数导数 ... 即将推出
- 有理数超越数 ... 即将推出
可以看出,还有很多工作要做,足以占据我很长一段时间的业余时间。如果有人受到启发,我将非常乐意与他们合作,共同推动 .NET 平台上有理数计算的发展。
我们为什么要关心有理数
.NET Framework 已经有了相当多的数值类型(sbyte
、byte
、char
、short
、ushort
、int
、uint
、long
、ulong
、float
、double
、decimal
、BigInteger
、Complex
),那么为什么我们应该对一个 Rational
数值类型感到兴奋呢?考虑以下例子。我有一个函数 f(x) = 4x4 - 3x3 + 12x2 + 3x + 1,我想计算它在 x = 10 处的导数。对于这么简单的函数,没有必要使用计算机。稍作计算即可得到 15343。让我们假设我们确实需要数值近似。函数 f 的导数定义为 h --> 0 时 (f(x + h) - f(x))/h 的极限。以下是当 h 取不同值时我们得到的结果。
h | x=10 处的导数 | Error(错误) |
1.0E-001 | 15576.7740000000 | -2.3E+002 |
1.0E-002 | 15366.2357039997 | -2.3E+001 |
1.0E-003 | 15345.3221569944 | -2.3E+000 |
1.0E-004 | 15343.2322015578 | -2.3E-001 |
1.0E-005 | 15343.0232196115 | -2.3E-002 |
1.0E-006 | 15343.0023128749 | -2.3E-003 |
1.0E-007 | 15343.0001228116 | -1.2E-004 |
1.0E-008 | 15343.0002683308 | -2.7E-004 |
1.0E-009 | 15342.9937199689 | +6.3E-003 |
1.0E-010 | 15342.9573401809 | +4.3E-002 |
1.0E-011 | 15341.3566295057 | +1.6E+000 |
1.0E-012 | 15337.7186506987 | +5.3E+000 |
1.0E-013 | 15279.5109897852 | +6.3E+001 |
1.0E-014 | 16007.1067512035 | -6.6E+002 |
1.0E-015 | 29103.8304567337 | -1.4E+004 |
1.0E-016 | 0.0000000000 | +1.5E+004 |
起初,它的表现符合我们的预期。随着 h 变得越来越小,近似值变得越来越好。然而,一旦 h 达到 10-8,我们看到随着 h 变小,误差反而增加了。如果这还不够令人不安,当 h 达到 10-16 及以下时,近似值总是 0。我们可能会倾向于将问题归咎于函数或所选的点,但它们并没有任何特殊之处。我们可以尝试一种更好的方法来近似导数,而且确实有更好的方法,例如导数的高阶近似法和理查德森外推法。不幸的是,它们都遵循相同的模式:误差先是减少一段时间,然后增加,最后整个过程失败。使用 double
值存在一个根本问题,无论多么巧妙的方法都无法完全解决它。在我们灰心丧气之前,我们可以用本文中包含的 Rational
结构进行相同的评估。
h | x=10 处的导数 | Error(错误) |
1/10 | 7788387/500 | -116887/500 |
1/100 | 1920779463/125000 | -2904463/125000 |
1/1000 | 3836330539251/250000000 | -580539251/250000000 |
老实说,以它们的原始格式查看结果并不是很有启发性,所以让我们看看将结果转换回 double 后的情况。
h | x=10 处的导数 | Error(错误) |
1.0E-001 | 15576.7740000000 | -2.3E+002 |
1.0E-002 | 15366.2357040000 | -2.3E+001 |
1.0E-003 | 15345.3221570040 | -2.3E+000 |
1.0E-004 | 15343.2322015700 | -2.3E-001 |
1.0E-005 | 15343.0232200157 | -2.3E-002 |
1.0E-006 | 15343.0023220002 | -2.3E-003 |
1.0E-007 | 15343.0002322000 | -2.3E-004 |
1.0E-008 | 15343.0000232200 | -2.3E-005 |
1.0E-009 | 15343.0000023220 | -2.3E-006 |
1.0E-010 | 15343.0000002322 | -2.3E-007 |
1.0E-011 | 15343.0000000232 | -2.3E-008 |
1.0E-012 | 15343.0000000023 | -2.3E-009 |
1.0E-013 | 15343.0000000002 | -2.3E-010 |
我们可以一直这样计算下去,很长很长时间。
h | x=10 处的导数 | Error(错误) |
1.0E-203 | 15343.0000000000 | -2.3E-200 |
1.0E-204 | 15343.0000000000 | -2.3E-201 |
有理数与任何内置类型都不同。它们的精度几乎是无限的。我本想说无限,但你受限于机器上可供 .NET Framework 使用的内存量。这使得它们可以用于像上面那样的计算,而在这些计算中 single
、double
和 decimal
类型将无法胜任。这赋予了有理数以下特殊属性。
有理数没有舍入误差
有理数不会舍入,所以它们怎么会有舍入误差呢?(这句话基本上是正确的,你应该相信它。)对于固定精度类型(single
、double
和 decimal
),几乎每个数值运算都会产生一点点误差。某些操作,如对大小差异巨大的数进行加减法,或除以非常小的数,尤其容易产生舍入误差。这并不是说有理数完全没有误差。像 π 或 2 的平方根这样的无理数只能近似表示,但总的来说,情况要好得多。
有理数不会出现灾难性抵消
当两个几乎相等的数相减,结果被四舍五入为零时,就会发生灾难性抵消。总的来说,这是一个相当不利的事件,因此用“灾难性”来形容。这就是我们在使用 double 类型,当 h=10-16 时发生的情况。f(x + h) 和 f(x) 变得足够接近,以至于它们的差被解释为零。一旦发生这种情况,h 有多小都无关紧要了,它无法平衡那个微小的分子。顺便说一句,我们可能会想,使用 f(x+h)/h - f(x)/h 代替 (f(x+h)-f(x))/h 会有所帮助,但可惜的是,并没有。这种情况永远不会发生在有理数上。例如,(a + b) - a 永远等于 b,而对于固定精度类型,(a + b) - a 有可能等于 0。
有理数的行为就好像机器精度为零
这可能是只有在处理矩阵时才会关心的问题。下面是一个几乎简单到荒谬的解释。每个矩阵都有一个叫做条件数的属性,它描述了矩阵的可逆性(即可解性)有多好。大的值是不好的,被称为病态条件。用“无可救药的堕落”来形容似乎更贴切,因为这通常是一种“计算至此,放弃所有希望”的情况。简单来说,你答案中可以使用的有意义的(有效)数字位数是由矩阵的条件数乘以一个被称为机器精度的神秘数字决定的,这个数字取决于用于计算的类型的数值精度。对于 double 类型,这个值应该是 1.11E-16。(请注意,名称不当的 Double.Epsilon
常量是完全不同的东西。)从计算线性代数的角度来看,由有理数组成的矩阵在精度方面要优越得多。
如何使用
没什么好说的。只需添加程序集,然后在文件顶部加上
using Mathematics.RationalNumbers;
你的文件顶部,你就可以开始使用了。它们的行为就像你所期望的数字一样。不过,有几点提示值得注意。此外,本着完全公开的精神,目前存在一个限制,即 sin()、cos()、exp() 以及其余的超越函数尚未实现。
using 是你的好朋友
你可能想用有理数做的第一件事就是创建一个。例如,要创建 1/3,你可以使用
Rational r = new Rational(1,3) ;
但这看起来不舒服。我们可以通过使用类型转换来改进它
Rational r = (Rational)1/3 ;
但是“Rational”这个词太长了,会分散注意力。然而,这可以通过别名来改进
using Q = Mathematics.RationalNumbers.Rational;
然后我们就可以自由地使用以下代码,这似乎是最自然的,因为 Q 是有理数的标准符号
Rational r = (Q)1/3 ;
遗憾的是,省略 (Q) 仍然会让表达式编译通过,但其值将是 0 而不是 1/3,因为显式转换 (Q) 在除法之前进行,而从整数到有理数的隐式转换则在除法之后进行。
using
还有另一个用处。框架中的一些数学函数不在类型本身中,而是在 System.Math
类型中。特别地,有理数中包含了以下函数:Abs
、Ceiling
、Floor
、Max
、Min
、Pow
、Round
和 Sign
。通过占用“Math
”
using Math = Mathematics.RationalNumbers.Rational;
我们可以写出看起来更熟悉的代码
Rational r = -(Q)1/3 ;
Rational rMag = Math.Abs(-(Q)1/3 );
这个库的一个目标是允许使用 double 的库通过搜索和替换转换为使用有理数的库。
你可以用半有理数的方式编码
值得记住的是,有理数是所有现有数字类型的超集。例如,以下代码可以工作
Double d = (Double)(Rational)d;
// for all doubles except Double.PositiveInfinity,
// Double.negativeInfinity, and Double.Nan
因此,可以根据需要轻松地来回切换。
工作原理
从本质上讲,这个类型非常简单。它由两个 BigInteger
值组成,一个用于分子,一个用于分母。它遵循了我们小学学过的分数规则。考虑到如此简单,我们可能会对这个类型在没有 XML 注释的情况下长度接近 800 行感到惊讶。这么简单的东西怎么会需要这么多代码呢?
首先要注意的是,这不是你日常接触的普通类。它是一个值类型(struct)。我们不经常创建这类东西。如果我们查看微软的指南,我们就能明白为什么,因为他们指出。(斜体部分是我的注释。)
建议您对满足以下任何一个标准的类型使用结构体
- 行为像原始类型。你永远不想在线程间共享的简单类型。
- 实例大小小于 16 字节。这个限制非常严格,96 位或三个
Int32
的大小;此外,BigInteger
、Complex
和Decimal
也不遵循这一条。 - 是不可变的。值只在构造函数中设置。
- 值语义是可取的。所有字段都应该是其他值类型,而不是引用类型。
什么样的类型符合这样的描述?它们中的绝大多数(sbyte
、byte
、char
、short
、ushort
、int
、uint
、long
、ulong
、float
、double
、decimal
、BigInteger
、Complex
)用“数字”这个词来描述可能比 ValueType
更合适。其余的值类型大多是枚举。只有少数例外,比如 Point
或 DateTime
结构体。
该类的核心如下。
namespace Mathematics.RationalNumbers
{
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct Rational : IComparable, IEquatable<Rational>, IComparable<Rational>
{
private readonly BigInteger mNumerator; //Can be positive or negative
private readonly BigInteger mDenominator; // only positive
public BigInteger Numerator
{
get { return mNumerator; }
}
public BigInteger Denominator
{
get { return mDenominator; }
}
...lots of other stuff
}
}
首先要注意的是声明和特性。它的选择是为了与其他数值类型匹配,这些类型都实现了相同的接口。
[Serializable, StructLayout(LayoutKind.Sequential)]
public struct BigInteger : IFormattable, IComparable,
IComparable<biginteger>, IEquatable<biginteger>
[Serializable, StructLayout(LayoutKind.Sequential), ComVisible(true)]
public struct Double : IComparable, IFormattable, IConvertible,
IComparable<double>, IEquatable<double>
为了与现有类型保持一致,除了 IFormattable
之外,所有接口都已实现。有理数足够特殊,值得拥有自己的格式化方式,如果我们想使用格式字符串,可以分别提取分子和分母来格式化它们。这些接口将在后面详细介绍。
只有两个字段,而且它们都是只读的。将字段设置为 readonly
是一个好习惯,因为它可以保护我们免于无意中编写出改变字段值的方法。
构造函数
基本的构造函数如下
private Rational(BigInteger numerator, BigInteger denominator, bool safe)
{
if (numerator == 0)
{
mNumerator = 0;
mDenominator = 0;
return;
}
else if (denominator == 0)
{
throw new DivideByZeroException();
}
int sign = ((numerator > 0 && denominator > 0) ||
(numerator < 0 && denominator < 0)) ?1 : -1;
if (safe)
{
mNumerator = sign * BigInteger.Abs(numerator);
mDenominator = BigInteger.Abs(denominator);
}
else
{
//Potentially expensive
BigInteger gcd =
BigInteger.GreatestCommonDivisor (numerator, denominator);
mNumerator = BigInteger.Abs(numerator / gcd);
mDenominator = BigInteger.Abs(denominator / gcd);
}
}
首先注意构造函数是私有的。公共构造函数通过构造函数链调用私有构造函数。
public Rational(BigInteger numerator, BigInteger denominator):
this(numerator , denominator, false ) {}
原因是 BigInteger.GreatestCommonDivisor
是一个可能耗费性能的操作,而我们有时已经知道分子和分母处于最简形式。例如,任何有理数的倒数都已经是最简形式。既然我们知道 2/3 是最简形式,我们也知道 3/2 也是最简形式。对于这样的数字,计算可以忽略不计,但如果分子和分母都有数万位,情况就不同了。不进行 GreatestCommonDivisor
评估的安全模式是特意不对外公开的,以减少出错的可能性。
所有其他整数类型都会隐式转换为 BigInteger
,因此不需要其他构造函数来覆盖其他整数数据类型,如 int
或 long
。有理数也是除 Complex
外所有现有数值类型的超类型。每个数值类型的实例都有一个对应的有理数,因此应该有相应的构造函数。
还有一个构造函数。所有值类型都有一个隐式定义的无参数构造函数。这个构造函数将所有字段设置为 0。它也不能被重写。我们也不能在字段的声明中赋值。对于有理数类型,这意味着分子和分母都为 0 必须是一个有效的状态。
public Rational(BigInteger numerator):this(numerator , 1, true ) {}
public Rational(double value)
{
this = Rational.Parse(value.ToString("R"));
}
public Rational(Decimal value)
{
this = Rational.Parse(value.ToString());
}
事实证明,通过将浮点类型转换为字符串然后解析结果来处理它们要容易得多。这将在“ToString 和 Parse”部分详细说明。
基本操作
以下操作是该类型的基础,并在许多其他方法中使用。
IntegerPart/BigIntegerPart 和 FractionalPart
我们可能想要表达有理数的一种方式是使用它的整数部分和分数部分。例如,7/4 可以表示为 1 + 3/4。这些非常简单的方法对于提取十进制表示的数字等任务非常有用。BigIntegerpart
和 IntegerPart
实际上是等效的,仅在返回类型上有所不同。
public BigInteger BigIntegerPart
{
get
{
if (Numerator == 0)
{
return 0;
}
return Numerator / Denominator;
}
}
public Rational FractionalPart
{
get
{
return this - BigIntegerPart;
}
}
Inverse (倒数)
求一个分数的倒数是另一项常见任务。唯一特殊之处在于,为了避免无穷大,有理数 0 的倒数是 0。
public Rational Inverse
{
get
{
if (Numerator == 0)
{
return this;
}
BigInteger numerator = (Denominator != 0) ? Denominator : 1;
BigInteger denominator = Numerator;
return new Rational(numerator, denominator, true);
}
}
模
Magnitude (数量级) 定义为当有理数用科学记数法近似表示时会出现的指数。例如,100 或 123 的数量级为 2,而 .2 的数量级为 -1。这是一个非常有用的数字,以至于我为 BigInteger
类添加了一个扩展方法来生成它。
public static int Magnitude(this BigInteger value)
{
return (value !=0)?(int)Math.Floor(BigInteger.Log10(BigInteger.Abs(value))):0;
}
C# 代码可能有点晦涩,但这只是使用了以 10 为底的对数的定义。考虑一个具体的例子。
log(3.2*104) = log(3.2) + log(104)=log(3.2) + 4* log(10) =log(3.2) + 4
log(10) = 1,log(1) = 0,所以如果我们使用 Floor
方法,log(3.2) 会被取整为零。if
是必需的,因为 log(0) 是负无穷,这是我们不希望的。
有理数的情况只有轻微的不同。
public int Magnitude
{
get
{
Rational NumeratorSig =
(Rational)Numerator / BigInteger.Pow(10, Numerator.Magnitude());
Rational DenominatorSig =
(Rational)Denominator / BigInteger.Pow(10, Denominator.Magnitude());
Rational Significand =
NumeratorSig / DenominatorSig;//between 9.9999 and .100000000
if (Rational.Abs(Significand) < 1)
{
return Numerator.Magnitude() - Denominator.Magnitude() -1 ;
}
return Numerator.Magnitude() - Denominator.Magnitude() +
Significand.BigIntegerPart.Magnitude() ;
}
}
很明显,分母的数量级应该从分子的数量级中减去。需要注意的是,两个有效数字(带小数点的部分)的有理数,其上界是 9.99999...,下界是 .1。这意味着我们可能需要将结果移动一位,以确保我们遵循科学记数法的规则(第一位是 1-9,而不是 0)。
结构体(Struct)重写
Equals
值类型表示值。它们不是我们通常使用的那种对象。区分这个 7 或那个 7 没什么意义。只有值 7。我们认为 7 应该等于 7。然而,所有值类型都继承自 object
类型,它对相等有非常明确的定义。如果两个对象位于内存中的同一位置,则它们相等。由于这个定义通常不适用于值类型,特别是对于 Rational
,因此需要重写 equals。
public override bool Equals(object obj)
{
if (!(obj is Rational))
{
return false;
}
return Equals((Rational)obj);
}
public bool Equals(Rational other) // used for IEquatable<Rational>
{
if (Numerator == 0)
{
return other.Numerator == 0;
}
return Numerator == other.Numerator && Denominator == other.Denominator;
}
GetHashCode
NET Framework 的一个设计考虑是,每个对象都应该能用作 Hashtable
或 Dictionary
的键。我不太确定是否赞成使用有理数作为字典键,但这无关紧要。如果你重写了 Equals
,你就需要重写 GetHashCode
。
public override int GetHashCode()
{
return Numerator.GetHashCode() * Denominator.GetHashCode();
}
设计好的哈希函数,说得委婉一点,是一个非常复杂和高级的话题,远远超出了本文的范围。所提供的哈希函数应该能足够好地工作,如果它不被用作 Hashtable
或 Dictionary
的键,那它是什么样的就真的不重要了。
ToString 和 Parse
我们期望 ToString
方法能产生一个人类可读的数字表示。为了实现这一点而不是返回类型名称,必须重写 ToString
。
public override string ToString()
{
if (mNumerator == 0)
{
return "0";
}
if (mDenominator == 1)
{
return mNumerator.ToString();
}
return mNumerator.ToString() + "/" + mDenominator.ToString();
}
我们期望有一个 Parse
函数,它可以接受 ToString
的结果并返回数字。这个方法会更复杂,因为它需要能够解析所有合理的数字字符串表示形式。
public static Rational Parse(String s)
{
int periodIndex = s.IndexOf(".");
int eIndeix = s.IndexOf("E");
int slashIndex = s.IndexOf("/");
if (periodIndex == -1 && eIndeix ==-1 && slashIndex == -1)
// an integer such as 7
{
return new Rational( BigInteger.Parse (s));
}
if (periodIndex == -1 && eIndeix == -1 && slashIndex != -1)
// a fraction such as 3/7
{
return new Rational(BigInteger.Parse(s.Substring(0, slashIndex)),
BigInteger.Parse(s.Substring(slashIndex +1)));
}
if (eIndeix == -1)// no scientific Notation such as 5.997
{
BigInteger n = BigInteger.Parse(s.Replace(".", ""));
BigInteger d = (BigInteger)Math.Pow(10, s.Length - periodIndex-1);
return new Rational(n, d);
}
else //In scientific notation such as 2.4556E-2
{
int characteristic = int.Parse(s.Substring(eIndeix + 1));
BigInteger ten = 10;
BigInteger numerator =
BigInteger.Parse(s.Substring(0, eIndeix).Replace(".", ""));
BigInteger denominator =
new BigInteger(Math.Pow(10, eIndeix - periodIndex - 1));
BigInteger charPower =BigInteger.Pow( ten,Math.Abs(characteristic));
if (characteristic > 0)
{
numerator = numerator * charPower;
}
else
{
denominator = denominator * charPower;
}
return new Rational(numerator, denominator);
}
}
扩展的 ToString
基本的 ToString
将作为一种字符串序列化的形式,但我们通常想要一个精确到特定小数位数的 Rational
的字符串表示。有两个这样的函数。
public string ToString(EApproxmationType approximation,int places,bool padWithZeroes){...}
public string ToScientific(int places, bool padwithZeroes){...}
这些函数分别对应于标准记数法或科学记数法的格式化。它们也是该类型中最复杂的例程。这两种方法都使用了返回一串数字的 Digits
方法。
internal static List<string> Digits(Rational r, int n)
{
List<string> digits = new List<string>();
//Divide into integral and fractional parts
BigInteger IntPart = Rational.Abs(r).BigIntegerPart;
Rational fracPart = Rational.Abs(r).FractionalPart;
int intplaces = IntPart.Magnitude() + 1;
char[] chars = IntPart.ToString().ToCharArray();
for (int i = 0; i < Math.Min(intplaces , n); i++)
//get digits from integral part
{
if (digits.Count > 0 || chars[i] != '0')// first digit not zero
{
digits.Add(chars[i].ToString());
}
}
while (digits.Count() < n)//get digits from fractional part
{
if (fracPart == 0)
{
break;
}
fracPart *=10;
if (digits.Count > 0 || fracPart.IntegerPart != 0)
// first digit not zero
{
digits.Add(fracPart.IntegerPart.ToString());
}
fracPart = fracPart.FractionalPart;
}
return digits;
}
该例程从数字的十进制表示中最多选择 n 位数字。有理数首先被分成整数部分和分数部分。4/3 会被分成 1 和 1/3。先从 IntegerPart
中取数字,然后从 FractionalPart
中取,直到获得足够多的数字,或者分数部分终止。
扩展的 ToString
负责确定需要从 Digits
方法请求多少位数字以及在哪里放置小数点。在获取数字后,它们被组合成一个字符串,并在必要时进行填充。
public string ToString(EApproxmationType approximation,int places,bool padWithZeroes)
{
StringBuilder sb = new StringBuilder();
if (this.Numerator < 0)
{
sb.Append("-");
}
int pointIndex =this.BigIntegerPart.Magnitude() + 1;
if (this.BigIntegerPart == 0)
{
sb.Append("0.");
pointIndex = -1;
}
Rational working = this;
while (working.BigIntegerPart == 0)
{
working *= 10;
if (working.BigIntegerPart == 0)
{
sb.Append("0");
}
}
bool sf = approximation == EApproxmationType.SignificantFigures;
int digitsNeeded = (sf) ? Math.Max(this.BigIntegerPart.Places(), places) :
this.BigIntegerPart.Places() + places;
int digitsToExtract = (sf) ? places : places + this.BigIntegerPart.Places();
List<string> digits = Digits(working, digitsToExtract);
if (digits.Count < digitsToExtract && !padWithZeroes)
{
digitsNeeded -= (digitsToExtract - digits.Count);
}
for (int i = 0; i < digits.Count; i++)
{
if (i ==pointIndex)
{
sb.Append(".");
}
sb.Append(digits[i]);
}
for (int i = digits.Count(); i < digitsNeeded; i++)
{
if (i == pointIndex)
{
sb.Append(".");
}
sb.Append("0");
}
return sb.ToString();
}
ToScientific
类似,只是它将数字转换成科学记数法。小数点的放置更容易,因为它总是在第一位数字之后。此外,将有理数转换为特定小数位数或特定有效数字位数之间没有区别。还有一个额外的复杂性,即确定正确的指数。这是通过非常有用的 Magnitude
属性来完成的,后面会讲到。
public string ToScientific(int places, bool padwithZeroes)
{
StringBuilder sb = new StringBuilder();
if (this.Numerator < 0)
{
sb.Append("-");
}
List<string> digits = Digits(this,places);
BigInteger b = this.BigIntegerPart;
Rational fractional = Rational.Abs(this.FractionalPart);
sb.Append(digits[0]);
if (digits.Count > 1 || (places > 1 && padwithZeroes))
{
sb.Append(".");
}
for (int i = 1; i < digits.Count; i++)
{
sb.Append(digits[i]);
}
if (padwithZeroes)
{
for (int i = digits.Count; i < places; i++)
{
sb.Append("0");
}
}
sb.Append("E");
if (this.BigIntegerPart != 0)
{
sb.Append("+");
sb.Append(this.Magnitude);
}
else
{
sb.Append(this.Magnitude);
}
return sb.ToString();
}
我曾犹豫是否将这段代码放在文章正文中。它不是特别容易理解,甚至也没有什么启发性。然而,它是在与单元测试来回切换时创建的典型代码。此外,它还说明了对于这些半原始数据类型,最困难的任务是转换和格式化,而不是创建该数据类型所要执行的实际操作。
运算符重载
数值类型通常使用运算符而不是方法。我们不使用加法方法来给数字相加,而是使用加法运算符。因此,作为 Rational
类型的一部分,需要重载大量的运算符,这应该不足为奇。
等于和不等于:==, !=
如果我们重写了 Equals
方法,我们也应该重写 ==
和 !=
运算符。每个人都期望 ==
和 Equals
的含义相同。
public static bool operator ==(Rational r1, Rational r2)
{
return r1.Equals(r2);
}
public static bool operator !=(Rational r1, Rational r2)
{
return ! r1.Equals(r2);
}
排序运算符: <, <=, >, >=
我们还期望能够使用排序运算符,无论是在有理数之间,还是在已转换为有理数的其他数值类型之间。作为这类运算符的一个示例,我们有 <
运算符。
public static bool operator <(Rational r1, Rational r2)
{
return r1.CompareTo(r2)< 0
}
数学运算符: +, -, *, /, %
数学运算符遵循我们在小学学过的分数规则。加法和减法非常相似。应该注意到,零测试是必要的,因为所有字段都为零需要是我们的结构体的一个有效状态。
public static Rational operator +(Rational r1, Rational r2)
{
if (r1 == 0){ return r2;}
if (r2 == 0){ return r1;}
if (r1.Denominator == r2.Denominator)
{
return new Rational(r1.Numerator + r2.Numerator, r1.Denominator, false);
}
return new Rational(r1.Numerator * r2.Denominator +
r2.Numerator * r1.Denominator,
r1.Denominator * r2.Denominator, false);
}
在定义运算符时,我们不局限于所讨论的类型。例如,我可能想为 int
和 Rational
的相加添加一个运算符。
public static Rational operator +(Rational r1, int i)
{
if (r1 == 0) { return i; }
if (i == 0) { return r1; }
return new Rational(r1.Numerator + i*r1.Denominator,
r1.Denominator, true);
}
public static Rational operator +(int i, Rational r1)
{
if (r1 == 0) { return i; }
if (i == 0) { return r1; }
return new Rational(r1.Numerator + i * r1.Denominator,
r1.Denominator, true);
}
乍一看,这似乎是多余的,因为我们将使整数隐式转换为有理数。这个版本的运算符存在的原因是,与整数相加不会使我的分数脱离最简形式。因此我们可以通过不检查来节省时间。此外,建议同时定义两个方向的运算符。我们无法使用阿贝尔(交换)属性来使一个定义对两个方向都有效。
乘法和除法的运算符非常相似。唯一需要稍加注意的是处理零的情况。
public static Rational operator *(Rational r1, Rational r2)
{
if (r1 == 0 || r2 == 0)
{
return 0;
}
return new Rational(r1.Numerator * r2.Numerator,
r1.Denominator * r2.Denominator, false);
}
public static Rational operator /(Rational r1, Rational r2)
{
if (r1 == 0)
{
return 0;
}else if (r2 == 0)
{
throw new DivideByZeroException();
}
return new Rational(r1.Numerator * r2.Denominator,
r1.Denominator * r2.Numerator, false);
}
类型转换运算符: (Rational), (Double)...
需要考虑两种类型转换运算符,即转换为有理数和从有理数转换。转换为有理数非常容易处理,因为我们已经有了接受所有适当类型的构造函数。它们都是相同的,只是调用了该类型的众多构造函数之一。由于每个其他数值类型都有一个等效的有理数,因此到有理数的转换是隐式的。
static public implicit operator Rational(int value)
{
return new Rational(value);
}
从有理数转换为其他类型只稍微复杂一些。有两种情况,基于整数的类型和浮点类型。对于基于整数的类型,取 BigIntegerPart
并将其转换为适当的类型。
static public explicit operator long (Rational value)
{
return (long)value.BigIntegerPart;
}
到浮点类型的转换是通过生成有理数的字符串近似值,然后使用该类型的 Parse
方法来完成的。
static public explicit operator Single(Rational value)
{
return Single.Parse(value.ToScientific(8, false));
}
static public explicit operator double(Rational value)
{
return Double.Parse(value.ToScientific(17, false));
}
static public explicit operator decimal(Rational value)
{
return decimal.Parse(value.ToString(
EApproxmationType.DecimalPlaces, 29,false));
}
接口
除了 Complex
之外,所有内置的数值类型都遵循某些接口。这些接口是 IFormattable
、IComparable
、IComparable<T>
和 IEquatable<T>
。大多数还实现了 IConvertible
。Rational
类型实现了 IComparable
、IComparable<Rational>
和 IEquatable<Rational>
。
IEquatable<Rational>
实现 IEquatable<T>
是为了管理值类型相等性而应完成的三个任务中的最后一个。另外两个是重写 Equals
方法和 ==
、!=
运算符。实现此接口对于在泛型集合(如字典)中的正确行为是必需的。IEquatable<T>
接口包含单个方法 Equals<T>
。
public bool Equals(Rational other)
{
if (Numerator == 0)
{
return other.Numerator == 0;
}
return Numerator == other.Numerator && Denominator == other.Denominator;
}
IComparer 和 IComparer<Rational>
这两个接口对于任何数值类型都是必不可少的,因为它们管理排序。List.Sort
和其他泛型集合使用 IComparer<T>
,而非泛型集合使用 IComparer
。为了保持一致性,这两个接口以及运算符 >
、<
、>=
、<=
都应该一起修改。IComparer
接口包含 CompareTo(object obj)
,而 IComparer<Rational>
包含 CompareTo(Rational other)
。
public int CompareTo(object obj)
{
if (!(obj is Rational))
{
throw new ArgumentException();
}
return this.CompareTo( (Rational)obj);
}
public int CompareTo(Rational other)
{
if (this == other)
{
return 0;
}
if (Sign(this) < Sign(other))
{
return -1;
}
if (Sign(this) > Sign(other))
{
return 1;
}
if (Numerator >= other.Numerator && Denominator <= other.Denominator )
{
return 1;
}
if (Numerator <= other.Numerator && Denominator >= other.Denominator )
{
return -1;
}
return Sign(Numerator * other.Denominator - other.Numerator *Denominator );
}
System.Math 函数
为了对大多数数值类型执行一些常见的数学运算,我们需要使用 System.Math
类型中的一系列方法。由于这是一个静态类型,无法使用扩展方法对其进行扩展。因此,Rational
类型遵循了 Decimal
和 BigInteger
类型设定的先例,即在类中包含与这些方法匹配的静态方法。这样做是为了保持一致性,即使其中一些方法(如 Sign
)可能更自然地表示为属性。
度量方法:Abs, Sign
public static Rational Abs(Rational value)
{
return new Rational(BigInteger.Abs(value.Numerator),
BigInteger.Abs( value.Denominator), true);
}
public static int Sign(Rational value)
{
return value.Numerator.Sign ;
}
比较方法:Min, Max
public static Rational Min(Rational val1, Rational val2)
{
return val1 <= val2 ? val1 : val2;
}
public static Rational Max(Rational val1, Rational val2)
{
return val2 >= val1 ? val2 : val1;
}
取整方法: Ceiling, Floor, Round
Ceiling
、Floor
和 Round
都用于将值强制转换为最接近的整数。这三者彼此非常相似。
public static Rational Floor(Rational value)
{
BigInteger bi = value.BigIntegerPart;
if (value == (Rational)bi)
{
return value;
}
else if (value >= 0)
{
return (Rational)bi ;
}
return (Rational)bi - 1;
}
对于有理数,我们可能还希望将值强制转换为其他有理数值。例如,我们可能希望相对于三分之一来计算 Floor
。
public static Rational Floor(Rational value, BigInteger denominator)
{
return Floor(value * denominator) / denominator;
}
public static Rational Floor(Rational value, Rational denominator)
{
return Floor(value / denominator) * denominator;
}
结论
我希望这个新类型能对您有所帮助。尽管我怀疑它会出现在下一个服务包或框架版本中,但我敢打赌,其接口将与本文介绍的非常相似。这个项目最初的目的是用于一个有理数值的滑块控件,但正如经常发生的那样,项目超出了其最初的范围。该项目的一些初步方法包含在静态的 RationalCollectionHelpers
类中。这个类具有相当大的潜力,所以请继续关注更多激动人心的数值计算内容。
更新
- 2010年7月3日:对
GetHashCode
和CompareTo
方法进行了更新。还进行了一些其他小的代码改进。 - 2010年7月22日:源代码现在可以为 .NET 和 Silverlight 编译。请注意,由于 Silverlight 版本的
BigInteger
没有Parse
方法,这需要一些小的更改。真奇怪。另外,对CompareTo
方法进行了修正。