1

在我的 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 的唯一使用,因为其余的都支持到数组中。这很好还是会带来麻烦?子类的其他部分会不会带来问题?任何帮助将不胜感激。谢谢

4

1 回答 1

3

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

包装对象

将其包装NSRunningApplication在另一个对象中并提供您自己的hash&isEqual:方法。

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

CFSet与 桥接,与NSSet桥接CFMutableSetNSMutableSet。这意味着您可以通过 Core Foundation API 创建一个集合,然后将其用作NSSet示例。Core Foundation 是一个强大的框架,它向你展示了更多的东西。

您可以为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);
于 2020-06-29T09:18:17.637 回答