游戏币种应使用哪种数据类型?


96

在一个简单的业务模拟游戏(用Java + Slick2D内置)中,玩家当前的钱应该存储为float还是int或其他?

在我的用例中,大多数交易将使用美分(0.50美元,1.20美元等),并且涉及简单的利率计算。

我见过有人说你不应该用float货币,也有人说你不应该用int货币。我觉得我应该使用int并舍入任何必要的百分比计算。我应该使用什么?


2
在StackOverflow上讨论了这个问题。好像BigDecimal可能是要走的路。stackoverflow.com/questions/285680/…–
阿德·米勒

2
Java是否没有Currency像Delphi那样的类型,该类型使用缩放的定点数学为您提供十进制数学,而没有浮点固有的精度问题?
梅森惠勒

1
是游戏 会计不会让您四舍五入,因此反对使用浮点数作为货币的正常原因并不重要。
罗伦·佩希特

@MasonWheeler:Java BigDecimal遇到了这类问题。
MartinSchröder2012年

Answers:


92

您可以使用int,并以美分考虑所有内容。1.20美元只是120美分。在显示时,将小数点放在其所属的位置。

利息计算将被截断或四舍五入。所以

newAmt = round( 120 cents * 1.04 ) = round( 124.8 ) = 125 cents

这样,您就不会总是有乱七八糟的小数。您可以通过将未计入的资金(由于四舍五入)添加到您自己的银行帐户中来致富


3
如果使用round,则没有真正的理由将其强制转换为int,是吗?
sam hocevar

19
+1。浮点数使相等比较搞砸了,它们很难正确格式化,并且随着时间的推移会引入有趣的舍入错误。最好自己动手做所有事情,这样以后就不会再有抱怨了。确实不是很多工作。
Dobes Vandermeer

+1。听起来是我进行游戏的好方法。我将自己处理整数,并避免所有浮动问题。
Lucas Tulio 2012年

只是免责声明:我更改了此答案的正确答案,因为它最终成为了我使用的答案。它要简单得多,在这样一个简单的游戏中,没有说明的小数点并不重要。
卢卡斯·图里奥

3
但是,请使用基点而不是美分(100个基点= 1美分)乘以10,000而不是100。GAAP要求所有财务,银行或会计应用程序使用此基点。
Pieter Geerkens

66

好吧,我进去。

我的建议:这是一场比赛。放轻松并使用double

这是我的基本原理:

  • float确实有一个精度问题,将单位添加到数百万时会出现,因此尽管它可能是良性的,但我会避免这种类型。double才开始在五面体(十亿亿)周围出现问题。
  • 由于您将要拥有利率,因此无论如何您都将需要理论上无限的精度:如果利率为4%,则$ 100将变成$ 104,然后是$ 108.16,然后是$ 112.4864,依此类推。这使得int并且long毫无用处,因为您不知道从何处止损小数点。
  • BigDecimal会给您任意精度,但会变得非常缓慢,除非您有时会限制精度。四舍五入规则是什么?您如何选择在哪里停下来?值得拥有比更高的精度位double吗?我相信不是。

在金融应用程序中使用定点算法的原因是因为它们是确定性的。四舍五入规则有时由法律完美定义,必须严格执行,但四舍五入仍会在某些时候发生。任何基于精度支持给定类型的参数都可能是虚假的。所有类型的计算类型都存在精度问题。

实际例子

我看到很多评论声称我不赞成四舍五入或精度。这里有一些其他示例来说明我的意思。

储存:如果您的基本单位是美分,则在存储值时可能要舍入到最接近的美分:

void SetValue(double v) { m_value = round(v * 100.0) / 100.0; }

当采用这种整数类型的方法时,绝对不会出现舍入问题。

检索:所有计算都可以直接在double值上完成,而无需转换:

double value = data.GetValue();
value = value / 3.0 * 12.0;
[...]
data.SetValue(value);

请注意,如果您更换上面的代码不工作doubleint64_t:会有一个隐式转换double,然后截断int64_t,与information.data.GetValue的可能损失()

比较:比较是浮点类型正确的一件事。我建议使用这样的比较方法:

/* Are values equal to a tenth of a cent? */
bool AreCurrencyValuesEqual(double a, double b) { return abs(a - b) < 0.001; }

舍入公平

假设您的帐户中有$ 9.99,利率为4%。玩家应该赚多少钱?使用整数舍入,您将获得$ 0.03; 使用浮点舍入,您将获得$ 0.04。我相信后者更公平。


4
+1。我要补充的唯一澄清是,就像金融应用程序确实符合特定的预定义要求一样,舍入算法也是如此。如果该游戏是娱乐场游戏,则它遵循类似的标准,因此不能选择加倍。
Ivaylo Slavov

