关于 @synthesize 的问题

14

当你从Xcode创建一个嵌入了CoreData的新应用程序时,你会在委托的实现文件中得到这些行:

@synthesize window=_window;

@synthesize managedObjectContext=__managedObjectContext;

使用单个下划线和双个下划线有什么区别?仅写一个下划线和写两个下划线有何不同:

@synthesize window;

3
这个问题已经被问烂了。为了帮助查找现有的问题,“_”被称为下划线。 - BoltClock
@BoltClock 谢谢你的提示... 我会进行更好的搜索。 - Matteo Alessani
2个回答

27

前导下划线是一种命名约定,有助于区分实例变量和访问器。对于编译器来说,这只是一个常见的ivar重命名。

考虑以下差异(非ARC代码):

self.date = [NSDate date];  // OK, the setter releases the old value first
date = [NSDate date];       // WRONG, skipping the setter causes a memory leak
_date = [NSDate date];      // WRONG but easier to see it's not a local variable

使用ARC,变量不会泄漏,但跳过@property属性仍然是错误的:

@property (copy) string;
// ...
self.string = someString;   // OK, string is copied
string = someString;        // WRONG string is retained but not copied
_string = someString;       // WRONG but hopefully easier to see

甚至更糟的是,一些API(如Core Data)依赖于KVC通知来执行延迟加载。如果您意外地跳过访问器,则数据将返回为nil。
这就是为什么您经常会发现@synthesize var=_var,它使:
- self.var成为访问器引用(调用设置器和获取器), - _var成为直接访问引用(跳过设置器和获取器), - 而var则成为无效引用。
鉴于@synthesize var=_var在省略@synthesize时由LLVM 4.0自动生成,因此您可以将其视为Objective-C中的默认命名约定。
继续阅读以获取详细信息...

现代运行时

在Objective-C 2.0中,您可以这样声明变量:

@interface User : NSObject
@property (nonatomic, assign) NSInteger age;
@end
@implementation User {
@synthesize age; // this line can be omitted since LLVM 4.0
@end

编译器将其翻译如下:

@interface User : NSObject {
    NSInteger age;
}
@end
@implementation User
-(void)setAge:(NSInteger)newAge {
    age=newAge;
}
-(void)age {
    return age;
}
@end

如果您喜欢使用下划线命名法,请添加以下内容:
@synthesize age=_age;

这就是你所需要的,因为在现代运行时中,如果你没有提供实例变量,编译器会为你添加一个。以下是编译后的代码:

@interface User : NSObject {
    NSInteger _age;
}
@end
@implementation User
-(void)setAge:(NSInteger)newAge {
    _age=newAge;
}
-(void)age {
    return _age;
}
@end

如果同时添加ivar和@property会发生什么?如果变量名称和类型相同,则编译器会使用它而不是生成新变量。引用“The Objective-C Programming Language>Declared Properties>Property Implementation Directives”:
“访问器合成的行为因运行时而异:
对于现代运行时,根据需要合成实例变量。如果同名实例变量已经存在,则使用它。 对于旧版运行时,必须在当前类的@interface块中声明实例变量。如果同名实例变量存在且其类型与属性的类型兼容,则使用它。否则,您将收到编译器错误。”
遗留运行时
但是,如果需要支持旧版运行时,则必须提供具有与属性兼容类型的相同名称的实例变量或在@synthesize语句中指定另一个现有的实例变量
因此,没有下划线的旧代码如下:
@interface User : NSObject {
    NSInteger age;
}
@property (nonatomic, assign) NSInteger age;
@end
@implementation User
@synthesize age;
@end

或者如果您更喜欢下划线约定:

@interface User : NSObject {
    NSInteger _age;
}
@property (nonatomic, assign) NSInteger age;
@end
@implementation User
@synthesize age = _age;
@end

什么是最佳实践?

苹果不建议在方法中使用下划线,但变量可以使用!

苹果关于方法的规定:Cocoa编码指南:排版约定

避免使用下划线作为表示私有的前缀,尤其是在方法中。苹果保留了这种惯例。第三方使用可能会导致命名空间冲突;他们可能会无意中用自己的方法覆盖一个现有的私有方法,造成灾难性后果。

苹果关于变量的规定:声明属性和实例变量

确保实例变量的名称简洁地描述存储的属性。通常,您不应直接访问实例变量,而应使用访问器方法(在init和dealloc方法中,确实需要直接访问实例变量)。为了帮助表明这一点,应将实例变量名以下划线(_)作为前缀,例如:@implementation MyClass { BOOL _showsTitle; }

ISO/IEC 9899 7.1.3 保留标识符(也称为 C99):

  • 所有以下划线和大写字母或另一个下划线开头的标识符始终保留任何用途。
  • 所有以下划线开头的标识符始终保留为普通名称空间和标记名称空间中具有文件范围的标识符。

除此之外,双下划线前缀通常保留给预处理器/编译器/库的供应商。这避免了您在代码中使用 __block,而 Apple 将其引入作为新的非标准关键字的情况。

Google Objective-C 样式指南

变量名 变量名以小写字母开头,并使用混合大小写来分隔单词。类成员变量带有尾部下划线。例如:myLocalVariable、myInstanceVariable_。用于 KVO/KVC 绑定的成员变量可以以前导下划线开头,如果不允许使用 Objective-C 2.0 的 @property。

Google 的尾部下划线不会强制您在 Xcode 启动自动完成之前再输入一个字符,但是如果下划线是后缀,您会更慢地意识到它是实例变量。

在C++中,使用前导下划线也是不鼓励的(参见C++标识符中使用下划线的规则是什么?),在Core Data属性中也是一样(尝试在模型中添加前导下划线,你会得到“名称必须以字母开头”的错误信息)。

无论你选择什么方式,冲突的可能性都很小,如果确实发生了冲突,编译器会给出警告。当有疑问时,可以使用默认的LLVM方式:@synthesize var=_var;


我对Mark Dalrymple的文章A Motivation for ivar decorations进行了编辑,建议你去看看。


1
你需要提供一些引用。至少要提到这仅适用于现代运行时。第三项非常可疑。 - Abizern
顺便提一下,在实例变量名后面添加下划线 _ 似乎是一种相当普遍的风格,例如在 Google Objective-C Style Guide 中。我认为这源于 Apple 曾经使用前导下划线保留 ivars 名称(尽管我找不到相关参考资料...难道我是在做梦吗?) - Daniel Dickison
苹果公司保留了以下划线开头的方法名。您可以在iVar上放置一个下划线而没有限制。 - Abizern
我认为你仍然需要在iOS中声明一个ivar,而no-ivar-needed只适用于OSX应用程序。 - Joey Gibson

4

如果你的实例变量名为'window',那么你可以只使用

@synthesize window;

然而,有些人使用在所有实例变量前缀加下划线的命名约定,但仍然希望其getter和setter没有下划线前缀,这就是'window=_window'的含义。

我不知道双下划线的含义,但我猜测这也是一种命名约定的问题。


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