Cocoa中实现Key-value coding的5种方法

Key-value coding(KVC)是用来使得一个类的行为摆脱它需要依附执行的特定属性的一种方式。它通常与NSKeyValueCoding协议联系在一起当然也有一系列其他的方法来实现相同的效果。在这篇文章中,我关注为什么KVC是重要的而且要介绍5种不同的方式来实现这个功能。

介绍

NSKeyValueCoding协议自从Mac OS X10.0起就已经在Cocoa中,但是它给人留下深刻印象是在Mac OS X10.3中当用户接口绑定展示了他们的潜力时——允许用户接口通过配置数据而不用更改代码就可以控制连接到对象的属性。

KVC的核心概念存在于大多数的编程框架中,它是流行而且普遍的在Cocoa中,当然也在其他的编译程序框架中。

尽管它很流行,你还是可以避免使用KVC如果你愿意。然而,Apple引入它是有原因的,它作为一个简单的设计模式可以使你在编程时受益。下面,我将讲述到底key-value coding(KVC)是什么,它如何改进你的代码以及不同的方式来实现这个效果。

什么是key-value coding?

KVC的思想是相当简单的,不用直接获取或设置一个对象的属性,KVC通过传递一个”key”(通常是一个字符串)来获取或更改与这个”key”相关的属性。这个听起来非常像NSDictionary

例如:

1
2
3
4
5
// Set a property directly...
someObject.someProperty = someValue;
 
// ...or set the same property using key-value coding
[someObject setValue:someValue forKey:@"someProperty"];

为什么你要这样做?答案是它简化了设置某个属性值的行为。

设想有这样一个可以编辑姓名和地址的表格:

tablescreenshot

没有KVC方式,利用NSTableViewDataSource方法来编辑某一行也许要这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)tableView:(NSTableView *)aTableView
    setObjectValue:(NSString *)anObject
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    if ([[aTableColumn identifier] isEqual:@"name"])
    {
        [[records objectAtIndex:rowIndex] setName:anObject];
    }
    else if ([[aTableColumn identifier] isEqual:@"address"])
    {
        [[records objectAtIndex:rowIndex] setAddress:anObject];
    }
}

而利用KVC方式,这个方法变成:

1
2
3
4
5
6
7
- (void)tableView:(NSTableView *)aTableView
    setObjectValue:(NSString *)anObject
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    [[records objectAtIndex:rowIndex] setValue:anObject forKey:[aTableColumn identifier]];
}

前面我提到了绑定(bindings):这个方法并没有使用用户接口绑定。但这也确实展示了一个简化的实现绑定的方式。

这个KVC方法是更好的因为它不必编辑每一个分立的属性。这就是KVC的精华之所在。

当数据量增加时,KVC愈发体现它的高效。一个有1000列的表格其实只需要与上述相同量的代码就可以实现编辑。

KVC方法1:NSKeyValueCoding protocol

我目前展示的所有KVC方式都是利用NSKeyValueCoding协议。其实,我称它为协议是因为它确实是一个通俗的协议(NSObject中一个分类)。

这个分类继承setValue:forKey: 和valueForKey:两个方法使你可以设置和获取值通过NSString的keys。

优点:

  • 自动找到已经存在的获取和设置的方法,而且如果该方法不存在,就直接获取或设置值。这意味这大多数的属性都能自动支持NSKeyValueCoding。可以在我的文章Key Value Information post.中学习更多关于此的内容。
  • 包含key的路径(为了传递多个属性)。
  • 集成了NSKeyValueObserving来实现观察设计实例。
  • 当key没有定义时提供后续处理方法。

缺点:

继承的搜索路径使这种方法是最的KVC方法(可以阅读我前面相关的文章Replacing Core Data Key Paths)。

要求这个类中有个相关方法或者有变量与这个名字的属性相关,而且可以被NSKeyValueCoding找到。

仅仅支持NSStrings作为属性的“key”。

KVC 方法2: 手动设置NSKeyValueCoding 行为的子集

NSKeyValueCoding根据选择器来查找方法,根据名字查找成员。

这些工作需要你首先进行设置。

1
2
3
4
5
6
// Manual KVC setter method implementation
NSString *setterString = [@"set" stringByAppendingString:[someKeyString capitalizedString]];
[someObject performSelector:NSSelectorFromString(setterString) withObject:someValue];
 
// Manual KVC ivar setter
object_setInstanceVariable(someObject, someKeyString, someValue);

