简体   繁体   中英

In a macOS Objective-C application, I have subclassed NSMutableSet for enforcing an equality different from isEqual. Is my implementation fine?

In my macOS Objective-C application, I have created a subclass of NSMutableSet. What I want to achieve is a NSMutableSet that does not use isEqual: as the comparing strategy. Specifically, The set will contain objects of type NSRunningApplication, and I want the set to work based on the equality of the objects bundle identifiers. Following is my implementation:

Header file:

#import <Cocoa/Cocoa.h>

NS_ASSUME_NONNULL_BEGIN

@interface BundleIdentifierAwareMutableSet : NSMutableSet 

@property (atomic, strong) NSMutableSet     *backStorageMutableSet;
@property (atomic, strong) NSMutableArray     *backStorageMutableArray;

@end

NS_ASSUME_NONNULL_END

Implementation file:

#import "BundleIdentifierAwareMutableSet.h"

@implementation BundleIdentifierAwareMutableSet

@synthesize backStorageMutableSet;

- (instancetype)init {
    
    self = [super init];
    
    if (self) {
        self.backStorageMutableSet = [[NSMutableSet alloc] init];
        self.backStorageMutableArray = [[NSMutableArray alloc] init];

    }
    
    return self;
}

- (NSUInteger)count {
    return [self.backStorageMutableArray count];
}

- (NSRunningApplication *)member:(NSRunningApplication *)object {
    
    __block NSRunningApplication *returnValue = nil;
    
    [self.backStorageMutableArray enumerateObjectsUsingBlock:^(NSRunningApplication * _Nonnull app, NSUInteger __unused idx, BOOL * _Nonnull stop) {
        
        if ([app.bundleIdentifier isEqualToString:[object bundleIdentifier]]) {
            returnValue = app;
            if (![app isEqual:object]) {
                NSLog(@"An ordinary set would have not considered the two objects equal.");
            }
            *stop = YES;
        }
        
    }];
    
    return returnValue;
    
}

- (NSEnumerator *)objectEnumerator {
    
    self.backStorageMutableSet = [NSMutableSet setWithArray:self.backStorageMutableArray];
    
    return [self.backStorageMutableSet objectEnumerator];
    
}

- (void)addObject:(NSRunningApplication *)object {
    
    NSRunningApplication *app = [self member:object];
    
    if (app == nil) {
        [self.backStorageMutableArray addObject:object];
    }
}



- (void)removeObject:(NSRunningApplication *)object {
    
    NSArray *snapShot = [self.backStorageMutableArray copy];
    
    [snapShot enumerateObjectsUsingBlock:^(NSRunningApplication * _Nonnull currentApp, NSUInteger __unused idx, BOOL * _Nonnull __unused stop) {
        
        if ([[currentApp bundleIdentifier] isEqualToString:[object bundleIdentifier]]) {
            [self.backStorageMutableArray removeObject:currentApp];
            if (![currentApp isEqual:object]) {
                NSLog(@"An ordinary set would have not considered the two objects equal.");
            }
        }
        
    }];
    
}

This seems to work, and indeed, When applicable, Xcode logs that an ordinary NSMutableSet would have not considered two members equal. I would like to bring this implementation to the Production App, but I am afraid I have not considered something important, since this is the first time I subclass NSMutableSet. For example, I am worried about the following method:

- (NSEnumerator *)objectEnumerator {
    
    self.backStorageMutableSet = [NSMutableSet setWithArray:self.backStorageMutableArray];
    
    return [self.backStorageMutableSet objectEnumerator];
    
}

This is the only use I do of the backStorageMutableSet since the rest is backed to the array. Is this fine or can bring troubles? Will other parts of the subclass bring problems? Any help will be greatly appreciated. Thanks

Don't do this. Subclassing collections should be the last resort. It can have implications on performance, ... Try to use highest possible abstraction and go down if it doesn't work for you for some reason.

Wrapper object

Wrap the NSRunningApplication in another object and provide your own hash & isEqual: methods.

Application.h :

@interface Application: NSObject

@property (nonatomic, strong, readonly, nonnull) NSRunningApplication *application;

@end

Application.m :

@interface Application ()

@property (nonatomic, strong, nonnull) NSRunningApplication *application;

@end

@implementation Application

- (nonnull instancetype)initWithRunningApplication:(NSRunningApplication *_Nonnull)application {
    if ((self = [super init]) == nil) {
        // https://developer.apple.com/documentation/objectivec/nsobject/1418641-init?language=objc
        //
        // The init() method defined in the NSObject class does no initialization; it simply
        // returns self. In terms of nullability, callers can assume that the NSObject
        // implementation of init() does not return nil.
        return nil;
    }
    
    self.application = application;
    return self;
}

