【问题标题】:What are the traps and tribulations when it comes to subclassing NSMutableDictionary?子类化 NSMutableDictionary 有哪些陷阱和困难?
【发布时间】:2015-10-09 00:52:04
【问题描述】:

苹果说

通常应该很少需要继承 NSMutableDictionary。 如果您确实需要自定义行为,通常最好考虑 组合而不是子类化。

(见https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/

他们可能应该让这件事变得更强大,并说后果自负

但是,在某些情况下,子类化 NSMutableDictionary 可能很重要。就我而言,从符号上讲,它确实与我的代码相关。有很多障碍需要克服。关于这个还有其他的 web 和 SO 条目,但是我在旅途中遇到了一些看似新的问题,所以想写下来以备不时之需并帮助他人。所以,我会发布我的答案。随意贡献您自己的其他发现。

【问题讨论】:

  • 这似乎不是一个问题。
  • 虽然这很棒并且允许它真的是一篇博客文章。但答案似乎更多是关于如何使这项工作正常工作的问题:NSCoding 和 Swift 下标。此外,这可能会在更新中对 Swift 造成破坏,Swift 仍在变化。

标签: ios subclass nsmutabledictionary


【解决方案1】:

1) 没有代理对象。一开始,出于某种原因,Apple 似乎使NSMutableDictionary 在某些不寻常的方面不同于NSMutableSet。我对NSMutableDictionary 子类化的潜在需求实际上源于需要了解NSMutableDictionary 实例的突变变化。例如,NSMutableSets 让这变得更容易一些。 NSMutableSets 让您可以访问“代理”对象:mutableSetValueForKey。这为您提供了一种了解集合内容何时发生变化的机制。有关详细信息,请参阅https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/。您希望看到的是 mutableDictValueForKey 之类的东西,但它似乎不存在。

2) 在您的子类方法中实现 init! Apple 告诉您需要重写方法:

在子类中,您必须重写它的两个原始方法:

setObject:forKey:

removeObjectForKey:

您还必须覆盖 NSDictionary 的原始方法 类。

NSDictionary 原始方法是:

initWithObjects:forKeys:count:

@property count

objectForKey:

keyEnumerator:

但是,您还必须重写 init 方法!

3) 在 Swift 中执行此操作还行不通! 至少在我尝试此操作的日期(大约 10/8/15 和 Xcode 7)之前,您必须让您的NSMutableDictionary Objective-C 中的子类,而不是 Swift。见Cannot override initializer of NSDictionary in Swift

4) NSCoding 不适用于 NSMutableDictionary 子类! 在我的 NSMutableDictionary 子类中,我尝试实现 NSCoding 协议,但无法使其在密钥归档器的上下文中工作。键控存档器会生成一个空的 NSMutableDictionary(解码时),而不是我自己的子类,我不知道为什么。一些特别的NSMutableDictionary 魔法?

5) 在 Swift 中的下标可能不会删除它。我尝试只为 Swift 实现下标方法(请参阅https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html),但从符号上看,这还有很多不足之处。我真的想要一个与 NSDictionary/NSMutableDictionary 完全互操作的类型,这似乎需要一个子类。

6) 不要只实现方法;您需要自己的数据!如果您只是尝试覆盖上述方法并调用“super”,您的代码将无法正常工作。您需要使用“组合”在内部实现 NSMutableDictionary 属性。或者您想要实现字典的任何其他机制。再一次,一些类集群魔法正在发生。在下面的 .m 文件中查看我的 dict 属性。

以下是我迄今为止的 Objective-C 代码:

//
//  SMMutableDictionary.h
//  Dictionary
//
//  Created by Christopher Prince on 10/6/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

/* I subclassed NSMutableDictionary because:
    1) because I needed a way to know when a key was set or removed. With other mutable objects you can use proxy objects (e.g., see https://www.objc.io/issues/7-foundation/key-value-coding-and-observing/), but a proxy object doesn't seem to be provided by Apple for NSMutableDictionary's.
    2) for notational convenience in some other code that I was writing.
*/

// QUESTION: Can I set up an observer to detect any changes to the value of the key's within the dictionary? We'd have to remove this KVO observer if the object was removed. Presumably, with this interface, the way that the object would be removed would be (a) setting with nil, and (b) deallocation of this SMMutableDictionary itself.

#import <Foundation/Foundation.h>

@class SMMutableDictionary;

@protocol SMMutableDictionaryDelegate <NSObject>

@required

// Reports on the assignment to a keyed value for this dictionary and the removal of a key: setObject:forKey: and removeObjectForKey:
- (void) dictionaryWasChanged: (SMMutableDictionary * _Nonnull) dict;

@end

@interface SMMutableDictionary : NSMutableDictionary

// For some reason (more of the ugliness associated with having an NSMutableDictionary subclass), when you unarchive a keyed archive of an SMMutableDictionary, it doesn't give you back the SMMutableDictionary, it gives you an NSMutableDictionary. So, this method is for your convenience. AND, almost even better, when you use a keyed archiver to archive, it uses our encoder method, but doesn't actually generate an archive containing our dictionary!! SO, don't use keyed archiver methods directly, use the following two methods:
- (NSData * _Nullable) archive;
+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;

// Optional delegate
@property (nonatomic, weak, nullable) id<SMMutableDictionaryDelegate> delegate;

@end

这是 .m 文件:

//
//  SMMutableDictionary.m
//  Dictionary
//
//  Created by Christopher Prince on 10/6/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

