静态常量 Vs 外部常量

22

我一直在我的头文件中使用静态常量,例如:

static NSString * const myString = @"foo";

但我已经读到过,这不是做这件事情的“安全”或正确方法。 显然,如果我想让我的const字符串可以从另一个类中访问,我应该在我的.h文件中声明字符串:

但我已经阅读到这不是这种操作的“安全”或正确方式。显然,如果我想让别的类能够访问我的const字符串,我应该在我的.h文件中声明该字符串:

extern NSString * const myString;

然后在我的.m文件中:

NSString * const myString = @"foo";

这样做是否正确?如果是,为什么不直接在我的.h文件中声明它为静态的呢?它完全可以正常工作,我没有看到任何与此相关的“安全”问题。它是一个const,因此它不能从外部更改,并且它是我故意需要在类外访问的东西。我唯一能想到的另一件事就是隐藏字符串的值吗?


请阅读一本好的"C"书籍,深入理解"C"是编写代码的良好基础。 - zaph
你可以随时更改静态变量的值,const 不会以任何方式阻止这种更改。而 extern 则会限制更改,因为你只能在声明语句中更改变量的值,所以无法在运行时更改变量。 - Jonathan Cichon
7个回答

41

你的第一个版本

static NSString * const myString = @"foo"; // In .h file, included by multiple .m files

在每个“翻译单元”(大致上是每个.m源文件)中,使用头文件定义一个名为myString的变量。所有字符串对象都具有相同的内容“foo”,但可能是不同的对象,因此myString(指向字符串对象的指针)的值在每个单元中可能会不同。

您的第二种选择

extern NSString * const myString; // In .h file, included by multiple .m files
NSString * const myString = @"foo"; // In one .m file only

定义了一个单一的变量myString,该变量在全局范围内可见。

示例:在一个类中,您使用myString作为用户对象发送通知。 在另一个类中,接收到该通知并将用户对象与myString进行比较。

在第一种情况下,比较必须使用isEqualToString:,因为发送和接收类可能具有不同的指针(都指向内容为"foo"的NSString对象)。因此,使用==进行比较可能会失败。

在第二种情况下,只有一个myString变量,因此可以使用==进行比较。

因此,从“共享字符串”的角度来看,第二种情况更加安全,因为在每个翻译单元中,“共享字符串”是相同的对象。


3
我不知道在使用Objective-C时,有什么理由将任何内容声明为外部内容。但是,当您将其与C或汇编程序模块混合使用时,可能会出现这种情况。
然而,使用extern的好处是,如果您真的需要节省大约20个字节的空间,常量将只在整个项目中存在一次。但是这带来了名称冲突的风险。其他库可能已经使用相同的名称声明了它们自己的外部内容。链接器会为它们使用内存中的完全相同的空间,尽管它们可能是不同类型的。
是的,在头文件中的extern声明应该伴随着.m文件中对应的定义。我不确定,但我认为您可以在.h文件中分配@"foo"。您甚至可以在@interface/@implementation-@end块之外声明它(我从未尝试过)。在这种情况下,变量将是全局的,并且即使没有extern关键字,也可以从任何地方访问。在编译时,编译器会抱怨在# include语句链中看不到其声明时访问它们。但是学术上,一个单独的.m文件可能包含两个或更多类(我绝对不建议这样做),然后该变量将从两个类中都可以访问,尽管它属于其中的一个。
最后,Objective-C只是ANSI C的附加组件。但是,将它们设置为静态变量没有意义。这些常量已经是静态的。在类甚至方法内部使用静态变量的目的是其范围(可见性)仅限于该类,但在运行时只有一个实例由该类的所有实例共享。
例如:
@implementation AClass : NSObject 

static NSString *someString 

- (void) setString:(NSString*) aString{
  someString = aString;
}

- (NSString*) getString (){
  return someString;
}

... and somewhere else:

AClass * a = [[AClass alloc] init];
AClass * b = [[AClass alloc] init]; 

[a setString:@"Me"];
[b setString;@"You"];
NSLog (@"String of a: ", [a getString]);

将会输出You但不包括Me

如果这正是你想要的,并且仅在这种情况下,使用静态方法。

使用简单的预处理宏(我更喜欢这种方法,但我有点老派)的缺点是这些字符串每次使用宏时都会被复制到二进制文件中。显然,这对你来说根本不是一个选择,因为你甚至没有要求它们。然而,对于大多数用途,在常见的共享.h文件中使用预处理宏可以解决跨类管理常量的问题。


这些常量无论如何都是静态的。它们是常量。据我所知,常量不一定要是静态的。 - Franklin Yu
@Franklin:在这种情况下,“static”表示“类”的成员,而不是“实例”的成员;如果不是静态的,则输出将为“Me”。 - tontonCD
@tontonCD,我说常量不必是静态的。你说变量someString不是常量)是静态的。这两个陈述并不相互冲突,所以我很困惑你在反对什么。 - Franklin Yu

1
其他答案提到,使用extern的方式可以节省一些内存(虽然很少),并且允许您通过==运算符进行比较(这本来就不是一个好习惯)。另外,如果您正在编写动态链接库,还有另一个优点:由于只有一个数据副本,您可以在下一个发布版本中原子地更改数据,而无需要求客户重新编译代码。

如果变量不是一个对象(例如您的情况中的NSString),而是一个基本类型(如整数),则静态方式的一个可能的优点是,编译器能够优化对常量的访问:

