VBA:Variant/Double和Double的区别

5

我正在使用Excel 2013。在下面的代码片段中,VBA计算damage为40:

Dim attack As Variant, defense As Variant, damage As Long
attack = 152 * 0.784637
defense = 133 * 0.784637
damage = Int(0.5 * attack / defense * 70)

如果数据类型更改为 Double ,VBA计算 damage 为39:
Dim attack As Double, defense As Double, damage As Long
attack = 152 * 0.784637
defense = 133 * 0.784637
damage = Int(0.5 * attack / defense * 70)

在调试器中,Variant/DoubleDouble值看起来是相同的。然而,Variant/Double似乎具有更高的精度。
有人能解释这种行为吗?

0.570 的顺序交换,你仍然可以得到 39:Int(70 * attack / defense * 0.5) - ThunderFrame
2个回答

6

简而言之; 如果你需要比 Double 更高精度的数值,不要使用 Double

答案在于将结果从 Variant 转化为 Double 的时机。 Double 是一个IEEE 754浮点数,根据IEEE规范,其可逆性在15个有效数字内是有保障的。而你的值已经接近这个极限了:

0.5 * (152 * .784637) / (133 * .784637) * 70 = 39.99999999999997 (16 sig. digits)

当VBA强制转换为double时,超出15个有效数字的任何内容都会被四舍五入:

Debug.Print CDbl("39.99999999999997") '<--Prints 40

实际上,您可以在VBE中观察此行为。输入或复制以下代码:

Dim x As Double
x = 39.99999999999997

VBE会将文字值“自动修正”为Double类型,所以你得到的是:

Dim x As Double
x = 40#

好的,到目前为止你可能会问这与两个表达式之间的区别有什么关系。VBA使用“最高阶”变量类型来评估数学表达式。

在你的第二个Sub中,你将所有变量声明为右侧的Double,操作是使用Double的高阶进行评估,然后结果在传递给Int()参数之前隐式转换为Variant

在你的第一个Sub中,你声明了Variant,在传递给Int之前不执行隐式转换-数学表达式中的最高阶是Variant,因此在传递结果给Int()之前不执行隐式转换 - Variant仍然包含原始IEEE 754浮点数。

根据Int文档

Int和Fix都会移除数字的小数部分并返回结果整数值。

不执行舍入。顶部代码调用Int(39.99999999999997)。底部代码调用Int(40)。答案取决于你想舍入到哪个浮点误差级别。如果15有效位数就可以,那么40就是“正确”的答案。如果你想将任何小于16个或更多有效数字的数字向下取整,则39是“正确”的答案。解决方案是使用Round并明确指定你要查找的精度级别。例如,如果你关心全部15位数字:

Int(Round((0.5 * attack / defense * 70), 15))

请记住,在任何输入中使用的最高精度为6位数字,因此这将是一个逻辑舍入截止点:

Int(Round((0.5 * attack / defense * 70), 6))

1
不错。值得注意的是,当类型为“货币”而不是“双精度浮点数”时,两个版本都返回40。 - Mathieu Guindon
2
@Mat'sMug - 货币将所有数值乘以 10,000,仅关注小数点右侧的 4 个数字。它在第5位四舍五入,因此相当于 Int(Round(399999.99999, 4)) / 10000 - Comintern
1
请注意,您的理解是错误的。顶部代码必须调用 Int(40),而底部代码必须调用 Int(39.something)。您的回答暗示了 Variant 比标准 IEEE 双精度浮点数具有更高的精度,可能存储为长双精度浮点数或某种原始 x86 双精度浮点数。据我所知,这种行为是未记录的。 - ltleelim

0
如果在计算伤害的两行中都去掉Int()函数,那么它们最终都将变成相同的。你不应该使用Int,因为这会产生错误的行为,你应该使用CLng,因为你正在转换为Long变量,或者如果伤害是Int,则应该使用CInt。
Int和CInt的行为不同。Int始终向下舍入到下一个更低的整数 - 而CInt将使用银行家舍入法四舍五入。您通常会看到这种数字行为的基数为0.5。
至于变体和双倍差异,如果对第一个代码块进行TypeName到MsgBox,您会发现攻击和防御在分配值后都被转换为双精度类型,尽管它们被声明为变体。

1
如果您在Int调用中保留并将RHS表达式更改为攻击和防御值的常量分配,则可以获得两个版本都返回40的结果:attack = 119.264824defense = 104.356721。不确定您的意图是什么。如果该函数应该返回一个Integer值,则使用CInt正好符合其显式类型转换的作用。OP的问题是,当攻击和防御值为Variant时与明确的Double不同时,结果为什么会有所不同,而不是如何“修复”它。 - Mathieu Guindon
但是,使用 damage As Long 时,转换应该是 CLng 而不是 CInt - Mathieu Guindon
抱歉,你是对的,更新更好地回答了这里发生了什么。 - Amorpheuses
说实话,那个问题确实有点让人费解;-) - Mathieu Guindon
你做了一个错误的假设。我没有说我想要答案是39或40。它就是这样。然而,我不想使用CLng,因为我需要截断浮点值。我想知道为什么当Variant应该像Double一样行事时,行为会有所不同。 - ltleelim
@ComIntern 解释说,Excel 正在将 Variant 强制转换为 Double,该转换会导致 Excel 自动将其四舍五入为 40,因为它非常接近该值。这可能必须发生才能将其转换为 IEEE 格式 - 基本上我们已经没有数字了。如果它是 39.99...999,仅具有 14 个数字,那么会发生这种情况吗?这种强制转换发生是因为您正在对其进行数学运算。我不清楚是否可以采取任何措施,除了从其中一个操作数减去“delta”,以便不会发生这种情况。 - Amorpheuses

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