// https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418795-isequal?language=objc
- (BOOL)isEqual:(id)object {
    if (![object isKindOfClass:[Application class]]) {
        return NO;
    }
    
    Application *app = (Application *)object;
    return [self.application.bundleIdentifier isEqualToString:app.application.bundleIdentifier];
}

// https://developer.apple.com/documentation/objectivec/1418956-nsobject/1418859-hash?language=objc
- (NSUInteger)hash {
    return self.application.bundleIdentifier.hash;
}
@end

Toll-free bridging & CFMutableSetRef

CFSet is bridged with the NSSet , CFMutableSet is bridged with the NSMutableSet , etc. It means that you can create a set via Core Foundation API and then use it as NSSet for example. Core Foundation is a powerful framework which exposes more stuff to you.

You can provide a custom set of callbacks for the CFSet .

/*!
    @typedef CFSetCallBacks
    Structure containing the callbacks of a CFSet.
    @field version The version number of the structure type being passed
        in as a parameter to the CFSet creation functions. This
        structure is version 0.
    @field retain The callback used to add a retain for the set on
        values as they are put into the set. This callback returns
        the value to store in the set, which is usually the value
        parameter passed to this callback, but may be a different
        value if a different value should be stored in the set.
        The set's allocator is passed as the first argument.
    @field release The callback used to remove a retain previously added
        for the set from values as they are removed from the
        set. The set's allocator is passed as the first
        argument.
    @field copyDescription The callback used to create a descriptive
        string representation of each value in the set. This is
        used by the CFCopyDescription() function.
    @field equal The callback used to compare values in the set for
        equality for some operations.
    @field hash The callback used to compare values in the set for
        uniqueness for some operations.
*/
typedef struct {
    CFIndex             version;
    CFSetRetainCallBack         retain;
    CFSetReleaseCallBack        release;
    CFSetCopyDescriptionCallBack    copyDescription;
    CFSetEqualCallBack          equal;
    CFSetHashCallBack           hash;
} CFSetCallBacks;

There're predefined sets of callbacks like:

/*!
    @constant kCFTypeSetCallBacks
    Predefined CFSetCallBacks structure containing a set of callbacks
    appropriate for use when the values in a CFSet are all CFTypes.
*/
CF_EXPORT
const CFSetCallBacks kCFTypeSetCallBacks;

Which means that you're not forced to provide all of them, but you're free to modify just some of them. Let's prepare two callback functions:

// typedef CFHashCode    (*CFSetHashCallBack)(const void *value);
CFHashCode runningApplicationBundleIdentifierHash(const void *value) {
    NSRunningApplication *application = (__bridge NSRunningApplication *)value;
    return [application.bundleIdentifier hash];
}

// typedef Boolean        (*CFSetEqualCallBack)(const void *value1, const void *value2);
Boolean runningApplicationBundleIdentifierEqual(const void *value1, const void *value2) {
    NSRunningApplication *application1 = (__bridge NSRunningApplication *)value1;
    NSRunningApplication *application2 = (__bridge NSRunningApplication *)value2;
    return [application1.bundleIdentifier isEqualToString:application2.bundleIdentifier];
}

You can use them in this way:

- (NSMutableSet<NSRunningApplication *> *_Nullable)bundleIdentifierAwareMutableSetWithCapacity:(NSUInteger)capacity {
    // > Predefined CFSetCallBacks structure containing a set of callbacks
    // > appropriate for use when the values in a CFSet are all CFTypes.
    //
    // Which means that you shouldn't bother about retain, release, ... callbacks,
    // they're already set.
    //
    // CFSetCallbacks can be on stack, because this structure is copied in the
    // CFSetCreateMutable function.
    CFSetCallBacks callbacks = kCFTypeSetCallBacks;
    
    // Overwrite just the hash & equal callbacks
    callbacks.hash = runningApplicationBundleIdentifierHash;
    callbacks.equal = runningApplicationBundleIdentifierEqual;
    
    // Try to create a mutable set.
    CFMutableSetRef set = CFSetCreateMutable(kCFAllocatorDefault, capacity, &callbacks);
    
    if (set == NULL) {
        // Failed, do some error handling or just return nil
        return nil;
    }
    
    // Transfer the ownership to the Obj-C & ARC => no need to call CFRelease
    return (__bridge_transfer NSMutableSet *)set;
}

&

NSMutableSet<NSRunningApplication *> *set = [self bundleIdentifierAwareMutableSetWithCapacity:50];
[set addObjectsFromArray:[[NSWorkspace sharedWorkspace] runningApplications]];
NSLog(@"%@", set);

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