When you declare a const in your program,

int const x = 2;

Compiler can optimize away this const by not providing storage to this variable rather add it in symbol table. So, subsequent read just need indirection into the symbol table rather than instructions to fetch value from memory.

这个回答中获取。

1
在头文件中使用static NSString* const myString = @"foo";意味着每个翻译单元都有一个单独的myString变量。我认为链接器可能会合并它们,但我不会指望它。这意味着使用if (someString == myString) ...比较接收到的字符串的代码,即使调用方来自不同的翻译单元,也可能得到false,如果调用方传递了myString。(当然,代码应该使用- isEqualToString:而不是==,但对于正确声明的字符串常量,后者可能可行。)

0

在存储类方面,静态有两种含义。

方法或函数内的静态变量会在调用之间保留其值。

全局声明的静态变量可以被任何函数或方法调用,只要这些函数出现在与静态变量相同的文件中。静态函数也是如此。

而静态使函数和变量在特定文件中全局可见,extern使它们在所有文件中全局可见。

每当您的应用程序在公共接口中使用具有非语言值的字符串常量时,应将其声明为外部字符串常量。

模式是在公共头文件中声明一个extern NSString * const,并在实现中定义该NSString * const。

来源:nshipster.com/c-storage-classes


当你引用某些内容时,请始终指明其来源。 - Lundin

0
但是我读到这种做法并不安全或正确。
除非程序是多线程的,否则它是安全的。在这种情况下,除非您使用互斥锁保护全局变量,否则它是不安全的。
然而,这并不正确。NSString * const myString 意味着一个指向(非常量)数据的常量指针。最有可能的是,您想要变量本身是常量:const NSString* myString。
显然,如果我希望从另一个类访问我的常量字符串,我应该在我的.h中声明字符串为extern。
正确。请注意,extern仅适用于常量。对于非常量变量,它是不可接受的:全局变量被认为是非常糟糕的实践。
那么,为什么不直接在我的.h文件中将其声明为静态变量,因为它完美地工作?
唯一需要将其声明为static的原因是因为您想将变量的范围限制为本地.c文件,换句话说是私有封装。因此,在.h文件中声明静态变量从来没有任何意义。
“它是一个const,因此无法从外部更改。”
它是可以更改的,因为它不是const,请参阅我的第一个备注。
通常情况下,不要做这样的事情。所有上述的调整都表明您的程序设计存在缺陷,需要进行修复。您需要以面向对象的方式设计程序,使得每个代码模块都是自主的,并且除了它指定的任务之外,不知道也不关心任何其他事情。

2
const NSString* myString 是错误的。首先,它意味着指针不是常量,因此可能会被指向其他内容。这违反了 myString 是常量的意图。其次,NSString 已经是不可变的,所以将指向的对象设置为 const 没有帮助。这也可能是有害的,因为不可变并不一定意味着严格的 const,我们不希望强制对对象施加这个要求。例如,当使用不同编码请求其内容时,NSString 可能会进行内部缓存,应该允许这样做。 - Ken Thomases
@KenThomases 为什么有人会从库中取出一个常量并将其设置为指向其他东西?这没有意义。但如果你担心这个问题,我猜你可以将它定义为 const NSString* const myString。然而,在我的经验中,const * const 大多数时候只是表明程序员过于多疑,因此增加了无意义的混乱。如果对象本身是不可变的,所有这些都似乎是多余的。如果一个类被设计为不可变的,它在对象构造期间以外不应该去操纵任何内部,否则按照定义它就不是不可变的。 - Lundin
首先,如果有人可以设置它,那么按照墨菲定律,他们很可能会意外设置。例如,当他们本意是使用 == 时不小心输入了 =。第二,“不可变”只在其公共接口所展示的外部可见方面。这就是为什么 C++ 有 mutable 关键字的原因。缓存是优化的实现细节,并不会改变从外部看到的对象值。 - Ken Thomases
@KenThomases 通过使用启用警告或静态分析器等专业工具,可以避免墨菲定律的影响。如果由于一些实现细节而无法使对象成为const不可变,则不要将该类用作不可变类,简单明了。当然,人们可能会质疑在现实世界中是否有任何地方都需要使用不可变设计模式,但这是其他论坛上的讨论内容... - Lundin

-1
#define myString @"foo"

这是一个例子的原因:

您将能够在编译时连接字符串:

NSLog(@"%@", @"Say hello to " myString);

将输出:向foo打招呼


你读了问题吗?OP要求提供有关声明常量的两种方式的解释:静态 vs 外部。 - Vlad Papko
@Visput,我的答案是没有一个,最好使用#define - Cy-4AH
1
@Cy-4AH 定义不是一个好的方法。定义会移除数据类型的可见性!在一个好的团队中,这样的代码不会通过代码审查。 - ssh88
@HermannKlecker,我的意思不仅仅是在这种情况下。我的一般化建议是尽可能避免在数据类型上使用#define宏,理由就像您所建议的那样。 - ssh88
@Cy-4AH 苹果公司建议使用 extern 进行声明: https://developer.apple.com/library/mac/documentation/cocoa/conceptual/codingguidelines/Articles/NamingIvarsAndTypes.html#//apple_ref/doc/uid/20001284-1003095 ...为此类目的使用字符串常量定义常量,例如通知名称和字典键。通过使用字符串常量,您可以确保编译器验证指定了正确的值(即,它执行拼写检查)。 - Tariq
显示剩余8条评论

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