解决 NSInteger <-> NSNumber 问题

12

我编写了一个大型的社交网络iPhone应用程序,遇到的最大问题之一是NSInteger(以及所有其他NS非对象类型)不是一等公民。这个问题源于它们显然没有表示nil值的方法。

这导致了两个主要问题:

  1. 在存储和从集合中检索数据时,需要花费大量的开销并且难以理解,因为需要将其转换为和从NSNumber。
  2. 无法表示空值。通常我想能够表示“未设置”的值。

解决方法之一是始终使用NSNumber,但这会变得极其混乱。在用户模型对象中,我将有大约20个不同的NSNumbers,并且没有简单的方法来告诉每个数字是浮点数,整数,布尔值等等。

因此,以下是我对潜在解决方案的思考以及它们的优缺点。 我并不是真的对它们中的任何一个都非常满意,因此我想征求反馈和/或此问题的替代解决方案。

  1. 继续使用NSInteger类型,只使用NSIntegerMax来表示nil。
    PRO - 内存开销小
    PRO - 类型清晰
    CON - NSIntegerMax并不真正表示 nil。 如果程序员不小心或不知道这个约定,无效值可能会泄漏到显示层。
    CON - 无法在集合中存储它们,需要进行转换。

  2. 使用NSNumber,并使用匈牙利命名法指定类型(例如NSNumber fHeight,NSNumber iAge)
    PRO - 一等公民
    PRO - 解决了nil问题
    CON - 内存开销增加
    CON - 失去编译器类型检查
    CON - 匈牙利命名法存在争议

  3. 编写自己的一等原始对象类型(类似于Javahttp://developer.android.com/reference/java/lang/Integer.html
    PRO - 一等公民
    PRO - 解决了nil问题
    PRO - 保持编译器类型检查
    PRO - 对象将比NSNumber更简单。 内部存储将特定于数据类型。
    CON - 内存开销增加
    CON - 牺牲了一定的代码可移植性和兼容性

寻找支持其中一种技术(或者如果你有其他的选择也行)的有力论据。


更新

我已经开始了一个开源项目 (Apache 2.0),我会在有时间的时候将我们内部的一些类放进去。目前它包括了一些常见本地数据类型 (BOOL、CGFloat、NSInteger、NSUInteger) 的对象包装器。我们之所以选择这样做,是因为它将这些数据类型升级为严格类型的一等公民。也许你不同意这种方法,但它对我们很有效,如果你愿意,可以随意使用它。

我正在添加其他我们发现有用的类,包括一个磁盘支持的LRU缓存、一个“Pair”对象、一个低内存释放池等等。

享受吧!github - Zoosk/ZSFoundation


这对我来说听起来像是社区维基 :) - Blitz
2
如果你想让数字成为 Objective-C 中的一流公民,请在 http://bugreport.apple.com 上提出一个功能请求。它会被标记为重复,但是重复越多越好(因为它会不断提醒语言开发人员,人们需要某些功能)。 - Dave DeLong
@DougW,你打算在ZSFloat中添加compare: withPrecision吗? - Stephen Furlani
@Stephen - 当然,不错的想法 - 我已将其添加到跟踪问题中。我真的徘徊在是否要编写一个浮点类。但如果我要这样做,我想我应该引导人们远离反模式。 - DougW
@DougW,有标准吗?ISO公约?您可以将其设置为类属性、编译定义或其他内容。 - Stephen Furlani
@Stephen - 短答案?不行。长答案?绝对误差完全取决于所使用的数字。以下是一些有关此主题的相关材料链接 - (http://hal.archives-ouvertes.fr/hal-00128124/en/) - (http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm) - (http://en.wikipedia.org/wiki/Machine_epsilon) - DougW
4个回答

5

表示 nil 这个概念的最常见约定是使用 NSNotFound 值作为 NSInteger。实际上,这与 NSIntegerMax 相等,但对于读者来说更加明显,这是一个哨兵值,表示缺少数字。在 Cocoa 中有许多这样的用例。其中一个常见用例是作为 -rangeOfString: 等方法返回值中的 NSRange 的位置字段。


嘿,是的,我同意,但说到底这只是一种约定,并不一定安全。例如,我们遇到过许多错误,比如用户的身高可能会意外地显示为NSIntegerMax,因为编写显示层逻辑的程序员并不了解这个约定。 - DougW
听起来你需要教育一下你的程序员。一个人永远不应该对正在使用的值做出盲目的假设。 - Lily Ballard
好的,这不是教育问题,而是知识传播问题。一些NSIntegers可能使用这种约定,而另一些则可能不使用。那CGFloats呢?如果没有更好的解决方案很快被发布,我会标记这个答案,因为可能没有更好的解决方案。 - DougW
如果你的程序员不知道有哨兵值存在,那么你使用的特定值并不会有太大区别。Cocoa几乎专门使用NSNotFound作为它的NSInteger/NSUInteger哨兵值。至于CGFloat,哨兵值在那里并不常见,但你可以使用NaN,但如果你这样做,你应该非常小心地处理它。例如,在iOS 4.2中,将CALayer的中心/边界组件设置为NaN会触发异常。 - Lily Ballard
完全就是我的观点。使用 nil,没有人需要“意识到”任何事情-它由架构而非约定所指示。如果我给你一个我的代码库,你怎么知道我用了什么约定?你会查阅我的文档以获取每个属性的信息,并相信我在每个地方都正确地记录了吗?我会接受这个因为没有更好的本地解决方案,但是在一个建立在故障安全消息传递和零值周围的语言中,这种模式在我看来是一场灾难。 - DougW
这个答案过于简化问题的微妙之处,仅关注于NSInteger约定。然而,Kevin后来的评论确实暗示了提供其他类型处理的其他方法。在任何面向对象的语言中(特别是Ruby),抽象的数字类都是一个棘手的妥协。 - ctpenrose

4
你可以试试这个吗?
#define numberWithFloat(float f) \
  [NSNumber numberWithFloat:f]
#define floatFromNumber(NSNumber *n) \
  [n floatValue]

以下是我的原始答案:

另外一个关于NSNumber的事情是,你不需要检索你所设置的内容。

例如:

NSNumber *myInt = [NSNumber numberWithInteger:100];
float myFloat = [myInt floatValue];

这是完全有效的。NSNumber的优点在于它允许您“弱类型”原始数据类型,使用compare:isEqualTo:stringValue进行简单的显示。


[编辑]

用户@Dave DeLong表示,在没有太多工作的情况下,子类化NSNumber是一个坏主意。由于它是一个类簇(意味着NSNumber是很多子类的抽象超类),如果您对其进行子类化,则必须声明自己的存储。不建议这样做,感谢Dave指出这一点。


这个想法需要更多的工作才能实现。NSNumber是类簇的一部分,文档中有关于你需要做的更多信息 - Dave DeLong
@Dave,是的,我刚刚检查了一下。不过我不知道它是什么意思。有关类簇的内容吗? - Stephen Furlani
2
@Stephen 类簇的文档定义 - Dave DeLong
@Stephen - 我不确定在这种情况下宏定义是否真的比仅使用NSNumbers更有优势。仍然存在关于给定变量应表示哪种类型数字的歧义。是的,我不会尝试子类化NSNumber。我可能更愿意创建自己的类(例如ZSInteger),它们是NSValue的同级或子级,而不是NSNumber。 - DougW
@Dave - 是的,我只是想知道它是否是类类型本身固有的东西,还是意味着如果你要搞乱了任何东西,你就必须搞乱所有东西。在这种情况下,我什么都不会动。不过我想我可能会自己编写一个类簇。 - DougW
显示剩余4条评论

3
作为替代方案,还有NSDecimal结构体用于表示数字类型。 NSDecimal(及其Objective-C类版本NSDecimalNumber)允许您表示真正的小数并避免浮点错误,因此它往往建议用于处理货币等事物
NSDecimal结构体可以表示数字以及不是数字状态(潜在的nil替换)。 您可以使用NSDecimalIsNotANumber()查询NSDecimal是否不是数字,并借助NSDecimalNumber生成不是数字值。
NSDecimals比NSDecimalNumbers {{link2:更快地处理} },而且结构体不会带来与对象相同类型的内存管理问题。
然而,没有简单的方法将值转换为NSDecimal形式,而不使用临时的NSDecimalNumber。此外,许多适用于简单浮点数的数学函数(例如三角函数操作)尚不适用于NSDecimal。我想编写自己的函数以增加一些这些功能,但是它们需要访问结构体的内部字段。苹果使用下划线标记这些字段为私有,但它们存在于头文件中,我猜它们不太可能在将来发生变化。

更新:Dave DeLong编写了一系列函数,用于执行NSDecimal三角函数、根和其他操作。我对此进行了微调,以提供高达34位小数精度,并且可以在此处下载这些函数的代码here。请注意,这种精度会以性能为代价,但是这些函数应该适用于相当少量的计算。


如果您为NSDecimal编写自己的三角函数,我很想了解一下! - Dave DeLong
有趣,我以前没有使用过NSDecimal。不确定它是否解决了我的问题,但感谢您指出它。 - DougW
NSDecimalNumber是NSNumber的一个子类。 - JeremyP
1
@JeremyP - 正确,但我想指出NSDecimal结构体,这是另一种非对象数值类型。诚然,在处理它时,您必须在某个时候接触NSDecimalNumber,但在某些情况下,NSDecimal可能具有优势。 - Brad Larson

0

我不明白为什么,如果你将东西存储为NSNumber,你需要关心它是什么类型。NSNumber会选择一个适当的内部表示,然后根据你要求的值的格式进行任何必要的转换。

如果你无法从变量的名称中确定适当的类型,那么你需要选择更好的名称。例如:

numberOfChildren = [NSNumber numberWithFloat: 2.5];

这很明显是愚蠢的。

对于货币值,我强烈建议使用NSDecimalNumber(它是NSNumber的子类),但定义了基于十进制的算术运算,因此您不必担心浮点舍入问题。


谢谢你的回答Jeremy。我认为如果你查一下关于匈牙利命名法的争议,你会发现很多反对使用明确变量类型的命名约定的论点。当然,numberOfChildren是显而易见的,但是"heightInInches"呢?它是整数还是允许小数英寸?即使有良好的命名,也存在许多情况下允许值范围不清晰的情况。我关心的原因是因为我想在整个堆栈中明确表达signed vs unsigned和int vs float等内容。 - DougW
我想补充一下,有许多弱类型甚至无类型的编程语言,这是完全合理的方法。但是,C和Obj C旨在具有强类型,有很多原因不要打破这种结构。所以我并不是说你不能按自己的方式做,我只是更喜欢坚持强类型的领域。这导致了另一个完全不同的争论,所以对于我的特定问题,我想坚持使用强类型。 - DougW

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