Objective-C中的常量

1048
我正在开发一个Cocoa应用程序,使用常量NSString作为存储首选项键名的方法。
我知道这是一个好主意,因为它允许在必要时轻松更改键。
此外,这是“将数据与逻辑分离”的整体概念。
无论如何,有没有一种好的方法可以使这些常量为整个应用程序定义一次?
我相信有一种简单而聪明的方法,但现在我的类只是重新定义它们使用的那些常量。

8
面向对象编程(OOP)是将数据和逻辑组合在一起的编程范式。您提出的建议只是一种良好的编程实践,即使程序变得更易于修改。 - khatchad
14个回答

1312

你应该创建一个像这样的头文件:

// Constants.h
FOUNDATION_EXPORT NSString *const MyFirstConstant;
FOUNDATION_EXPORT NSString *const MySecondConstant;
//etc.

如果你的代码不会在混合C / C ++环境或其他平台上使用,可以使用extern代替FOUNDATION_EXPORT

您可以将此文件包含在每个使用常量的文件中,也可以将其包含在项目的预编译标头中。

您可以在.m文件中定义这些常量,例如:

// Constants.m
NSString *const MyFirstConstant = @"FirstConstant";
NSString *const MySecondConstant = @"SecondConstant";

需要将Constants.m添加到您的应用程序/框架的目标中,以便它链接到最终产品中。

使用字符串常量而不是#define常量的优点是可以使用指针比较(stringInstance == MyFirstConstant)进行相等性测试,这比字符串比较([stringInstance isEqualToString:MyFirstConstant])更快(在我看来更易读)。


69
对于一个整型常量,它将是:extern int const MyFirstConstant = 1; 外部可见且不可更改的整型常量MyFirstConstant被定义为1。 - Dan Morgan
183
总体来说,回答很好,但有一个非常明显的警告:在Objective-C中,您不要使用==运算符测试字符串相等性,因为它测试内存地址。始终使用-isEqualToString:进行此操作。通过比较MyFirstConstant和[NSString stringWithFormat:MyFirstConstant],您可以轻松获得不同的实例。不要对您拥有的字符串实例做任何假设,即使是字面量也是如此。无论如何,#define是“预处理器指令”,在编译之前被替换,因此编译器最终都会看到字符串字面量。 - Quinn Taylor
75
如果常量符号真正被用作常量符号(即使用符号MyFirstConstant而不是包含@"MyFirstConstant"的字符串),则在测试其与常量的相等性时,可以使用==。在这种情况下,可以使用整数代替字符串(实际上你使用的就是指针作为整数),但是使用常量字符串使得调试稍微容易一些,因为常量的值具有可读性。 - Barry Wark
18
+1代表将"Constants.m"添加到您的应用程序/框架的目标中,以便它链接到最终产品中。这让我恢复了理智。@amok,请对"Constants.m"执行"获取信息"并选择"目标"选项卡。确保为相关目标选中该文件。 - PEZ
73
在Cocoa中,我看到了许多类将它们的 NSString 属性定义为 copy 而不是 retain。因此,它们可以(也应该)保存一个不同的实例,而直接内存地址比较将失败。另外,我认为任何合理优化的 -isEqualToString: 实现都会在进行字符比较之前检查指针相等性。 - Ben Mosher
显示剩余26条评论

284

最简单的方法:

// Prefs.h
#define PREFS_MY_CONSTANT @"prefs_my_constant"

更好的方式:

// Prefs.h
extern NSString * const PREFS_MY_CONSTANT;

// Prefs.m
NSString * const PREFS_MY_CONSTANT = @"prefs_my_constant";

第二种方法的一个好处是,更改常量的值并不会导致整个程序重新构建。