为什么你要这样做而不是使用NSKeyValueCoding方法呢?当你要避免使用NSKeyValueCoding默认找到的方法或成员时你可以使用这个方法。这个方法允许你自定义查找路径。

优点:

  • 相比NSKeyValueCoding更多的设置查找路径。
  • 潜在地比NSKeyValueCoding要快。
  • 可以为不是继承于NSObject的类工作因此不用继承NSKeyValueCoding。
  • 手动实现可以获得与设置非对象的值。

缺点:

  • 不如NSKeyValueCoding灵活。
  • 在大多数情况下,它需要更大的工作量。

KVC 方法3: 关联对象

Objective-C 2.0执行时(在iPhone或64位Mac OS X程序上)允许设置一个对象关联到任何其他对象。这使得执行时任何对象都可以通过key来任意被设置额外的属性,而不需要来自这个对象的方法或成员支持。

objc_setAssociatedObject(someObject, someKey, someValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

你会用到这个方法的主要原因是你打算在外部设置对象的属性—例如,不用对象支持,被包含于甚至不知道这个属性的路径。某个属性可以被程序的其它部分根据需要来设置到一个对象上面。

优点

  • 不需要一个对象支持。
  • Key可以是任意指针(也可以是实体只要使用OBJC_ASSOCIATION_ASSIGN)。
  • 有可能是最快的KVC方法

缺点

  • Key是一个指针而不是实体,因此如果使用实体,它必须是具有唯一指针的实体(即如果你打算通过一个字符串值的不同分配空间来获取与设置值,那是不可以的)。
  • 不会影响对象内部的方法或变量。这表示如果你打算让这个对象了解在它本身发生的事情,你就不得不使用一个不同的方法。

KVC 方法 4: 将选择器作为keys

KVC根本上来说是通过Key来查找一个属性,之后在这个属性上进行操作。

Objective-C在它的内核有一个检查—方法查找。这个查找的keys就是选择器。

objc_msgSend(someObject, someSetterSelector, someValue);

这个方法与手动设置NSKeyValueCoding方法部分类似,但是它不是形成一个选择器的字符串来作为key,这个方法直接利用选择器作为key。

这个方法的缺点是需要为获取和设置分别设置选择器。

优点

  • 最快的方式来遍历查找方法(由于方法可以重写因此更加化为子类更加方便)
  • 能够获取和设置非对象数据(需要objc_msgSend_fpret 和 objc_msgSend_stret 来得到 float型,double型以及struct型的属性)。

缺点

  • 获取和设置需要不同的keys。
  • 选择器不是实体一次不能直接在Objective-C数组中存储(必须使用CoreFoundation 或者 NSValuewrappers)。

KVC 方法5: 自己实现它

最后一个方法是自己处理这个实现。如果你需要最大的灵活性的话(处理特殊的keys或者数据)或者想要暴露一个对象实体的不同的关键值。

最简单的实现这一条的方式是暴露获取和设置方法,之后在对象包含的dictionary上获取和设置这些值。

1
2
3
4
5
6
7
8
9
- (void)setCollectionValue:(id)value forKey:(NSString *)key
{
    [collectionDictionary setObject:value forKey:key];
}
 
- (id)getCollectionValueForKey:(NSString *)key
{
    return [collectionDictionary objectForKey:key];
}

你可以使用Cocoa中任何一种关键值存储结构来处理这些值内部存储:

  • NSMutableDictionary
  • NSMapTable
  • CFMutableDictionaryRef
  • associated objects on selfor other objects (see above)

或者你自己的存储方案。

优点

  • 一个独立的对象能够暴露多种的分类收集。
  • 能够获取以及设置任何被这个收集所支持的数据类型。
  • 能够为特殊的情况提供最灵活的后续方法。

缺点

  • 必须在target class上实现(不能支持随意对象)。
  • 不能与NSKeyValueObserving或者NSKeyValueObserving的相关概念交互操作。

结论

使用key-value coding并不是必需的。实现整个工程不用KVC当然是可能的。但是,它是最好的代码样式之一,可以减少重复代码以及使得类可以复用。

你可以看到,有许多不同的KVC编程方法。NSKeyValueCoding可能是最灵活的,复用的而且能够很好的被Cocoa支持的方式,因此除非你需要上面提到的其他方法的优点之一或者你打算使你的方案限定解决特定的问题,这个方法可能是最好的方式。

作者:Matt Gallagher

原文链接:http://cocoawithlove.com/2010/01/5-key-value-coding-approaches-in-cocoa.html

原文:

5 key-value coding approaches in Cocoa

Key-value coding (KVC) is a way of decoupling a generic action from the specific properties it may need to act upon. It is most commonly associated with the NSKeyValueCoding protocol but there are a number of other ways to achieve the same effect. In this post, I look at why key-value coding is important and show you 5 different ways — each with their own particular advantages — to implement this pattern.

Introduction

The NSKeyValueCoding protocol has been in Cocoa since Mac OS X 10.0 but it first made a real mark in Mac OS X 10.3 when user-interface bindings demonstrated their potential — allowing user-interface controls to connect to properties on objects by configuring the data, not changing the code.

While the core concepts in key-value coding exist (or can be implemented) in most programming frameworks, key-value coding is both prevalent and pervasive throughout Cocoa in a way that is uncommon, certainly among other compiled application frameworks.

Despite its prevalence, you can avoid key-value coding if you choose. However, Apple introduced it for a reason — it is a simple design pattern that can be highly beneficial to your code. To that end, I’m going to spend this post talking about what key-value coding is, how it can improve your code and different approaches you can use to achieve the effect.

What is key-value coding?

The idea behind key-value coding is pretty simple: instead of directly getting and setting specific properties on an object, key-value coding involves passing in a “key” (usually a string) and getting or setting the property associated with that key.

This may sound a lot like NSDictionary

For example:

1
2
3
4
5
// Set a property directly...
someObject.someProperty = someValue;
 
// ...or set the same property using key-value coding
[someObject setValue:someValue forKey:@"someProperty"];

Why would you want to do this? The answer is that it decouples the action of setting a value for a property from the specific property involved.

Imagine a table for editing names and addresses:

tablescreenshotWithout key-value coding, the NSTableViewDataSource method to handle an edit for one of the rows might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)tableView:(NSTableView *)aTableView
    setObjectValue:(NSString *)anObject
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    if ([[aTableColumn identifier] isEqual:@"name"])
    {
        [[records objectAtIndex:rowIndex] setName:anObject];
    }
    else if ([[aTableColumn identifier] isEqual:@"address"])
    {
        [[records objectAtIndex:rowIndex] setAddress:anObject];
    }
}

