对Kevin的回答进行详细说明:
当您声明一个类时,例如:
@interface SomeClass : NSObject {
@public
id publicIvar;
@protected
id protectedIvar;
@private
id privateIvar;
}
@end
编译器1为该类决定实例变量布局。此布局确定了相对于该类实例地址的实例变量偏移量。一种可能的布局如下:
+--> publicIvar address = instance address + offsetOfPublicIvar
|
|
+-----+------------+-----+---------------+-----+-------------+-----+
| ... | publicIvar | ... | protectedIvar | ... | privateIvar | ... |
+-----+------------+-----+---------------+-----+-------------+-----+
|
|
+--> instance address
当代码中引用实例变量时,无论是在类的实现中还是在代码库的其他部分中,编译器都会将该引用替换为相应实例变量相对于相应实例地址的偏移量。
例如,在SomeClass的实现中,
privateIvar = someObject;
或者
self->privateIvar = someValue;
被翻译成类似于:
*(self + offsetOfPrivateIvar) = someObject;
同样地,在类的外部,
SomeClass *obj = [SomeClass new];
obj->publicIvar = someObject;
被翻译成类似于:
SomeClass *obj = [SomeClass new];
*(obj + offsetOfPublicIvar) = someObject;
然而,编译器只允许根据实例变量的可见性进行操作:
- 私有实例变量只能在相应类的实现中引用;
- 受保护的实例变量只能在相应类及其子类的实现中引用;
- 公共实例变量可以在任何地方引用。
当一个实例变量在类扩展中声明时,例如:
@interface SomeClass () {
id extensionIvar;
}
@end
编译器将其添加到实例变量布局中:
+-----+------------+-----+---------------+
| ... | otherIvars | ... | extensionIvar |
+-----+------------+-----+---------------+
任何对该实例变量的引用都将被替换为相应的偏移量,以便于该实例。然而,由于该实例变量仅在声明类扩展的实现文件中已知,因此编译器不允许其他文件引用它。任意源文件只能引用它所知道的实例变量(遵守可见性规则)。如果实例变量在由源文件导入的头文件中声明,则源文件(或更准确地说,翻译该单元的编译器)会意识到它们。
另一方面,扩展变量仅由声明它的源文件知道。因此,我们可以说在类扩展中声明的实例变量对其他文件是隐藏的。相同的推理适用于在类扩展中声明的属性的支持实例变量。这类似于@private
,但更加严格。
然而,请注意,在运行时,可见性规则不受强制执行。使用键值编码,任意源文件有时(规则在这里描述)可以访问实例变量:
SomeClass *obj = [SomeClass new];
id privateValue = [obj valueForKey:@"privateIvar"];
包括在扩展中声明的实例变量:
id extensionValue = [obj valueForKey:@"extensionIvar"];
无论使用KVC与否,都可以通过Objective-C运行时API访问实例变量:
Ivar privateIvar = class_getInstanceVariable([SomeClass class],
"privateIvar");
Ivar extensionIvar = class_getInstanceVariable([SomeClass class],
"extensionIvar");
id privateValue = object_getIvar(obj, privateIvar);
id extensionValue = object_getIvar(obj, extensionIvar);
请注意,一个类可以有多个类扩展。然而,一个类扩展不能声明与另一个实例变量相同名称的实例变量,包括在其他类扩展中声明的实例变量。由于编译器会发出像这样的符号:
_OBJC_IVAR_$_SomeClass.extensionIvar
对于每个实例变量,使用不同的扩展名来声明相同名称的实例变量不会导致编译器错误,因为给定的源文件不能感知另一个源文件,但它确实会产生链接器错误。
1这种布局可以被Objective-C运行时更改。实际上,偏移量由编译器计算并存储为变量,运行时可以根据需要更改它们。
PS:这个答案中的并非所有内容都适用于所有编译器/运行时版本。我只考虑了具有非脆弱ABI和最新版本Clang/LLVM的Objective-C 2.0。