12
我认为常量的值不应该被改变。 - ruipacheco
74
安德鲁指的是在编写代码时更改常量的值,而不是在应用程序运行时更改。 - Randall
7
在做extern NSString const * const MyConstant时,即将其设置为常量指针指向常量对象,相比于仅将其设置为常量指针,是否有任何附加价值? - Hari Honor
4
如果我在头文件中使用这个声明,static NSString * const kNSStringConst = @"const value",会发生什么?如果在.h和.m文件中没有分别声明和初始化的区别是什么? - karim
4
@Dogweather - 有些地方只有编译器知道答案。例如,如果你想在关于菜单中包括编译应用程序构建所使用的编译器,可以将其放在那里,因为编译后的代码否则无法知道这个信息。我想不出还有很多其他的地方能使用宏定义。宏定义在许多地方都不应该被使用。比如说,如果我在一个地方使用了 #define MY_CONST 5,在另一个地方使用了 #define MY_CONST_2 25,结果就可能导致编译器尝试编译 5_2 而出现错误。不要使用 #define 宏定义来定义常量,应该使用 const 来定义常量。 - ArtOfWarfare
显示剩余4条评论

194
还有一件事情需要提到。如果你需要一个非全局常量,你应该使用static关键字。
例子
// In your *.m file
static NSString * const kNSStringConst = @"const value";

由于使用了static关键字,这个常量在文件外不可见。

@QuinnTaylor 进行了小的修正: 静态变量在编译单元内是可见的。通常,这是一个单独的 .m 文件(就像这个例子中一样),但如果你在其他地方包含了一个头文件并在其中声明了它,那么它可能会让你遇到链接器错误。


41
轻微更正:静态变量在 编译单元 内可见。通常情况下,这是一个单独的 .m 文件(就像这个例子中一样)。但如果你在一个头文件中声明它并将其包含到其他地方,那么你在编译后会得到链接器错误。 - Quinn Taylor
2
好的,刚刚检查了一下...如果你不加static,Xcode在其他文件中不会为它提供自动补全功能,但是我尝试在两个不同的地方放置相同的名称,并重现了Quinn的链接器错误。 - Danyal Aytekin
2
头文件中的静态变量不会导致链接器问题。然而,每个包含该头文件的编译单元都将获得自己的静态变量,因此如果您从100个.m文件中包含该头文件,则会获得100个静态变量。 - gnasher729
@kompozer,为什么这个名字? - Iulian Onofrei
@kompozer,是的,我指的是变量名。这是标准Apple惯例吗?为什么不使用STRING_CONST而不是StringConst?这只存在于Java世界中吗? - Iulian Onofrei
显示剩余5条评论

117

接受(且正确)的答案说“你可以在项目的预编译头文件中包含这个[Constants.h]文件……”

作为一个新手,我很难在没有进一步的解释下做到这一点——以下是方法:在您的YourAppNameHere-Prefix.pch文件中(这是Xcode中预编译头文件的默认名称),将Constants.h导入 #ifdef __OBJC__内。

#ifdef __OBJC__
  #import <UIKit/UIKit.h>
  #import <Foundation/Foundation.h>
  #import "Constants.h"
#endif

还要注意的是,Constants.h和Constants.m文件中除了已接受答案中描述的内容之外,绝不能包含其他任何内容。(不包括接口或实现部分)。


我已经尝试了这个,但是一些文件在编译时会抛出错误:“使用未声明的标识符'CONSTANTSNAME'”。如果我在抛出错误的文件中包含constant.h,则可以解决问题,但这不是我想要做的。我已经清理了代码并关闭了Xcode,重新构建,但仍然存在问题...有什么想法吗? - J3RM

51

我通常使用Barry Wark和Rahul Gupta发布的方式。

虽然我不喜欢在.h和.m文件中重复相同的单词。 注意,在下面的示例中,这一行在两个文件中几乎完全相同:

// file.h
extern NSString* const MyConst;

//file.m
NSString* const MyConst = @"Lorem ipsum";
因此,我想要做的是使用一些C预处理器机制。让我通过一个例子来解释一下。
我有一个头文件,其中定义了宏STR_CONST(name, value)
// StringConsts.h
#ifdef SYNTHESIZE_CONSTS
# define STR_CONST(name, value) NSString* const name = @ value
#else
# define STR_CONST(name, value) extern NSString* const name
#endif

在我的.h/.m文件对中常量进行定义,我按照以下方式操作:

// myfile.h
#import <StringConsts.h>

STR_CONST(MyConst, "Lorem Ipsum");
STR_CONST(MyOtherConst, "Hello world");

// myfile.m
#define SYNTHESIZE_CONSTS
#import "myfile.h"

然后,我只需要查看.h文件中所有常量的信息即可。