With key-value coding, the method becomes:

1
2
3
4
5
6
7
- (void)tableView:(NSTableView *)aTableView
    setObjectValue:(NSString *)anObject
    forTableColumn:(NSTableColumn *)aTableColumn
    row:(int)rowIndex
{
    [[records objectAtIndex:rowIndex] setValue:anObject forKey:[aTableColumn identifier]];
}

I spoke about bindings before: this approach isn’t using user-interface bindings (that would require no code at all). This is really showing a simplified way that bindings might be implemented.

This key-value coding approach is better because it doesn’t have to handle the edit of each property as a separate condition. This is the essence of key-value coding.

And key-value coding remains just as efficient as the data set increases in size. A table with 1,000 columns would require the same amount of code to edit.

KVC approach 1: NSKeyValueCoding protocol

All the key-value coding that I’ve show so far uses the NSKeyValueCoding protocol. Actually, I call it a protocol but it is actually an “informal protocol” (a category on NSObject).

This category implements the setValue:forKey: and valueForKey: methods that you can use for setting and getting values by NSString keys.

Advantages
  • Finds getter and setter methods automatically and will even get or set ivars directly if a getter or setter can’t be found. This means that most properties automatically support NSKeyValueCoding. Learn more about the search paths in my Key Value Information post.
  • Includes key paths (for traversing multiple properties).
  • Integrates with NSKeyValueObserving for implementing the Observer design pattern.
  • Offers fallbacks and ways of dealing with undefined keys.
Disadvantages
  • The extended search path makes this one of the slowest key-value coding approaches (see my earlier performance-related post on Replacing Core Data Key Paths).
  • Requires either a method or ivar on the class matching the property name that can be found by NSKeyValueCoding.
  • Only supports NSStrings as property keys.

KVC approach 2: Manual subsets of NSKeyValueCoding behavior

The NSKeyValueCoding protocol looks up methods by selector names and looks up ivars by name.

This is work that you can do yourself.

// Manual KVC setter method implementation
NSString *setterString = [@"set" stringByAppendingString:[someKeyString capitalizedString]];
[someObject performSelector:NSSelectorFromString(setterString) withObject:someValue];

