从 float 或 double 实例化 NSDecimalNumber 的正确方法

Posted

技术标签:

【中文标题】从 float 或 double 实例化 NSDecimalNumber 的正确方法【英文标题】:Proper way to instantiate an NSDecimalNumber from float or double 【发布时间】:2011-03-14 21:34:20 【问题描述】:

我正在寻找从 double 或 short 实例化 NSDecimalNumber 的最佳方法。有以下 NSNumber 类和实例方法...

+NSNumber numberWithFloat
+NSNumber numberWithDouble
-NSNumber initWithFloat
-NSNumber initWithDouble

但这些似乎返回 NSNumber。另一方面,NSDecimalNumber 定义如下:

+NSDecimalNumber decimalNumberWithMantissa:exponent:isNegative:
+NSDecimalNumber decimalNumberWithDecimal:

这里有几种可能性。如果您将 NSDecimalNumber 设置为上述 NSNumber 便捷方法的返回值,Xcode 会生成警告。

希望您能就最干净和正确的方式提供意见...

【问题讨论】:

请记住,确定对象类型的是类方法alloc,而不是init*方法。 initWithFloat 不返回 NSNumber。您正在对 alloc 返回的 NSDecimalNumber 实例调用 initWithFloat。 @orj - 是的,但是正如我对您的回答的评论,这实际上是将 NSNumber 类型转换为 NSDecimalNumber,不建议这样做:cocoabuilder.com/archive/cocoa/…。我在下面的 cmets 中指出了一个失败的案例。 【参考方案1】:

你走在正确的轨道上(有一些警告,见下文)。您应该能够只使用来自 NSNumber 的初始化程序,因为它们是由 NSDecimalNumber 继承的。

NSDecimalNumber *floatDecimal = [[[NSDecimalNumber alloc] initWithFloat:42.13f] autorelease];
NSDecimalNumber *doubleDecimal = [[[NSDecimalNumber alloc] initWithDouble:53.1234] autorelease];
NSDecimalNumber *intDecimal = [[[NSDecimalNumber alloc] initWithInt:53] autorelease];

NSLog(@"floatDecimal floatValue=%6.3f", [floatDecimal floatValue]);
NSLog(@"doubleDecimal doubleValue=%6.3f", [doubleDecimal doubleValue]); 
NSLog(@"intDecimal intValue=%d", [intDecimal intValue]);

关于这个主题的更多信息可以找到here。

但是,我在 Stack Overflow 和网络上看到了很多关于初始化 NSDecimalNumber 问题的讨论。似乎有一些与精度和与NSDecimalNumber 之间的双精度转换有关的问题。尤其是当你使用继承自 NSNumber 的初始化成员时。

我敲了下面的测试:

double dbl = 36.76662445068359375000;
id xx1 = [NSDecimalNumber numberWithDouble: dbl]; // Don't do this
id xx2 = [[[NSDecimalNumber alloc] initWithDouble: dbl] autorelease];
id xx3 = [NSDecimalNumber decimalNumberWithString:@"36.76662445068359375000"];
id xx4 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L exponent:-17 isNegative:NO];

NSLog(@"raw doubleValue: %.20f,              %.17f", dbl, dbl);

NSLog(@"xx1 doubleValue: %.20f, description: %@", [xx1 doubleValue], xx1);   
NSLog(@"xx2 doubleValue: %.20f, description: %@", [xx2 doubleValue], xx2);   
NSLog(@"xx3 doubleValue: %.20f, description: %@", [xx3 doubleValue], xx3);   
NSLog(@"xx4 doubleValue: %.20f, description: %@", [xx4 doubleValue], xx4);   

输出是:

raw doubleValue: 36.76662445068359375000               36.76662445068359375
xx1 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168
xx3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375
xx4 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

所以你可以看到,当在NSNumber 上使用numberWithDouble 便捷方法时(由于它返回错误的指针类型,你不应该真正使用它)甚至初始化器initWithDouble (IMO“应该”可以调用)你得到一个带有内部表示的 NSDecimalNumber(通过在对象上调用 description 返回),它不如你调用 decimalNumberWithString:decimalNumberWithMantissa:exponent:isNegative: 时得到的准确。