嗯,然而有一点要注意,如果头文件被导入预编译头文件中,就不能像这样使用这个技术,因为它不会将.h文件加载到.m文件中,因为它已经被编译过了。但是有一种方法——请参见我的答案(因为我无法在评论中插入漂亮的代码)。 - Scott Little
我无法让它工作。如果我将#define SYNTHESIZE_CONSTS放在#import "myfile.h"之前,它会在.h和.m文件中同时使用NSString*...(使用助手视图和预处理器进行检查)。这会引发重新定义错误。如果我将其放在#import"myfile.h"之后,则在两个文件中都会使用extern NSString*...。然后会引发“未定义符号”错误。 - arsenius

29

我自己有一个头文件,专门用于声明常量NSString,用于类似偏好设置的用途,就像这样:

extern NSString * const PPRememberMusicList;
extern NSString * const PPLoadMusicAtListLoad;
extern NSString * const PPAfterPlayingMusic;
extern NSString * const PPGotoStartupAfterPlaying;

然后在相应的.m文件中声明它们:

NSString * const PPRememberMusicList = @"Remember Music List";
NSString * const PPLoadMusicAtListLoad = @"Load music when loading list";
NSString * const PPAfterPlayingMusic = @"After playing music";
NSString * const PPGotoStartupAfterPlaying = @"Go to startup pos. after playing";

这种方法对我非常有帮助。

编辑:请注意,如果字符串在多个文件中使用,则此方法效果最佳。如果只有一个文件使用它,则可以在使用该字符串的 .m 文件中执行 #define kNSStringConstant @"Constant NSString"


25

以下是对@Krizz建议的细微修改,以便在需要包含常量头文件的PCH中正常工作,这是相当正常的。由于原始文件已导入PCH,因此它不会重新加载到.m文件中,因此您得不到符号,连接器会出错。

但是,以下修改允许其正常工作。它有点复杂,但它能工作。

您将需要3个文件:.h文件用于常量定义,.h文件和.m文件,我将分别使用ConstantList.hConstants.hConstants.mConstants.h的内容很简单:

// Constants.h
#define STR_CONST(name, value) extern NSString* const name
#include "ConstantList.h"

Constants.m文件看起来像:

// Constants.m
#ifdef STR_CONST
    #undef STR_CONST
#endif
#define STR_CONST(name, value) NSString* const name = @ value
#include "ConstantList.h"

最后,ConstantList.h文件包含了实际的声明,就是这些:

// ConstantList.h
STR_CONST(kMyConstant, "Value");
…

需要注意的几点:

  1. 我不得不在.m文件中重新定义宏,在使用#undef取消定义之后才能使用该宏。

  2. 我还必须使用#include而不是#import,以使其正常工作并避免编译器看到先前预编译的值。

  3. 每当更改任何值时,这将需要重新编译您的PCH(可能是整个项目),如果它们被分开(并且重复)存储,则情况就不同。

希望对某些人有所帮助。


1
使用 #include 解决了我的这个头疼问题。 - OdieO
与被接受的答案相比,这种方法是否会有性能/内存损失? - Gyfis
与已接受的答案相比,就性能而言是没有区别的。从编译器的角度来看,它们实际上是完全相同的。你最终得到了相同的声明。如果你用FOUNDATION_EXPORT替换上面的extern,它们将完全相同。 - Scott Little

14
// Prefs.h
extern NSString * const RAHUL;

// Prefs.m
NSString * const RAHUL = @"rahul";

12

正如Abizer所说,你可以将它放入PCH文件中。另一种不那么粗糙的方法是为所有密钥创建一个包含文件,然后将其包含在使用密钥的文件中,或将其包含在PCH中。将它们放在自己的包含文件中,至少可以让你在一个地方查找和定义所有这些常量。


11

如果您需要像全局常量一样的东西;一个快速而简单的方法是将常量声明放入 pch 文件中。


7
编辑.pch通常不是最好的选择。你需要找到一个真正定义变量的地方,几乎总是在.m文件中,所以更有意义的做法是在相应的.h文件中声明它。如果您需要在整个项目中使用它们,则创建Constants.h/m对的接受答案是一个好方法。我通常会尽可能将常量放置在层次结构的较低位置,根据它们将被使用的位置。 - Quinn Taylor

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