【问题标题】:Minutia on Objective-C Categories and Extensions关于 Objective-C 类别和扩展的细节
【发布时间】:2011-06-08 19:29:31
【问题描述】:

在试图弄清楚为什么我在私有类别中声明的读写属性没有生成设置器时,我学到了一些新东西。这是因为我的类别被命名为:

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

改成:

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

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

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

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

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

现在,我想转到实现文件并授予自己对该属性的私有读写访问权限。如果我做得对:

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

当我不合成时会收到警告,当我合成时,我可以设置属性,一切都很好。但是,令人沮丧的是,如果我碰巧对 Category 和 Class Extension 之间的区别略有误导,我会尝试:

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

编译器完全放心,认为该属性是可读写的。在设置 myString 时,我没有收到任何警告,甚至没有很好的编译错误“无法设置对象 - 要么是只读属性,要么没有找到设置器”,如果我没有在类别中声明读写属性。我只是在运行时收到“不响应选择器”异常。如果(命名的)类别不支持添加 ivars 和属性,那么要求编译器遵循相同的规则是不是太过分了?我是否错过了一些宏大的设计理念?

【问题讨论】:

  • 我相信您的意思是 readwrite 用于私有 myString 属性声明。我进行了编辑以反映这一点;希望这是正确的。

标签: objective-c


【解决方案1】:

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

  1. 允许对象具有由编译器检查的“私有”接口。
  2. 允许公共可读、私人可写的属性。

私有接口

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

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

但是,这些私有方法经常混入类的 @implementation 块(不是Private 类别的单独 @implementation 块)。那么为何不?这些并不是类的真正扩展;它们只是弥补了 Objective-C 类别中公共/私有限制的不足。

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

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

公共可读、私有可写的属性

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

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

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

它们看起来相似,但不同

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

【讨论】:

  • 很好的解释。我熟悉在类的@implementation 中实现的声明私有方法的pre 2.0 私有Category 方法。我显然也错过了关于扩展出现的备忘录。对我来说,似乎许多开发人员只是选择了不那么明确的方法来不命名他们的私有类别,这是我不喜欢的。我需要找到的道德基本上是没有未命名的类别之类的东西。谢谢。
【解决方案2】:

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

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

在 Objective-C 的大部分历史中,在加载类别时,不可能在运行时向类添加实例变量。最近在新的运行时已经解决了这个问题,但是该语言仍然显示出其脆弱的基类的伤痕。其中之一是该语言不支持添加实例变量的类别。你必须自己写出 getter 和 setter,老派风格。

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

【讨论】:

  • 是的,我对句法相似性感到困惑 =)。我可以看到扩展是一个与类别根本不同的概念。我想知道为什么语法上如此小的差异会导致如此不同的行为。对于那些不熟悉扩展的人(我,大约是昨天),扩展在语法上是一个未命名的类别。
  • @Matt Wilding:我们将不得不希望 Apple 的 Cocoa 团队中的一个人停下来为“为什么”提供官方答案。秘密类别是向类添加“私有”行为的旧方法,所以我的假设是使用类似的语法来进行高级替换似乎很方便。我怀疑他们是否意识到对于那些一直没有使用 Cocoa 的人来说这会显得多么令人困惑。
  • 我们想不出更好的了,扩展确实有点像语义稍有不同的类别。当时看起来很自然。
  • @bbum:感谢官方的洞察。我很好奇,在将类扩展添加到 Objective-C 之前,声明一个没有名称的类别有什么作用?是编译错误,还是类扩展取代了这样做的能力?
  • 我不记得了,但我几乎可以肯定这是一个编译错误(这就是我们可以将它用于扩展的原因——你会对“辉煌”的数量感到惊讶由于 C++ 中的一些不可靠的有效性,实际上不可能的句法发明)。
【解决方案3】:

你可以在一个类别中添加一个属性,你只是不能合成它。如果使用类别,则不会收到编译警告,因为它希望在类别中实现 setter。

【讨论】:

  • 当然。我应该想到的。我花了很长时间在类别中声明私有方法,这些方法在类的@implementation 中实现,我忽略了类别通常有自己的独立实现。
【解决方案4】:

稍微澄清一下未命名类别(现在称为类扩展)和正常(命名)类别的不同行为的原因。

事情很简单。您可以让许多类别扩展同一个类,在运行时加载,而编译器和链接器并不知道。 (想想人们写给 NSObject 的许多漂亮的扩展,它们在事后添加了它的功能)。

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

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

那么至少在运行时会出现内存溢出。

相反,类扩展没有名称,因此只能有一个。这就是您可以在那里定义 iVar 的原因。它们肯定是独一无二的。

至于与类别和类扩展 + ivars 和属性定义相关的编译器错误和警告,我不得不承认它们并没有太大帮助,而且我花了太多时间试图理解为什么事情会编译,以及它们是如何编译的编译后工作(如果他们工作)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-11-11
    • 2016-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多