【问题标题】:Quirk with Core Data, protocols, and readwrite vs. readonly property declarations核心数据、协议和读写与只读属性声明的怪癖
【发布时间】:2011-03-07 14:44:53
【问题描述】:

我遇到了一个奇怪的问题,涉及 Core Data、声明的协议,也许还有 LLVM 1.5 编译器。情况是这样的。

我有一个核心数据模型,其中有两个类,IPContainer 和 IPEvent,其中 IPContainer 是 IPEvent 的父实体。每个实体在项目中都有一个自定义类,使用 mogenerator 创建。 mogenerator 会生成一个仅包含建模属性声明的附加子类,因此类层次结构实际上是 IPEvent > _IPEvent > IPContainer > _IPContainer > NSManagedObject。 IPContainer 实体有一个名为“id”的属性,在_IPContainer.h 中声明为@property(nonatomic, retain) NSNumber* id;。 _IPContainer.m 在实现中有@dynamic id;,告诉Core Data 在运行时生成访问器

我的项目中还声明了一个协议 IPGridViewGroup,它定义了几个属性,其中一个是相同的“id”属性。但是,实现该协议的类不需要setter,因此协议中的属性声明为@property(readonly) NSNumber* id; IPEvent 类声明它符合IPGridViewGroup 协议。

使用 Clang/LLVM 1.0.x 编译器(无论 Xcode 3.2.2 附带的哪个版本)都可以正常工作,但是在升级到 Xcode 3.2.3 和 Clang/LLVM 1.5 后,一大堆事情发生了变化。首先,我在编译 IPEvent 类时收到以下警告:

/Volumes/Ratbert/Users/bwebster/Projects/UberProject/iPhotoLibraryManager/IPGridViewGroup.h:19:31: warning: property 'id' requires method 'id' to be defined - use @synthesize, @dynamic or provide a method implementation

然后,当我实际运行程序时,它会在控制台中打印出来:

Property 'id' is marked readonly on class 'IPEvent'.  Cannot generate a setter method for it.

紧随其后的是:

-[IPEvent setId:]: unrecognized selector sent to instance 0x200483900

我还尝试在 IPEvent 类上重新声明该属性,但这只是给了我一个不同的编译器警告,以及在运行时的相同行为:

/Volumes/Ratbert/Users/bwebster/Projects/UberProject/iPhotoLibraryManager/IPManagedObject/IPEvent.h:14:40: warning: property 'id' 'retain' attribute does not match the property inherited from 'IPGridViewGroup'

现在,这里唯一改变的是编译器,所以改变的催化剂很清楚,但我不知道这是否可以被认为是新版本编译器中的错误,或者如果旧版本的编译器实际上表现不正确,而新版本现在显示这是我自己的代码有问题。

所以我的问题包括:

  1. 似乎应该可以让一个类符合具有只读属性的协议,但在其自己的实现中为该属性提供读写访问权限,对吗?不过这里的怪癖是 readwrite 属性实际上是在符合协议的类的超类中声明的。
  2. 我假设控制台消息正在Core Data 内部的某个地方打印出来。不过这很奇怪,因为 IPEvent 本身并没有明确声明“id”属性,除非符合 IPGridViewGroup 协议。但是,如果是这种情况,那么我认为会出现编译器 error,因为它会有效地使用相同属性的只读版本覆盖读写属性(在 _IPContainer 超类中声明) ,AFAIK 通常不允许允许。
  3. 如果这是一个编译器错误,那很好,我现在可以通过几种不同的方式解决它。如果编译器在这里做正确的事情,那么我将无法想出一种方法来组织所有这些,因此我不会收到任何编译器警告或运行时错误。

编辑: 所以,解决方法是再次在 IPEvent 类上重新声明该属性,但我仍然对为什么两个版本的编译器行为不同感到困惑。还不清楚协议上声明的属性应该如何与类上声明的属性进行交互。

如果我在覆盖读写属性的类(而不是协议)中声明只读属性,我会收到消息“警告:属性 'longitude' 的属性 'readonly' 限制从 '_IPEvent 继承的属性的属性 'readwrite' '”。似乎如果在协议中声明它具有相同的效果,编译器应该会出现类似的警告。

不过,直观地说,我认为由于 IPEvent 已经为该属性实现了必要的 getter,这应该算作“符合协议”,即使它碰巧也为该属性实现了一个 setter。