2
您仍然必须考虑舍入规则以格式化UI。因为如果您不想让人们发现当后端发现您的后端使用的精度与您的UI精度不一样时,总有一部分玩家会尝试将游戏当作电子表格对待,显示“错误”结果时,使它们保持安静的最佳选择是使用相同的内部和外部表示,这意味着使用定点格式。除非性能成为问题,否则BigDecimal是执行此操作的最佳选择。
Dan Neely 2012年

10
我不同意这个答案。确实出现了四舍五入问题,如果它们出现问题,并且如果您不使用任何其他四舍五入,则$ 1.00可能会突然变成$ 0.99。但最重要的是,(几乎)完全无关紧要的慢速任意精度小数。我们快要到2013年了,除非您要对数以千计的数字进行大型分解,触发或对数运算,否则您甚至都不会注意到性能下降。无论如何,简单总是最好的,所以我的建议是将所有数字存储为美分。这样,他们都是int_64t
熊猫睡衣

8
@Sam上次检查银行帐户时,我的余额未以.0000000000000001结尾。实际货币系统必须使用不可分割的单位,并且必须正确地用整数表示。如果您必须正确进行货币分割(实际上在现实的金融世界中根本不那么普遍),那么您就必须进行货币分割,以免造成金钱损失。例如10/3 = 3+3+410/3 = 3+3+3 (+1)。对于所有其他运算,整数可以完美地工作,这与浮点数不同,浮点数在任何运算中都会产生舍入问题。
熊猫睡衣

2
@SamHocevar 999 * 4/100。除非法规没有规定,否则所有四舍五入都将以银行有利的方式进行。
Dan Neely 2012年

21

Java(floatdouble)中的浮点类型不能很好地表示货币,原因之一是- 舍入过程中会出现机器错误。即使一个简单的计算返回一个整数-样12.0/2(6.0),浮点可能错误地绕着它(由于寿这些类型的存储器中的特定表示),为6.00000000000015.999999999999998或类似。这是处理器中发生特定机器舍入的结果,它是计算它的计算机所独有的。通常,使用这些值很少会出现问题,因为该错误相当疏忽,但是要向用户显示该错误很麻烦。

一个可能的解决方案是使用自定义的浮点数据类型实现BigDecimal。它支持更好的计算机制,该机制至少将舍入误差隔离为不是特定于机器的,但是在性能方面较慢。

如果您需要高生产率,那么最好坚持使用简单的类型。如果您使用重要的财务数据,并且每一分都很重要(例如外汇应用程序或某些娱乐场游戏),那么我建议您使用LonglongLong将使您能够处理大量且精度很高的数据。假设您需要,比如说,小数点后4位数字,那么您所需Long要做的就是将金额乘以10000。拥有开发在线娱乐场游戏的经验,我经常使用美分来表示钱。 。在外汇应用中,精度更为重要,因此您需要更大的乘数-整数仍然没有机器舍入的问题(当然,像3/2这样的手动舍入您应该自己处理)。

可接受的选择是使用标准浮点类型- FloatDouble,如果性能比精度要高百分之几分之一。然后,在您的显示逻辑上,您所需要做的就是使用预定义的格式设置,以使潜在的机器舍入不给用户带来麻烦。


4
+1详细信息:“只要假设您需要小数点后4位数字,您所要做的就是将金额乘以1000。” ->这称为定点。
Laurent Couvidou 2012年

1
@Sam,为示例股份提供了数字。不过,我个人已经看到如此简单的数字会产生怪异的结果,但是这种情况并不常见。当浮点数开始发挥作用时,这很有可能会发生,但更不可能发现
Ivaylo Slavov 2012年

2
@Ivaylo Slavov,我同意你的看法。在我的回答中,我发表了意见。我喜欢讨论并愿意采取正确的做法。也感谢您的意见。:)
Md Mahbubur Ra​​hman

1
@SamHocevar:这种推理并非在所有情况下都有效。请参阅:ideone.com/nI2ZOK
Samaursa

1
@SamHocevar:也不保证。仍然是整数范围内的数字在第一部分开始累积误差:ideone.com/AKbR7i
Samaursa

17

对于 小规模的游戏以及处理速度而言,内存是重要的问题(由于精度或与数学协处理器一起使用会导致速度缓慢),因此加倍就足够了。

但是对于大型游戏(例如社交游戏)并且处理速度,内存不受限制的地方, BigDecimal更好。因为在这里

  • 货币计算的整数或整数。
  • 浮点数和双精度数不能准确表示大多数以10为底的实数。

资源:

来自https://stackoverflow.com/questions/3730019/why-not-use-double-or-float-to-represent-currency

因为浮点数和双精度数不能准确表示大多数以10为底的实数。

这就是IEEE-754浮点数的工作方式:它为符号指定一个位,为指数存储一些位,其余为实际分数。这导致数字以类似于1.45 * 10 ^ 4的形式表示;除了不是10,而是2。

