Quirk with Core Data, protocols, and readwrite vs. readonly property declarations
I'm running into an odd quirk involving Core Data, a declared protocol, and perhaps the LLVM 1.5 compiler. Here's the situation.
I have a Core Data model that among others has two classes, IPContainer and IPEvent, with IPContainer being the parent entity of IPEvent. Each entity has a custom class in the project for it, created using mogenerator. mogenerator generates an additional subclass that just contains the modeled property declarations, so the class hierarchy is actually IPEvent > _IPEvent > IPContainer > _IPContainer > NSManagedObject. The IPContainer entity has an attribute named 'id', which is declared as @property(nonatomic, retain) NSNumber* id;
in _IPContainer.h. _IPContainer.m has @dynamic id;
in the implementation, to tell Core Data to generate the accessors at runtime
I also have a protocol IPGridViewGroup declared in my project which defines several properties, one of which is that same 'id' property. However, a setter is not necessary for classes implementing this protocol, so the property in the protocol is declared as @property(readonly) NSNumber* id;
The IPEvent class declares that it conforms to the IPGridViewGroup protocol.
This worked fine using the Clang/LLVM 1.0.x compiler (whichever version shipped with Xcode 3.2.2), but upon upgrading to Xcode 3.2.3 and Clang/LLVM 1.5, a whole bunch of things changed. First, I get the following warning when compiling the IPEvent class:
/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
Then, when I actually run the program, this gets printed out in the console:
Property 'id' is marked readonly on class 'IPEvent'. Cannot generate a setter method for it.
Followed shortly by:
-[IPEvent setId:]: unrecognized selector sent to instance 0x200483900
I also tried redeclaring the property on the IPEvent class, but that just gave me a different compiler warning, and the same behavior at runtime:
/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'
Now, the only thing that's changed here is the compiler, so the catalyst for the change is clear, but what I don't know is whether this could be considered a bug in the new version of the compiler, or if the old version of the compiler was actually behaving incorrectly, and the new version now reveals that it's my own code that's buggy.
So among the questions I have here are:
- It seems like it should be OK to have a class conform to a protocol with a readonly property, but provide readwrite access for the property in its own implementation, is that correct? The quirk here though is that the readwrite property is actually declared in the superclass of the class that conforms to the protocol.
- I'm assuming that console message is being printed out somewhere in the bowels of Core Data. This is odd though, because IPEvent itself doesn't declare the 'id' property explicity, except by conforming to the IPGridViewGroup protocol. However, if this is the case, then I would think a compiler error would come up, since it would effectively being overriding a readwrite property (declared in the _IPContainer superclass) with a readonly version of the same property, which AFAIK is normally not allowed.
- If this is a compiler bug, then fine, I can work around it in a couple different ways for now. If the compiler is doing the right thing here though, then I'm at a loss to come up with a way to organize all this so I don't get any compiler warnings or runtime errors.
Edit: so, the workaround is to redeclare the property again on the IPEvent class, but I'm still puzzled as to why the two versions of the compiler act differently. It's also unclear how exactly properties declared on a protocol are supposed to interact with those declared on a class.
If I declare a readonly property in the class (rather tha开发者_如何转开发n the protocol) overriding a readwrite property, I get the message "warning: attribute 'readonly' of property 'longitude' restricts attribute 'readwrite' of property inherited from '_IPEvent'". It seems like if declaring it in the protocol has the same effect, a similar warning should come up from the compiler.
Intuitively though, I would think that since IPEvent already implements the necessary getter for the property, that should count as "conforming to the protocol", even if it happens to also implement a setter for the property.
Now, the only thing that's changed here is the compiler, so the catalyst for the change is clear, but what I don't know is whether this could be considered a bug in the new version of the compiler, or if the old version of the compiler was actually behaving incorrectly, and the new version now reveals that it's my own code that's buggy.
The newer compiler has noticed that you have two separate definitions for the accessors for the same instance variable of the same class. Of course, the linker should complain.
The old compiler should have kicked this back. The @property declaration is an implicit method declaration whether it occurs in a class or a protocol. When you have both a class and a protocol define a property with the same name, you end up with two sets of method declarations for one class. This is obviously going to cause problems somewhere along the line.
The differences between the two compilers could be something trivial such as the order of the #import
statements in source or even the modification dates on the source files.
You're obviously getting a collision because the IPContainer class has two dynamic method definitions, one generates just a setter and the other a setter and a getter. How should the compiler know which one to use? You've just told it that you want a readonly readwrite property. Worse, since this is a dynamic property, there is no telling what will actually be generated at runtime.
1 It seems like it should be OK to have a class conform to a protocol with a readonly property, but provide readwrite access for the property in its own implementation, is that correct?
Define "OK". Will the compiler accept it? Probably. After all, in a readonly property in the protocol you've defined a getter method but in the class you've also defined a setter method. Since a protocol doesn't restrict what additional methods an implementing class can have, the setter method can be added just like you could add any other unrelated method.
However, this is obviously very, very dangerous, especially in the case of NSManagedObject subclasses. The managed object context has very firm expectations about what it expects to find in the classes it works with.
2 This is odd though, because IPEvent itself doesn't declare the 'id' property explicity, except by conforming to the IPGridViewGroup protocol.
If the property is required by the protocol, it is explicitly declaring it by adopting the protocol.
3 If this is a compiler bug, then fine, I can work around it in a couple different ways for now. If the compiler is doing the right thing here though, then I'm at a loss to come up with a way to organize all this so I don't get any compiler warnings or runtime errors.
The simplest solution is (1) Don't define protocols that overlap class properties. Doing so defeats the entire purpose of having a protocol anyway. (2) Make the protocol property readwrite so the compiler and the runtime are not confused.
Intuitively though, I would think that since IPEvent already implements the necessary getter for the property, that should count as "conforming to the protocol", even if it happens to also implement a setter for the property.
You could probably get away with it if your weren't using dynamic properties. With a dynamic property the complier has to generate a message to the runtime explaining what accessors to generate on the fly. What's it supposed to say in this case? "Generate a method that conforms to the readonly protocol but by the way make it readwrite at the same time?"
No wonder the compiler is complaining. If it was a dog, it would be wetting itself in confusion.
I think you need to seriously rethink your design. What possible benefit can you gain from such a non-standard, risky design? Getting compiler errors is the best case scenario. In the worst case, the runtime gets confused with unpredictable results.
In short, (with apologies to Shakespeare) "...the fault lies not in the complier but with ourselves."
Let's try and break this down a bit. If I understand correctly:
IPEvent
is a class which inherits_IPEvent
and implementsIPGridViewGroup
.IPGridViewGroup
has areadonly
propertyid
._IPEvent
inherits thereadwrite
propertyid
from_IPContainer
.
Assuming those assumptions are correct (and please tell me if I'm wrong) then IPEvent
has inherited two different id
properties, one of which is readonly
and one of which is not.
Did you try redefining the id
property in IPEvent
with the explicit readwrite
modifier?
ex:
@property (nonatomic, retain, readwrite) NSNumber *id;
Hopefully then the compiler will get the hint and generate a setter.
@property(readonly) NSNumber* id
Looks incorrect. Core Data docs say you should use nonatomic (since you can't use threading here), and you should also be retaining id since it's an object, not assigning it (the default).
If a subclass needs to access an ivar of a superclass it needs to declare its property and use @dynamic to tell the compiler to be quiet. It does not look like you are doing that.
It could also be connected with this bug I found that varies between compilers:
http://openradar.appspot.com/8027473 Compiler forgets superclass ivar exists if a prop without an ivar is declared
It is also possible that id has a special meaning in Core Data and you should use a different name.
精彩评论