【问题讨论】:

    标签: objective-c cocoa core-data llvm-clang


    【解决方案1】:

    现在,唯一改变的是 这是编译器,所以催化剂 因为变化很明显,但我 不知道这是否可以 被认为是新版本中的一个错误 编译器,或者如果旧版本的 编译器实际上在表现 不正确,现在新版本 表明这是我自己的代码 越野车。

    较新的编译器注意到,对于同一个类的同一个实例变量,您有两个不同的访问器定义。当然,链接器应该抱怨。

    旧的编译器应该把它踢回来。 @property 声明是一个隐式方法声明,无论它出现在类还是协议中。当你让一个类和一个协议都定义一个同名的属性时,你最终会得到一个类的两组方法声明。这显然会在某个地方引起问题。

    两个编译器之间的差异可能是微不足道的,例如源代码中#import 语句的顺序,甚至源文件的修改日期。

    您显然会遇到冲突,因为 IPContainer 类有两个动态方法定义,一个只生成一个 setter,另一个生成一个 setter 和一个 getter。编译器应该如何知道使用哪一个?你刚刚告诉它你想要一个只读的读写属性。更糟糕的是,由于这是一个动态属性,因此无法确定在运行时实际会生成什么。

    1 看起来应该没问题 有一个符合协议的类 具有只读属性,但提供 对 in 中的属性的读写访问权限 它自己的实现,是 对吗?

    定义“确定”。编译器会接受吗?大概。毕竟,在协议的只读属性中,您已经定义了一个 getter 方法,但在类中,您还定义了一个 setter 方法。由于协议不限制实现类可以具有的其他方法,因此可以添加 setter 方法,就像添加任何其他不相关的方法一样。

    但是,这显然非常非常危险,尤其是在 NSManagedObject 子类的情况下。托管对象上下文对它期望在它使用的类中找到什么有着非常坚定的期望。

    2 这很奇怪,因为 IPEvent 本身没有声明“id” 财产明确性,除了 符合 IPGridViewGroup 协议。

    如果协议需要该属性,则通过采用协议明确声明它。

    3 如果这是一个编译器错误,那么 好的,我可以分几次解决它 现在不同的方式。如果 编译器在这里做正确的事情 不过,那我就不知所措了 用一种方法来组织这一切,所以我 不要收到任何编译器警告或 运行时错误。

    最简单的解决方案是 (1) 不要定义与类属性重叠的协议。这样做无论如何都违背了拥有协议的全部目的。 (2) 使协议属性为读写,这样编译器和运行时就不会混淆。

    不过,凭直觉,我认为 因为 IPEvent 已经实现了 财产的必要吸气剂, 这应该算作“符合 协议”,即使它碰巧 还为 属性。

    如果您不使用动态属性,您可能会侥幸逃脱。使用动态属性,编译器必须向运行时生成一条消息,说明要即时生成哪些访问器。在这种情况下应该说什么? “生成一个符合只读协议的方法,顺便让它同时读写?”

    难怪编译器会抱怨。如果它是一条狗,它会在混乱中弄湿自己。

    我认为您需要认真重新考虑您的设计。您可以从这种非标准的、有风险的设计中获得什么好处?获得编译器错误是最好的情况。在最坏的情况下,runtime 会与不可预测的结果混淆。

    简而言之,(向莎士比亚道歉)“……错不在于编译者,而在于我们自己。”

    【讨论】:

    • 我猜部分困惑来自编译器如何处理协议中声明的属性与方法有些不同。例如,如果 IPContainer 声明: - (NSNumber*)id; - (void)setId:(NSNumber*)newId;并且声明了 IPGridViewGroup 协议: - (NSNumber*)id;那么 IPEvent 声明它符合 IPGridViewGroup 就没有问题了。不同之处在于,该属性具有运行时用于 @dynamic 和 @synthesize 之类的元数据,而普通方法不存在此类元数据。
    【解决方案2】:

    让我们试着把它分解一下。如果我理解正确:

    • IPEvent 是一个继承_IPEvent 并实现IPGridViewGroup 的类。
    • IPGridViewGroup 有一个 readonly 属性 id
    • _IPEvent_IPContainer 继承 readwrite 属性 id

    假设这些假设是正确的(如果我错了,请告诉我)然后IPEvent 继承了两个不同的id 属性,其中一个是readonly,另一个不是。

    您是否尝试使用显式 readwrite 修饰符重新定义 IPEvent 中的 id 属性?

    例如:

    @property (nonatomic, retain, readwrite) NSNumber *id;
    

    希望编译器能得到提示并生成一个 setter。

    【讨论】:

    • 再摆弄一点之后,在重新声明 IPEvent 上的属性时,Core Data 运行时的怪异似乎消失了(作为“非原子,保留”,不需要显式的读写修饰符),尽管我仍然明白编译器警告。但这似乎仍然很奇怪,来自协议的 readonly 修饰符会覆盖超类的 readwrite 属性。
    • 如果代码现在可以工作,您可以通过将@dynamic id; 添加到IPEvent 来抑制编译器警告。是的,这种行为确实很奇怪——这就是为什么我尝试将 Core Data 相关类中的继承保持在最低限度。
    【解决方案3】:
    @property(readonly) NSNumber* id
    

    看起来不正确。核心数据文档说你应该使用非原子(因为你不能在这里使用线程),你还应该保留 id 因为它是一个对象,而不是分配它(默认)。

    如果子类需要访问超类的 ivar,则需要声明其属性并使用 @dynamic 告诉编译器保持安静。看起来你没有这样做。

    它也可能与我发现的这个错误有关,该错误因编译器而异:

    http://openradar.appspot.com/8027473 如果声明了没有 ivar 的 prop,编译器会忘记超类 ivar 的存在

    也有可能 id 在 Core Data 中具有特殊含义,您应该使用不同的名称。

    【讨论】:

    • 请注意,只读属性仅在 IPGridViewGroup 协议中声明,而不是直接在任何核心数据类中声明。非原子修饰符对于只读属性似乎也毫无意义,因为它只会真正影响合成设置器的行为,而只读属性没有。这里没有涉及任何 ivar,因为所有这些都是由 Core Data 动态管理的,所以没有直接的 ivar 访问。我想我在其他情况下也遇到了那个 ivar 错误。它还会影响其他一些具有不同名称的属性。
    • Getter 也必须锁定。即使不考虑线程,获取也远非核心数据的原子。
    • 啊,是的,我想吸气剂也确实如此。
    猜你喜欢
    • 2015-11-21
    • 1970-01-01
    • 1970-01-01
    • 2016-03-24
    • 1970-01-01
    • 1970-01-01
    • 2014-06-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多