关于Objective-C类别和扩展的细节

时间:2011-01-13 21:49:30

标签: objective-c

我在尝试弄清楚为什么我在私有类别中声明的readwrite属性没有生成setter时学到了新东西。这是因为我的类别被命名为:

// .m
@interface MyClass (private)
@property (readwrite, copy) NSArray* myProperty;
@end

将其更改为:

// .m
@interface MyClass ()
@property (readwrite, copy) NSArray* myProperty;
@end

我的二传手是合成的。我现在知道,类扩展不仅仅是匿名类别的另一个名称。保留一个未命名的类别会导致它变成一个不同的野兽:一个现在提供编译时方法实施强制并允许你添加ivars。我现在理解每个基础的一般原理:类别通常用于在运行时向任何类添加方法,而类扩展通常用于强制私有API实现并添加ivars。我接受了。

但有些小事让我感到困惑。首先,在高层:为什么要这样区别?这些概念看起来像是类似的想法,无法决定它们是相同的还是不同的概念。如果它们是相同的,我希望使用没有名称的类别和具有命名类别(它们不是)的类别可以完全相同。如果它们不同,(它们是)我会期望两者之间存在更大的语法差异。看起来很奇怪,“哦,顺便说一句,要实现一个类扩展,只需写一个类别,但不要写出名字。它会神奇地改变。”

其次,关于编译时强制的主题:如果你不能在命名的类别中添加属性,为什么这样做会使编译器说服你这样做呢?为了澄清,我将用我的例子来说明。我可以在头文件中声明一个readonly属性:

// .h
@interface MyClass : NSObject
@property (readonly, copy) NSString* myString;
@end

现在,我想转到实现文件并给自己私有的readwrite访问属性。如果我做得正确:

// .m
@interface MyClass ()
@property (readwrite, copy) NSString* myString;
@end

当我不合成时,我得到一个警告,当我这样做时,我可以设置属性,一切都很好。但是,令人沮丧的是,如果我碰巧有点误导了类别和类扩展之间的区别,我会尝试:

// .m
@interface MyClass (private)
@property (readwrite, copy) NSString* myString;
@end

编译器完全平心以为该属性是readwrite。我没有得到任何警告,甚至没有好的编译错误“对象无法设置 - 无论是readonly属性还是没有找到setter”,在设置myString时我都没有在类别中声明readwrite属性。我只是在运行时获得“不响应选择器”异常。如果(命名)类别不支持添加ivars和属性,那么要求编译器按相同规则播放是否太过分了?我错过了一些宏伟的设计理念吗?

4 个答案:

答案 0 :(得分:53)

在Objective-C 2.0中添加了类扩展来解决两个特定问题:

  1. 允许对象具有由编译器检查的“私有”接口。
  2. 允许可公开阅读的私有可写属性。
  3. 专用接口

    在Objective-C 2.0之前,如果开发人员希望在Objective-C中拥有一组方法,他们通常会在类的实现文件中声明一个“私有”类别:

    @interface MyClass (Private)
    - (id)awesomePrivateMethod;
    @end
    

    但是,这些私有方法经常被混合到类的@implementation块中(不是 @implementation类别的单独Private块。那么为何不?这些并不是课堂的真正延伸;它们只是弥补了Objective-C类别中缺乏公共/私人限制。

    问题是Objective-C编译器假设在类别中声明的方法将在别处实现,因此他们不会检查以确保方法已实现。因此,开发人员可以声明 awesomePrivateMethod但未能实现它,并且编译器不会警告他们这个问题。这就是你注意到的问题:在一个类别中,你可以声明一个属性(或一个方法),但是如果你从未实际实现它就不会得到警告 - 这是因为编译器希望它“在某个地方”实现(很可能) ,在另一个独立于此的编译单元中。)

    输入课程扩展名。假定在类扩展中声明的方法在主@implementation块中实现;如果不是,编译器将发出警告。

    可公开阅读的私有可写属性

    实现不可变数据结构通常是有益的 - 也就是说,外部代码不能使用setter来修改对象的状态。但是,为内部使用可写属性仍然很好。类扩展允许:在公共接口中,开发人员可以将属性声明为只读,但随后在类扩展中声明它是可写的。对于外部代码,该属性将是只读的,但可以在内部使用setter。

    那为什么我不能在类别中声明可写属性?

    类别无法添加实例变量。设定者通常需要某种后备存储。决定允许类别声明可能需要后备存储的属性是A Bad Thing™。因此,类别不能声明可写属性。

    他们看起来很相似,但却不同

    混淆在于类扩展只是一个“未命名的类别”。语法类似,暗示了这个想法;我想它只是被选中,因为它对Objective-C程序员很熟悉,在某些方面,类扩展就像是类别。它们相似,因为这两个功能都允许您向现有类添加方法(和属性),但它们用于不同的目的,从而允许不同的行为。

答案 1 :(得分:8)

你对句法的相似性感到困惑。类扩展只是一个未命名的类别。类扩展是一种将接口的一部分设为私有和部分公共的方法 - 两者都被视为类的接口声明的一部分。作为类接口的一部分,必须将扩展定义为类的一部分。

另一方面,类别是一种在运行时向现有类添加方法的方法。例如,这可能是一个仅在星期四加载的单独包。

对于Objective-C的大部分历史记录,在加载类别时,无法在运行时向类添加实例变量。这在最近的新运行时已经解决了,但该语言仍然显示其脆弱的基类的伤疤。其中之一是该语言不支持添加实例变量的类别。你必须自己写出吸气剂和制定者,老派风格。

类别中的实例变量也有些棘手。由于在创建实例时它们不一定存在且初始化程序可能对它们一无所知,因此初始化它们是普通实例变量不存在的问题。

答案 2 :(得分:3)

您可以在类别中添加属性,但无法合成它。如果使用类别,则不会收到编译警告,因为它希望在类别中实现setter。

答案 3 :(得分:0)

对于未命名类别(现在称为类扩展)和普通(命名)类别的不同行为的REASON稍作澄清。

事情很简单。您可以在运行时加载多个类,扩展同一个类,而无需编译器和链接器知道。 (考虑一下人们写给NSObject的许多美妙的扩展,即事后添加它的功能)。

现在Objective-C没有NAME SPACE的概念。因此,在命名类别中定义iVars可能会在运行时创建符号冲突。如果两个不同的类别能够定义相同的

@interface myObject (extensionA) {
 NSString *myPrivateName;
}
@end
@interface myObject (extensionB) {
 NSString *myPrivateName;
}
@end

然后至少会在运行时出现内存溢出。

相反,类扩展没有名称,因此只能有一个。这就是为什么你可以在那里定义iVars的原因。确保它们是独一无二的。

至于与类别和类扩展+ ivars和属性定义相关的编译器错误和警告,我必须同意它们没有那么有用,我花了太多时间试图理解为什么编译或不编译,以及它们如何在编译之后工作(如果他们工作)。