实际上,所有实际的十进制数字都可以看作是10的幂的精确分数。例如,10.45实际上是1045/10 ^ 2。正如有些分数不能精确表示为10的幂的分数(想到的是1/3)一样,其中某些分数也不能精确表示为10的幂的分数。举一个简单的例子,您根本无法在浮点变量中存储0.1。您将获得最接近的可表示值,类似于0.0999999999999999996,并且在显示时,软件会将其舍入为0.1。

但是,当您对不精确的数字执行更多的加法,减法,乘法和除法运算时,随着微小错误的加总,您将越来越失去精度。这使得浮子和双子不足以处理需要完美准确性的金钱。

摘自Bloch,J.,《有效Java》,第二版,第48项:

The float and double types are particularly ill-suited for 

货币计算,因为不可能将0.1(或其他任何负的10的幂)精确地表示为浮点数或两倍。

For example, suppose you have $1.03 and you spend 42c. How much money do you have left?

System.out.println(1.03 - .42);

prints out 0.6100000000000001.

The right way to solve this problem is to use BigDecimal, 

货币计算的整数或整数。

也看看


我只不同意长期适用于小型计算的部分。在我的现实生活中,它已被证明足以处理高精度和大量金钱。是的,使用起来可能有点麻烦,但它在两个关键点上击败了BigDecimal:1)速度更快(BigDecimal使用软件舍入,这比float / double中使用的内置CPU舍入要慢得多)和2) BigDecimal很难保留在数据库中而不损失精度-并非所有数据库都支持这种“自定义”类型。Long是众所周知的,并在所有主要数据库中得到广泛支持。
Ivaylo Slavov

@Ivaylo Slavov,对不起,我的错。我已经更新了答案。
Md Mahbubur Ra​​hman

无需道歉即可针对同一问题提供不同的方法:)
Ivaylo Slavov 2012年

2
@Ivaylo Slavov,我同意你的看法。在我的回答中,我发表了意见。我喜欢讨论并愿意采取正确的做法。也感谢您的意见。:)
Mh Mahbubur Ra​​hman 2012年

该站点的目的是根据具体情况来澄清问题并帮助OP做出正确的决定。我们所能做的就是帮助加快流程:)
Ivaylo Slavov

13

您想要货币存储在中,long并以计算货币double,至少作为备份。您希望所有交易都以进行long

您要存储货币的原因long是,您不想损失任何货币。

假设您使用double,而您没有钱。有人给您三个角钱,然后将它们取回。

You:       0.1+0.1+0.1-0.1-0.1-0.1 = 2.7755575615628914E-17

好吧,那不是很酷。也许有10美元的人想通过先给你三角钱,然后再给别人$ 9.70来发家致富。

Them: 10.0-0.1-0.1-0.1-9.7 = 1.7763568394002505E-15

然后将它们的钱退还给他们:

Them: ...+0.1+0.1+0.1 = 0.3000000000000018

这是坏的。

现在,让我们使用多头,我们将追踪十分之一美分(因此1 = 0.001美元)。让我们给地球上的每个人十亿,一亿一千一百二十万,七万五千,一百四十三美元:

Us: 7000000000L*1112075143000L = 1 894 569 218 048

嗯,等等,我们可以给每个人超过十亿美元,而只花两点多一点?溢出在这里是一场灾难。

因此,每当您要计算转帐金额时,请使用doubleMath.round获得long。然后使用修改余额(增加和减少两个帐户)long

您的经济不会泄漏,它将扩大到万亿美元。

还有更多棘手的问题-例如,如果您付款二十次,该怎么办?*-但这应该可以帮助您入门。

*您计算一笔款项,四舍五入为long; 然后乘以20.0并检查是否在范围内;如果是这样,您可以将付款20L金额乘以以从余额中扣除。通常,所有事务都必须以处理long,因此您确实需要总结所有单个事务;您可以将其作为快捷方式进行乘法,但是您需要确保不添加舍入误差并且不会溢出,这意味着您需要先使用进行检查,double然后再使用进行真正的计算long


8

我要说的是,可能显示给用户的任何值几乎都应该是整数。金钱只是最突出的例子。对拥有900点生命值的怪物造成225次伤害四次,并发现它还剩下1点生命值,这将从经验中减去,就像发现您只是一分钱所能承受的无形部分一样。

从技术角度来说,我认为值得注意的是,不必再花钱就可以做兴趣之类的高级工作。只要您在选择的整数类型中有足够的净空,就可以进行乘法运算,并且除法运算将乘以一个十进制数字,例如,加4%,四舍五入:

number=(number*104)/100

要添加4%(按标准惯例四舍五入):

number=(number*104+50)/100

这里没有浮点数错误,四舍五入总是在 .5标记。

编辑,真正的问题:

