私有的@property会创建一个@private实例变量吗?

7
我读过@synthesize会自动为@property创建相应的实例变量,并且ivars默认是@protected的。但是,如果我使用类扩展(如下所示)来指示@property方法是私有的,会怎样呢?
// Photo.m
@interface Photo ()
@property (nonatomic, retain) NSMutableData *urlData;
@end

那么相应的ivar是否会是@private?还是我需要像这样明确声明它为@private
// Photo.h
@interface Photo : Resource {
@private
    NSMutableData *urlData;
}
2个回答

31

对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。


尽管不鼓励这类评论:“非常感谢你提供如此丰富、简洁、启发性的答案。” - Madbreaks

12

@private 实例变量是编译时的特性。由于 @property 的支持 ivars 已经被隐藏了,所以 @private 没有任何作用。因此,从本质上讲,它已经是 @private

(说明:@private 是 Objective-C 语言中的修饰符,用于指定实例变量的访问权限。该段文字解释了 @private 在编译时的作用以及在使用 @property 后的实际效果。)


7
“匿名类别”这种东西不存在。在OP的问题中,这将是一个“类扩展”,与类别非常不同(尽管在特性方面类似)。请注意,此处仅为翻译,未包含解释和其他额外内容。 - bbum

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