在 macOS Objective-C 应用程序中,我将 NSMutableSet 子类化以强制执行不同于 isEqual 的相等性。我的实施没问题吗?

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

在我的 macOS Objective-C 应用程序中,我创建了 NSMutableSet 的子类。我想要实现的是一个不使用 isEqual: 作为比较策略的 NSMutableSet。具体来说,该集合将包含 NSRunningApplication 类型的对象,我希望该集合基于对象包标识符的相等性来工作。以下是我的实现:

头文件:

#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

执行文件:

#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.");
            }
        }
        
    }];
    
}

这似乎有效,事实上,在适用时,Xcode 记录了一个普通的 NSMutableSet 不会认为两个成员相等。我想把这个实现带到生产应用程序中,但恐怕我没有考虑重要的事情,因为这是我第一次子类化 NSMutableSet。例如,我担心以下方法:

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

这是我对 backStorageMutableSet 的唯一用途,因为其余的都备份到数组中。这很好还是会带来麻烦?子类的其他部分会不会带来问题?任何帮助将不胜感激。谢谢

不要这样做。子类化集合应该是最后的手段。它可能会对性能产生影响,...尝试使用尽可能高的抽象,如果由于某种原因它对您不起作用,则下降。

包装对象

NSRunningApplication 包装在另一个对象中并提供您自己的 hashisEqual: 方法。

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

免费桥接 & CFMutableSetRef

CFSetNSSet 桥接,CFMutableSetNSMutableSet 桥接,等等。这意味着您可以通过 Core Foundation 创建集合 API 然后将它用作 NSSet 例如。核心基础是强大的 向您展示更多内容的框架。

您可以为 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;

有预定义的回调集,例如:

/*!
    @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;

这意味着您不必提供所有这些,但您可以自由修改其中的一部分。让我们准备两个回调函数:

// 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];
}

您可以这样使用它们:

- (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);