另外,请注意,通过在 NSDecimalNumber 实例上调用 doubleValue 转换回双精度会丢失精度,但有趣的是,无论您调用什么初始化程序,都是一样的。

所以最后,我认为在处理高精度浮点数时,我认为建议您使用在 NSDecimalNumber 类级别声明的 decimalNumberWith* 方法之一来创建您的 NSDecimalNumber 实例。

那么当你有一个 double、float 或其他 NSNumber 时,你如何轻松地调用这些初始化器呢?

两种方法描述了here“工作”,但仍然存在精度问题。

如果您已经将号码存储为 NSNumber,那么这个应该可以工作:

id n = [NSNumber numberWithDouble:dbl];
id dn1 = [NSDecimalNumber decimalNumberWithDecimal:[n decimalValue]];

NSLog(@"  n doubleValue: %.20f, description: %@", [n doubleValue], n);   
NSLog(@"dn1 doubleValue: %.20f, description: %@", [dn1 doubleValue], dn1);  

但正如您从下面的输出中看到的那样,它去掉了一些不太重要的数字:

  n doubleValue: 36.76662445068359375000, description: 36.76662445068359
dn1 doubleValue: 36.76662445068357953915, description: 36.76662445068359

如果数字是原始数字(浮点数或双精度数),那么这个应该工作:

id dn2 = [NSDecimalNumber decimalNumberWithMantissa:(dbl * pow(10, 17))
                                          exponent:-17 
                                        isNegative:(dbl < 0 ? YES : NO)];

NSLog(@"dn2 doubleValue: %.20f, description: %@", [dn2 doubleValue], dn2);

但是您会再次遇到精度错误。正如您在下面的输出中看到的:

dn2 doubleValue: 36.76662445068357953915, description: 36.76662445068359168

我认为这种精度损失的原因是由于涉及浮点乘法,因为以下代码可以正常工作:

id dn3 = [NSDecimalNumber decimalNumberWithMantissa:3676662445068359375L 
                                           exponent:-17 
                                         isNegative:NO];

NSLog(@"dn3 doubleValue: %.20f, description: %@", [dn3 doubleValue], dn3);   

输出:

dn3 doubleValue: 36.76662445068357953915, description: 36.76662445068359375

因此,从 doublefloat 到 NSDecimalNumber 的最一致准确的转换/初始化是使用 decimalNumberWithString:。但是,正如 Brad Larson 在他的回答中指出的那样,这可能有点慢。如果性能成为问题,他使用 NSScanner 进行转换的技术可能会更好。

【讨论】:

请注意,不推荐将 NSNumber 的初始化器与 NSDecimalNumber 一起使用。它可能导致浮点数的错误转换,如以下问题示例所示:***.com/questions/2479100/…。另外,请在此处查看 Marcus Zarra 的文章后的 cmets:cimgf.com/2008/04/23/…。这也是在 cocoa-dev 邮件列表中的某个时间点提出的,但我现在找不到参考。 啊,这是 Bill Bumgarner 在 cocoa-dev 邮件列表上的回复:cocoabuilder.com/archive/cocoa/…。 很好的后续分析。很高兴看到各种初始化程序所发生的一些硬性结果。 很好的详细信息。感谢分享。我正在编写一个应用程序,它以浮点形式接收股票报价,只需显示它们而无需对它们进行任何数学运算。在这种情况下,使用 NSNumber 性能会更好吗?我总是可以自己使用浮点数,但我认为使用 NSNumbers 更好,因为这样可以立即使用 NSNumberFormatter 类。 @Paul - NSNumberFormatter 与 NSDecimalNumber: ***.com/questions/2940439/… 配合得很好。但是,如果您只是显示服务收集的值,浮点数应该没问题。 NSDecimalNumber 适用于您对这些数字进行任何操作时,因为在处理货币时您最不想要的就是拥有 43.1 - 43.2 + 1 = 0.89999999999999857891(实际情况)。这不仅仅是精度问题,还有 IEEE 754 浮点小数表示的问题。【参考方案2】:

