简体   繁体   中英

Objective-C - Effective Subclassing of Cocoa Class Clusters

I have an object that used to be an NSMutableSet but needed some more stuff attached to it. The obvious (and obviously not supported) thing to do is to subclass NSMutableSet and tack on the two additional properties. Since NSMutableSet, like basically all Cocoa data structures, is a class cluster I cannot subclass it in the usual way, since the super class just throws exceptions. This led me down several paths.

The first path was to create sort of a composite object that declared itself as a subclass of NSMutableSet but really just forwarded the invocations to an internal NSMutableSet. I didn't want to have to implement every method on NSMutableSet, so I thought forwardInvocation: would be a good way to accomplish my mission. Unfortunately, the abstract class of NSMutableSet implements all of the methods on the interface and their implementations throw exceptions, so I was never getting to the point where I could forward an invocation.

The second path was to subclass NSProxy and forward the invocation from there. This solution falls short in that I need to copy the interface of NSMutableSet over unless there's a way to declare "this class implements this interface" that I don't know about (this could very well be the solution).

The third path was to create a category on NSMutableSet and import it just for the class that needs to use it but that falls short since you cannot add non-dynamic properties via a category. That led me to using associated objects in a category. I'm willing to admit that that is the correct solution for this use case, but I wish it weren't since it's kind of clunky. It's doubly clunky since the properties I'm adding are primitive so I'll have to wrap and unwrap them when setting and getting the association (unless there's a way to associate primitives which I'm unfamiliar with).

Essentially, what I would like is something that behaves functionally as a subclass of NSMutableSet (and all class clusters) but cannot figure out the best approach. Thanks!

Trying to subclass Cocoa class clusters will just create an awful lot of hurt. It may seem a good idea, but you will forever run into problems.

Just create an NSObject with an NSMutableSet as the first member object.

Subclassing Cocoa class cluster is kind of discouraged. Not without reasons. Please do not enter this crashy world.

Either of your solutions will work. I've successfully used the first path with NSArray and NSDictionary , so I believe it should work fine for NSMutableSet as well. Just remember that you need to override not only forwardInvocation: , but a few of other methods as well. Please consult Surrogate Objects sections of Apple docs:

Although forwarding mimics inheritance, the NSObject class never confuses the two. Methods like respondsToSelector: and isKindOfClass: look only at the inheritance hierarchy, never at the forwarding chain.

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html

In my case, I've overridden:

  • conformsToProtocol:
  • isKindOfClass:
  • isMemberOfClass:
  • respondsToSelector:
  • instancesRespondToSelector:
  • forwardInvocation:
  • methodSignatureForSelector:
  • instanceMethodSignatureForSelector:

from which isKindOfClass: , conformsToProtocol: and respondsToSelector: are definitely crucial.

I've also used the third path with good results, but I admit the associated objects API is clunky.

First, gnasher729 is correct. Don't subclass class clusters. Just don't do it. Can you do it? If I tell you that you can't, will it help you convince yourself that you shouldn't? I can lie if it helps you make good choices.

But in all seriousness, it is almost always meaningless as well. Is your subclass really a specific kind of set? Or is it really kind of like a set. Consider NSAttributedString . It isn't a kind of string, it has-a string. This is almost always better.

And also, class clusters happen to be a royal pain to subclass.

That said, adding associated values onto a data structure, as you've already discovered, is generally just fine, because what you really want is "hey, I have some data that needs to go along with this other data." Wrapping has gotten so easy that it shouldn't really slow you down. See https://stackoverflow.com/a/14918158/97337 :

objc_setAssociatedObject(self, animatingKey, @(value), OBJC_ASSOCIATION_RETAIN_NONATOMIC);

And with "one weird trick" , you can make this really easy:

@interface NSObject (BoolVal)
@property (nonatomic, readwrite, assign) BOOL boolVal;
@end

@implementation NSObject (BoolVal)

- (BOOL)boolVal {
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setBoolVal:(BOOL)value {
    objc_setAssociatedObject(self, @selector(boolVal), @(value), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

But I'd still come back to the question of whether this is really a kind of set (rather than just like a set), and whether it really needs to respond to every message that can be sent to a set. As with NSAttributedString , your real needs are often much smaller than that in practice, and wrapping the handful of methods you need is often worth the simplicity and control.

For completeness, let's look at your first path:

create sort of a composite object that declared itself as a subclass of NSMutableSet but really just forwarded the invocations to an internal NSMutableSet

Can you subclass an NSMutableSet ? Yes, but should you? The documentation for NSMutableSet says:

Subclassing Notes

There should be little need of subclassing. If you need to customize behavior, it is often better to consider composition instead of subclassing.

So weigh that up and if you want to subclass refer again to the documentation:

Methods to Override

In a subclass, you must override both of its primitive methods:

addObject:

removeObject:

You must also override the primitive methods of the NSSet class.

And looking at the NSSet class documentation we find its primitive methods are:

Methods to Override

In a subclass, you must override all of its primitive methods:

count

member:

objectEnumerator

That's it, 5 methods.

You can define your own class as a subclass of NSMutableSet , add an instance variable which is an instance of NSMutableSet , implement 5 methods and redirect them to the set instance, add whatever init methods you wish, and then add your additional properties.

If performance is of concern then the tradeoff is between redirecting those five methods and accessing associated objects for your additional properties. You'll need to profile to work that out, but if and only if performance becomes an issue.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM