65.9K
CodeProject 正在变化。 阅读更多。
Home

C#/Saltarelle 中的定点数数据类型类

starIconstarIconstarIconemptyStarIconemptyStarIcon

3.00/5 (1投票)

2012年12月5日

CPOL

5分钟阅读

viewsIcon

19142

在 Saltarelle 中实现定点数数值类型。

简介     

Saltarelle 这样的 C# 到 JavaScript 编译器在 .NET 开发者中越来越受欢迎,因为它们允许编写针对 Web 平台(HTML5/CSS3/JS)的 C# 应用程序。

不幸的是,JavaScript 缺乏定点数数值数据类型,这使得该平台不适合编写客户端业务应用程序。

本文展示了如何利用 C# 语言和 Saltarelle 编译器的强大功能来实现一个定点数数据类型类,该类可以透明地映射到 JavaScript 中,并用作 .NET Decimal 类型的良好替代品,应用于应用程序中。

问题所在 

JavaScript 中唯一可用的数值数据类型是浮点数类型(“Number”)。浮点数适合进行科学计算,但在存储包含小数(美分)的货币值时,其精度不足。

考虑以下 JavaScript 代码

var a = 0.1; 
a = a + 0.2;
if(a==0.3) { window.alert('equal!'); }
else       { window.alert('NOT equal!'); }

您可能不会相信,但结果是“不相等”!

这是因为浮点数表示法无法准确存储 0.1 的值,只能存储一个非常接近的近似值。这种微小的差异可能会累积并导致不可预测的错误,表现为检查失败(如示例所示)或值不正确。

当然,这在业务应用程序中是不可接受的,因为最基本的要求是计算机必须正确计算!

这个问题通常通过采用定点表示法来解决。确定一个精度级别(例如 4 位小数),所有数字都被向上缩放以消除小数点,并以整数形式存储在内存中。所有后续的算术运算都必须考虑比例因子,例如,在乘法 a * b 时,结果需要向下缩放一次,因为每个操作数都添加了自己的比例因子,如果没有校正,则结果的比例会翻倍。

在本文中,我将介绍一个定点类实现,名为 Currency,它具有以下特性

  • 它被透明地映射到 JavaScript 的 Number 类型;这意味着它不是一个真正的类,因此不包含任何代码(没有方法、属性或类主体)。与 Saltarelle 中的所有其他数值类型类似,运行时无法检索其原始 C# 类型(调用 GetType() 不会返回“Currency”而是“Number”)。 
  • 算术运算被转换成 JavaScript 的内联代码(Saltarelle 的 [InlineCode] 属性装饰器完成了所有神奇的操作)。
  • 在执行操作时,所有数字首先被转换为定点数,然后计算操作,最后将结果再次转换为浮点数。这使得数字可以存储为正常的浮点数,而对最终用户没有任何比例的介入。(反之则是将数字存储为向上缩放的整数,并在引用时向下缩放——但这不太实用)。 
  • 在 C# 中,该类被视为一个普通的数值类型,类似于 .NET 的 Decimal,并定义了运算符和隐式/显式转换。
  • Currency 数据类型的精度(比例)固定为 4(比例因子 10000),这对于涉及货币的实际应用程序应该足够了。通过在源代码中进行简单的搜索和替换,可以将“10000”更改为其他 10 的幂(例如,“100”表示更少的小数位数),从而获得其他比例。

它是如何工作的

该类的核心功能在于算术运算符的定义。作为一项规则,当需要进行数学运算时,操作数通过向上缩放截断转换为定点数;然后计算运算,最后将结果再次转换为浮点数。

例如,加法运算定义为

[InlineCode("( (({d1}*10000)>>0) + (({d2}*10000)>>0) )/10000")]
public static Currency operator +(Currency d1, Currency d2) 
{ 
   return d1; 
}          

[InlineCode] 属性告诉编译器忽略方法体(“return d1”),而是输出字符串参数中包含的内联 JavaScript 代码。

在此示例中,两个操作数 d1d2

- 通过“* 10000向上缩放
- 通过“>> 0截断
- 然后通过“+”将数字相加
- 并通过“/ 10000再次向下缩放

该类的真正实用性在于它完全隐藏了这些繁琐的操作,否则必须手动硬编码,这将导致源代码可读性差且难以维护。那些不使用 Saltarelle 而直接用 JavaScript 编写代码而不是 C# 的人别无选择;他们所能做的最好的就是拥有一个外部类库并使用方法调用而不是运算符(例如 d1.Add(d2) 等)。

如何使用代码 

Currency.cs 添加到您的 Saltarelle 脚本项目中,然后像使用新数据类型一样开始使用该类。

变量可以用整数或浮点数常量初始化。例如

Currency a = 10;
Currency b = (Currency) 3.14;

由于从浮点数常量(3.14)初始化可能会导致数字舍入,因此需要显式使用转换运算符。整数被精确编码,因此无需转换。同样,在将 Currency 值转换为其他数值类型时,始终需要转换,例如

double c = (double) b; 

可以通过简单的 .ToString() 进行转换为字符串。为了完整起见,Currency 还实现了以下方法

  • .ToFixed([fractionDigints]) 
  • .ToPrecision([precision])  

以及静态方法:Ceiling()Floor()Round()

测试该类

作为一个简单的测试,我们使用以下 C# 代码

// double test
double fa = 0.1;
fa = fa + 0.2;
double fb = 0.3;
if(fa==fb) Window.Alert("equals");
else Window.Alert("NOT equals");
 
// currency test
Currency ca = (Currency) 0.1; 
ca = ca + (Currency) 0.2;
Currency cb = (Currency) 0.3;
if(ca==cb) Window.Alert("equals");
else Window.Alert("NOT equals"); 

运行时,double 部分失败,但 Currency 部分按预期通过。

现在看看生成的 JavaScript 代码:   

// double test
var fa = 0.1;
fa = fa + 0.2;
var fb = 0.3;
if (fa === fb) {
    window.alert('equals');
}
else {
    window.alert('NOT equals');
}
   
// currency test
var ca = (0.1 * 10000 >> 0) / 10000;
ca = ((ca * 10000 >> 0) + ((0.2 * 10000 >> 0) / 10000 * 10000 >> 0)) / 10000;
var cb = (0.3 * 10000 >> 0) / 10000;
if (ca === cb) {
    window.alert('equals');
}
else {
    window.alert('NOT equals');
} 
这两部分非常相似,编译器添加的唯一额外代码与内联算术运算相关(现在想象一下如果这些必须手动编写!)。

免责声明和结论  

我不是数学专家,本文介绍的代码是我对理论理解的结果,因此我建议在实际使用前对其进行适当的测试。我写这篇文章是为了好玩,作为 Saltarelle 的一项练习,我希望它能激励更多开发者从纯 JavaScript 转向使用完整的 C# 来开发客户端 Web 应用程序。

历史

  • 版本 1.0 - 2012 年 12 月 4 日 
© . All rights reserved.