如果在创建 NSDecimalNumber 时使用 NSNumber 的初始化程序,可能会出现一些问题。请参阅this question 中发布的示例,了解一个很好的例子。另外,我相信比尔·布姆加纳(Bill Bumgarner)在his response in the cocoa-dev mailing list 上的话。另请参阅 Ashley 的回答 here。

因为我对这些转换问题持怀疑态度,所以我过去曾使用以下代码来创建 NSDecimal 结构:

NSDecimal decimalValueForDouble(double doubleValue)

    NSDecimal result;
    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSScanner *theScanner = [[NSScanner alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];

    [theScanner scanDecimal:&result];
    [theScanner release];

    return result;

这里代码复杂的原因是我 found NSScanner to be about 90% faster 不是从字符串初始化 NSDecimalNumber。我使用 NSDecimal 结构,因为他们可以yield significant performance advantages 超过他们的 NSDecimalNumber 兄弟。

对于更简单但稍慢的 NSDecimalNumber 方法,您可以使用类似

NSDecimalNumber *decimalNumberForDouble(double doubleValue)

    NSString *stringRepresentationOfDouble = [[NSString alloc] initWithFormat:@"%f", doubleValue];
    NSDecimalNumber *stringProcessor = [[NSDecimalNumber alloc] initWithString:stringRepresentationOfDouble];
    [stringRepresentationOfDouble release];
    return [stringProcessor autorelease];

但是,如果您使用的是 NSDecimalNumbers,则需要尽您所能避免在任何时候将值强制转换为浮点数。从文本字段中读回字符串值并将它们直接转换为 NSDecimalNumbers,使用 NSDecimalNumbers 进行数学运算,然后将它们作为 NSStrings 或核心数据中的十进制值写入磁盘。一旦您的值在任何时候转换为浮点数,您就会开始引入浮点表示错误。

【讨论】:

布拉德,谢谢分享。如何创建 NSDecimal C 风格的结构?我对它们进行了研究,但在 Apple 的文档中并没有太多了解 @Paul - 我在答案here 中描述了 NSDecimal 结构的内部格式,这是通过检查适当的头文件获得的。但是,这种格式被认为是私有的,并且在标头之外没有记录。通常,创建它们的最安全方法是从 NSDecimalNumber 中获取值,或者像我上面那样在字符串上使用 NSScanner。一旦你有了一个 NSDecimal 格式的值,它通常很容易使用。【参考方案3】:

你可以试试:

float myFloat = 42.0;
NSDecimal myFloatDecimal = [[NSNumber numberWithFloat:myFloat] decimalValue];
NSDecimalNumber* num = [NSDecimalNumber decimalNumberWithDecimal:myFloatDecimal];

显然有faster ways of doing this,但如果代码不是瓶颈,可能不值得。

【讨论】:

【参考方案4】:

-[NSNumber initWithFloat:]-[NSNumber initWithDouble:]实际上不返回NSNumber *对象,它们返回id,而NSDecimalNumber继承自NSNumber,所以你可以使用:

NSDecimalNumber *decimal = [[[NSDecimalNumber alloc] initWithFloat:1.1618] autorelease];

呃,我刚刚意识到+numberWithFloat: 也是如此,所以你可以改用它。

【讨论】:

【参考方案5】:

或者你可以定义一个宏来缩短代码

#define DECIMAL(x) [[NSDecimalNumber alloc]initWithDouble:x]

budget.amount = DECIMAL(5000.00);

【讨论】:

以上是关于从 float 或 double 实例化 NSDecimalNumber 的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

C 语言实例 - 计算 int, float, double 和 char 字节大小。

Python:Float或Double

Java类的实例化的初始化过程

为什么我无法从std :: stringstream二进制流中获得完全精度(double,float)

为啥不使用 Double 或 Float 来表示货币?

为啥不使用 Double 或 Float 来表示货币?