看到辩论如何进行,我开始认为概述问题的实质可能比简单的int/float答案。问题的核心不是数据类型,而是控制程序细节。

使用整数表示非整数值将迫使程序员处理实现细节。“使用什么精度?” 和“如何舍入?” 是必须明确回答的问题。

另一方面,浮点数不会使程序员担心,它已经做了很多人期望的事情。但是由于浮点数不是无限精确的,因此会进行一些舍入,并且这种舍入是不可预测的。

一次使用浮动控件并希望控制舍入会发生什么?事实证明这几乎是不可能的。使浮点真正可预测的唯一方法是仅使用可以整体表示的值2^n。但是这种结构使得浮子很难使用。

因此,简单问题的答案是:如果要控制,请使用整数,否则请使用浮点数。

但是,正在辩论的问题只是问题的另一种形式:您想控制住吗?


5

即使只是“一个游戏”,我也会使用Martin Fowler的Money模式,并需要很长一段时间的支持。

为什么?

本地化(L10n):使用该模式,您可以轻松地本地化游戏币种。想想像“运输大亨”这样的大亨游戏。它们可以轻松地让玩家更改游戏中的货币(即,从英镑到美元)以符合现实世界的货币。

long数据类型是64位带符号的二进制补码整数。最小值为-9,223,372,036,854,775,808(最大值)为9,223,372,036,854,775,807(含Java教程

这意味着您可以存储9000倍当前的M2美国货币供应量(约10万亿美元)。给您足够的空间使用任何其他世界货币,甚至包括那些曾经有过恶性通货膨胀的人(如果好奇,请参阅第一次世界大战后的德国通货膨胀,其中1磅面包等于30亿马克)

Long易于保持,非常快速,并且应该给您足够的空间以仅使用整数算术来进行所有利息计算,eBusiness答案给出了如何执行此操作的说明。


2

您要为此付出多少工作?准确性有多重要?您是否关心跟踪舍入和二进制系统中表示十进制数字的不精确性所引起的小数误差?

最终,我倾向于花更多的时间来编码和实施单元测试,以应对“极端情况”和已知的有问题的情况-因此,我将考虑修改后的BigInteger,其中包含任意大的数量并使用BigDecimal或小数位维护小数位一个BigRational部分(两个BigIntegers,一个用于分母,另一个用于分子)-并包括代码,该代码通过将任何非小数部分添加到主BigInteger中(也许只是周期性地)来保持小数部分为实际小数。然后,我将在内部跟踪所有以美分为单位的内容,以使小数部分不会出现在GUI计算中。

对于一个(简单的)游戏来说,可能是复杂的方法-但对于以开源形式发布的库而言,则是一个好方法!只是需要找出处理小数位时保持性能良好的方法...


1

当然不会浮动。仅用7位数字,您就只能像:

12,345.67 123,456.7x

因此,您已经知道,已经有了10 ^ 5美元,您便失去了几分钱。double出于游戏目的而不是现实生活中出于精度考虑而可用。

但是,如果您要跟踪事务并对其进行汇总,那么长(这就是64位或C ++中的长)是不够的。足以维持任何一家公司的净持仓量,但是一年中的所有交易都可能溢出。这取决于您的财务“世界”在游戏中有多大。


0

我将在此处添加的另一个解决方案是赚钱类。该类类似于(甚至可以只是一个结构)。

class Money
{
     string currencyType; //the type of currency example USD could be an enum as well
     bool isNegativeValue;
     unsigned long int wholeUnits;
     unsigned short int partialUnits;
}

在这种情况下,这将使您可以将美分表示为整数,将整数表示为整数。由于您有一个单独的负数标志,因此可以对值使用无符号整数,这会使可能的数量加倍(您也可以编写一个将这个想法应用于数字的数字类,以得到真正大的数字)。您需要做的就是重载数学运算符,并且可以像使用任何旧数据类型一样使用它。它占用更多的内存,但实际上扩展了值限制。

可以进一步扩展到包括诸如每单位的部分单位数量之类的东西,这样您就可以将货币细分为除100个子单位以外的其他货币(在本例中为美分/美元)。例如,您可能有125个floopies组成一个floper。

编辑:

我将扩展您的想法,因为您还可以查找某种形式的表格,货币类可以访问该表格,以提供其货币对所有其他货币的汇率。您可以将其内置到操作员重载函数中。因此,如果您自动尝试在4英镑对4英镑中增加4美元,则将自动给您6.6英镑(截至撰写本文时)。



-1

使用类别是最好的方法。您可以隐藏资金的实现,可以随时切换两倍/多长时间。

class Money
{
    private long count;//Store here what ever you want in what type you want i suggest long or int
    //methods constructors etc.
    public String print()
    {
        return count/100.F; //convert this to string
    }
}

在您的代码中使用简单的getter,这是我认为的更好方法,您可以存储更多有用的方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.