// I wanted to make this a Swift NSMutableDictionary subclass, but run into issues...
// See https://stackoverflow.com/questions/28636598/cannot-override-initializer-of-nsdictionary-in-swift
// http://www.cocoawithlove.com/2008/12/ordereddictionary-subclassing-cocoa.html
// See also https://stackoverflow.com/questions/10799444/nsdictionary-method-only-defined-for-abstract-class-my-app-crashed
// I tried only implementing the subscript method for Swift (see https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Subscripts.html), but notationally this left much to be desired. I really wanted a type that was fully interoperable with NSDictionary/NSMutableDictionary, which seems to require a subclass.

// See also http://www.smackie.org/notes/2007/07/11/subclassing-nsmutabledictionary/

#import "SMMutableDictionary.h"

@interface SMMutableDictionary()
@property (nonatomic, strong) NSMutableDictionary *dict;
@end

// See this for methods you have to implement to subclass: https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Reference/Foundation/Classes/NSMutableDictionary_Class/index.html
// HOWEVER, while they didn't say you have to subclass the init method, it did't work for me without doing that. i.e., I needed to have [1] below.

@implementation SMMutableDictionary

- (instancetype) initWithObjects:(const id  _Nonnull __unsafe_unretained *)objects forKeys:(const id<NSCopying>  _Nonnull __unsafe_unretained *)keys count:(NSUInteger)cnt;
{
    self = [super init];
    if (self) {
        self.dict = [[NSMutableDictionary alloc] initWithObjects:objects forKeys:keys count:cnt];
    }
    return self;
}

// [1].
- (instancetype) init;
{
    self = [super init];
    if (self) {
        self.dict = [NSMutableDictionary new];
    }
    return self;
}

// Both of these are useless. See the keyed archiver/unarchiver methods on the .h interface.
/*
- (void)encodeWithCoder:(NSCoder *)aCoder;
{
    //[aCoder encodeObject:self.dict];
    [aCoder encodeObject:self.dict forKey:@"dict"];
}
 */

/*
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder;
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        //self.dict = [aDecoder decodeObject];
        self.dict = [aDecoder decodeObjectForKey:@"dict"];
    }
    return self;
}
*/

- (NSData * _Nullable) archive;
{
    return [NSKeyedArchiver archivedDataWithRootObject:self.dict];
}

+ (instancetype _Nullable) unarchiveFromData: (NSData * _Nonnull) keyedArchiverData;
{
    NSMutableDictionary *dict = [NSKeyedUnarchiver unarchiveObjectWithData:keyedArchiverData];
    if (nil == dict) return nil;

    return [[SMMutableDictionary alloc] initWithDictionary:dict];
}

- (NSUInteger) count;
{
    return self.dict.count;
}

- (id) objectForKey:(id)aKey;
{
    return [self.dict objectForKey:aKey];
}

- (NSEnumerator *)keyEnumerator;
{
    return [self.dict keyEnumerator];
}

- (void) setObject:(id)anObject forKey:(id<NSCopying>)aKey;
{
    [self.dict setObject:anObject forKey:aKey];
    if (self.delegate) {
        [self.delegate dictionaryWasChanged:self];
    }
}

- (void) removeObjectForKey:(id)aKey;
{
    [self.dict removeObjectForKey:aKey];
    if (self.delegate) {
        [self.delegate dictionaryWasChanged:self];
    }
}

@end

2015 年 10 月 9 日更新

为了澄清我所说的“突变变化”(回复下面的@quelish)的意思,这里有一个带有 NSMutableDictionary 的 KVO 示例。请注意,它的输出反映了下面的测试 1。即,KVO 不指示对密钥的更改。本例改编自https://developer.apple.com/library/prerelease/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html#//apple_ref/doc/uid/TP40014216-CH7-XID_5

如果您确实知道字典的所有键,则可以使用 KVO。见Observing NSMutableDictionary changes

//
//  ViewController.swift
//  Dictionary2
//
//  Created by Christopher Prince on 10/9/15.
//  Copyright © 2015 Spastic Muffin, LLC. All rights reserved.
//

import UIKit

private var myContext = 0

class ViewController: UIViewController {
    var obj = MyObserver()

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        print("Test 1")
        obj.objectToObserve.myDict["key1"] = "value1"

        print("Test 2")
        obj.objectToObserve.myDict = NSMutableDictionary()
    }
}

class MyObjectToObserve: NSObject {
    dynamic var myDict = NSMutableDictionary()
    override var description : String {
        return "\(myDict)"
    }
}

class MyObserver: NSObject {
    var objectToObserve = MyObjectToObserve()

    override init() {
        super.init()
        objectToObserve.addObserver(self, forKeyPath: "myDict", options: NSKeyValueObservingOptions(rawValue: 0), context: &myContext)
    }

    override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if context == &myContext {
            //let newValue = change?[NSKeyValueChangeNewKey]
            print("change: \(change)")
            print("object: \(object)")
        } else {
            super.observeValueForKeyPath(keyPath, ofObject: object, change: change, context: context)
        }
    }

    deinit {
        objectToObserve.removeObserver(self, forKeyPath: "myDate", context: &myContext)
    }
}

【讨论】:

  • “源于需要了解突变变化”。使用键值观察。
  • KVO 很适合告诉您属性的值已更改,但不太适合告诉您可变集合内容已更改。这就是我所说的“突变变化”。我将在上面的答案下方添加一个演示。
  • 完全不正确。集合在其内容更改时发送键值通知。这适用于有序、无序和键值集合。
猜你喜欢
  • 1970-01-01
  • 2011-12-11
  • 1970-01-01
  • 2012-06-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-03
  • 2015-07-07
相关资源
最近更新 更多