// Manual KVC ivar setter
object_setInstanceVariable(someObject, someKeyString, someValue);

Why would you do this instead of using the NSKeyValueCoding approach? You would only use this in situations where you want to avoid methods or ivars that would ordinarily be found by NSKeyValueCoding. This approach allows you to define your own lookup path.

Advantages
  • More control over the lookup path than with NSKeyValueCoding.
  • Potentially faster than NSKeyValueCoding.
  • Will work for classes that don’t inherit from NSObject and hence don’t have NSKeyValueCoding implementations.
  • Manual method implementations can get and set non-object values.
Disadvantages
  • Less flexible than NSKeyValueCoding.
  • In most cases, it is more work than using NSKeyValueCoding.

KVC approach 3: Associated objects

The Objective-C 2.0 runtime (used on the iPhone and 64-bit Mac OS X apps) allows you to set any object to be associated with any other object. This allows any object in the runtime to have an arbitrary set of extra properties set by key, without support from ivars or methods on the object itself.

objc_setAssociatedObject(someObject, someKey, someValue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

The main reason why you would use this approach is that you want to set properties on an object from the outside — i.e. without the object supporting, being involved with or even knowing about the property access. Properties can be set on an object by other parts of the program for their own purposes.

Advantages
  • No support from the object (methods or ivars) required.
  • Key can be any pointer (so can the object if OBJC_ASSOCIATION_ASSIGN is used).
  • Potentially the fastest KVC approach.
Disadvantages
  • Key is a pointer, not an object, so if an object is used, it must be a pointer-unique object (i.e. won’t work if you try to get and set with different allocations of the same string value).
  • Does not affect ivars or methods on the object. Generally this means if you want the object itself to know about the change, you must use a different approach.

KVC approach 4: selectors as keys

Key-value coding is primarily about looking up a property for a key and then acting upon the property found during lookup.

Objective-C has a lookup at its very core — the method lookup. The keys for this lookup are selectors.

1
objc_msgSend(someObject, someSetterSelector, someValue);

This approach is similar to manually implementing the method part of NSKeyValueCoding but rather than forming a selector string from the key and then looking up the selector string, this approach opts to use the selector as the key.

The disadvantage this approach has is that separate selectors are needed for getting and setting.

Advantages
  • Fastest approach that goes through methods (which is good since methods are overrideable and hence more subclass friendly).
  • Can get and set non-object data (although objc_msgSend_fpret and objc_msgSend_stret need to be used to get float, double and struct properties).
Disadvantages
  • Different keys required for getting and setting.
  • Selectors are not objects and are therefore can’t be stored directly in Objective-C arrays and dictionaries (must use CoreFoundation or NSValue wrappers).

KVC approach 5: do it yourself

The final approach to key-value coding is to handle the implementation yourself. This is something you would do if you needed maximum flexibility (for handling unusual keys/values) or wanted to expose different key-value sets from a single object.

The easiest way to do this is to expose a getter and a setter method and simply get or set the values on a dictionary contained by the object.

1
2
3
4
5
6
7
8
9
- (void)setCollectionValue:(id)value forKey:(NSString *)key
{
    [collectionDictionary setObject:value forKey:key];
}
 
- (id)getCollectionValueForKey:(NSString *)key
{
    return [collectionDictionary objectForKey:key];
}

To handle the internal storage of the values, you could use any of the key-value storage structures in Cocoa:

  • NSMutableDictionary
  • NSMapTable
  • CFMutableDictionaryRef
  • associated objects on self or other objects (see above)

or your own storage solution.

Advantages
  • A single object can exposes multiple, separate collections.
  • Can get and set any data type supported by the underlying collection.
  • The most flexible approach for fallbacks and handling special cases.
Disadvantages
  • Must be implemented by the target class (will not work for arbitrary objects).
  • Doesn’t interoperate with NSKeyValueObserving or any of the other NSKeyValueCoding concepts.

Conclusion

Using key-value coding is not mandatory — it is certainly possible to implement whole projects without it. However, it is one of the best code patterns for reducing repetitious code and making classes more reusable by decoupling actions from properties and data.

As you can see, there are a number of different kinds of key-value coding you can use in your program. NSKeyValueCoding is probably the most flexible, reusable and best supported throughout Cocoa, so unless you need one of the advantages of the other approaches or you want to keep your solution to a problem narrow, it is probably the best approach to use.