第十八章、键/值编码

  • 许多编程思想基于间接机制,我们将介绍另一间接机制,这种机制不属于Objective-C语言的特性,而是Cocoa提供的一种特性。
  • 到目前为止,我们已经介绍了通过直接调用方法、属性的点表示或设置实例变量来直接更改对象状态。键/值编码(key-value coding)是一种间接更改对象状态的方式,许多人称之为KVC,其实现方法是使用字符串表示要更改的对象的状态。

1.入门项目

  • 继续使用Car项目,为了让项目更加生动,我们想Car类添加了一些属性,例如常见的品牌和型号。
@interface Car : NSObject  {
    NSString *name;
    NSMutableArray *tires;
    Engine *engine;
    
    NSString *make;
    NSString *model;
    int modelYear;
    int numberOfDoors;
    float mileage;
}

@property (readwrite, copy) NSString *name;
@property (readwrite, retain) Engine *engine;
@property (readwrite, copy) NSString *make;
@property (readwrite, copy) NSString *model;
@property (readwrite) int modelYear;
@property (readwrite) int numberOfDoors;
@property (readwrite) float mileage;
...
@end // Car
  • 我们添加了@synthesize指令,这样编译器将会自动生成setter和getter方法。
  • 我们还修改了-copyWithZone方法,以匹配新的属性。
  • 我们还更改了-description方法,以输出这些新的属性并省略Engine和Tire的详细输出。
@implementation Car

@synthesize name;
@synthesize engine;
@synthesize make;
@synthesize model;
@synthesize modelYear;
@synthesize numberOfDoors;
@synthesize mileage;

- (id) copyWithZone: (NSZone *) zone
{
    Car *carCopy;
    carCopy = [[[self class] 
                allocWithZone: zone]
               init];
    
    carCopy.name = name;
    carCopy.make = make;
    carCopy.model = model;
    carCopy.numberOfDoors = numberOfDoors;
    carCopy.mileage = mileage;
    
    Engine *engineCopy;
    engineCopy = [[engine copy] autorelease];
    carCopy.engine = engineCopy;
    
    int i;
    for (i = 0; i < 4; i++) {
        Tire *tireCopy;
        
        tireCopy = [[self tireAtIndex: i] copy];
        [tireCopy autorelease];
        
        [carCopy setTire: tireCopy
                 atIndex: i];
    }
    
    return (carCopy);
    
} // copyWithZone

- (NSString *) description {
    NSString *desc;
    desc = [NSMutableString stringWithFormat: 
            @"%@, a %@ %@, has %d doors, %.1f miles, and 4 tires:",
            name, make, model, numberOfDoors, mileage];

    return desc;
    
} // description

@end // Car
  • 最后在main函数中,我们将为car设定以上属性,并将它们输出。同时,我们使用了autorelease以及alloc和init的方法,这样可以在同一位置执行完所有的内存管理。
int main (int argc, const char * argv[])
{
    @autoreleasepool
    {
        Car *car = [[[Car alloc] init] autorelease];
        car.name = @"Herbie";
        car.make = @"Honda";
        car.model = @"CRX";
        car.modelYear = 1984;
        car.numberOfDoors = 2;
        car.mileage = 110000;
        
        for (int i = 0; i < 4; i++)
        {
            AllWeatherRadial *tire;
            tire = [[AllWeatherRadial alloc] init];
            [car setTire: tire atIndex: i];
            [tire release];
        }
        
        Slant6 *engine = [[[Slant6 alloc] init] autorelease];
        car.engine = engine;
       
        NSLog (@"Car is %@", car);
     }
}

运行结果:Car is Herbie, a 1984 Honda CRX, has 2 doors, 110000.0 miles, and 4 tires.

2.KVC简介

  • 键/值编码中的基本调用是-valueForKey:和-setValue:方法。你可以向对象发送消息,并传递你想要访问的属性名称的键作为参数。

  • 那么,我们可以这样访问car对象的name属性:

NSString *name = [car valueForKey:@"name"];
NSLog(@"%@",name);
  • 以上代码将输出Herbie。使用类似的方法,我们还可以获取品牌信息。
NSLog(@"make is %@",[car valueForKey:@"make"]);
  • valueForKey:的功能非常强大,它可以找到make属性的值并将其返回。

  • valueForKey:会首先查找以参数命名(格式为-key或-isKey)的getter方法。对于以上两个调用,valueForKey:会先寻找-name和-make方法。如果没有这样的getter方法,它将会在对象内寻找名称格式为_key或key的实例变量。如果我们没有使用@synthesize来提供访问方法,valueForKey方法将会寻找名称为_name和name以及_make和make的实例变量。

  • 最后,非常重要的一点是,-valueForKey在Objective-C运行时中使用元数据打开对象并进入其中查找需要的信息。在C或C++语言中不能执行这种操作。通过使用KVC,没有相关getter方法也能获取对象值,不需要通过对象指针来直接访问实例变量。

  • 可以对型号年份使用同样的技术:

NSLog(@"model year is %@",[car valueFoeKey:@"modelYear"]);
  • 它的输出结果为:model year is 1984。

  • NSLog中的%@是用来输出对象的,但modelYear是一个int值,而不是对象。这该如何处理呢?对于KVC,Cocoa会自动装箱和开箱标量值。也就是说,当使用setValueForKey时,它自动将标量值(int,float和struct)放入NSNumber和NSValue中;当使用-setValueForKey时,它自动将标量值从这些对象中取出。仅KVC具有这种自动装箱功能,常规方法调用和属性语法不具备该功能。

  • 除了检索值外,还可以使用-setValue:forKey:方法依据名称设置值。

[car setValue:@"Harold" forKey:@"name"];
  • 这个方法的工作方式和-valueForKey:相同。他首先查找名称的setter方法,例如-setName,然后调用它并传递参数@"Harold"。如果不存在setter方法,它将在类中寻找名为name或_name的实例变量,然后为它赋值。

  • 谨记以下规则:编译器和苹果公司都以下划线开头的形式保存实例变量名称,如果你尝试在其他地方使用下划线,可能会出现严重的错误。你可以不遵守这条规则,但如果不遵守它,有可能会遇到某种风险。

  • 如果你想要设置一个标量值,在调用-setValue:forKey:方法之前需要将它们包装起来,也就是装箱到对象中。

[car setValue:[NSNumber numberWithFloat: 25062.4] forKey:@"mileage"];
  • 此外,-setValue:forKey:方法会先开箱取出该值,再调用-setMileage:方法或更改mileage实例变量。

3.键路径

  • 除了通过键设置值以外,键/值编码还支持指定键路径,就像文件系统路径一样,你可以遵循一系列关系来指定该路径。

  • 为了更深入的了解这项功能,不放加大引擎的马力。向Engine类添加一个新的实例变量。

@interface Engine : NSObject{
   int horsepower;
}
@end
  • 请注意我们没有添加任何访问方法或属性。通常需要为有用的对象添加访问方法或属性,不过为了展示KVC是如何直接获取对象的,在这里我们不会使用这些方法。

  • 为了让引擎有马力能够启动,我们添加了一个init方法。

- (id) init {
   if (self = [super init]) {
       horsepower;
   }
   return self;
}
  • 另外,我们也在-copyWithZone方法中添加了关于horsepower实例变量的代码,这样在复制对象时也能获取这个值,并且在-description方法中也添加了相关代码。我们已经很熟悉这个方法了,所以这里就不详细介绍了。

  • 在实现文件添加以下代码,以确保能够获取并设置值。

NSLog(@"horsepower is %@",[engine valueForKey:@"horsepower"]);
[engine setValue:[NSNumber numberWithInt:150] forKey:@"horsepower"];
NSLog(@"horsepower is %@",[engine valueForKey:@"horsepower"]);
  • 运行这段代码将会输出:horsepower is 145 horsepower is 150

  • 如何表示这些键路径呢?可以在对象和不同的变量名称之间用圆点分开。通过查询car的engine.horsepower,就能够获马力值。现在,我们试着使用-valueForKeyPath和-setValueForKeyPath方法来访问键路径。将以下消息发送给car对象,而不是发送给engine。

[car setValue:[NSNumber numberWithInt:150] forKey:@"horsepower"];
NSLog(@"horsepower is %@",[car valueForKey:@"horsepower"]);
  • 这些键路径的深度是任意的,具体取决于对象图(object graph,可以表示对象之间的关系)的复杂度,可以使用诸如car.interior.airconditioner.fan.velocity这样的键路径。在某种程度上,使用键路径比使用一些列嵌套方法调用更容易访问到对象。

4.整体操作

  • 关于KVC非常棒的一点是,如果使用某个关键值来访问一个NSArray数组,它实际上会查询相应数组中的每个对象,然后将查询结果打包到另一个数组中并返回给你。这种方法也同样适用于通过键路径访问的位于对象中的数组(是不是想到了以前说过的复合?)。

  • 在KVC中,通常认为对象中的NSArray具有一对多的关系。举个例子,汽车与多个(一般都是四个)轮胎具有联系。因此,我们可以说Car与Tire之间存在一对多的关系。如果键路径中含有一个数组属性,则该键路径的其余部分将被发送给数组中的每个对象。

  • 一对一关系:你现在已经了解了一对一关系,可能还想知道什么是一对一的关系。一般对象的复合都是一对一的关系。例如,汽车与引擎之间就是一对一的关系。

  • 还记得Car类中有一个tires数组吗?每个轮胎都有它自己的空气压力。我们可以在一个调用中获取所有的轮胎压力值。

NSArray *pressure = [car valueForKeyPath:@"tires.pressure"];
NSLog(@"pressures %@",pressures);
  • 调用以下代码之后,就会输出如下结果:
pressures(
34,
34,
34,
34)
  • 除了告诉我们轮胎的状态之外,这里还发生了什么呢?valueForKeyPath:将路径分解并从左向右进行处理。首先,它向car对象请求轮胎信息,然后使用键路径的剩余部分(在本示例中是pressure)向tires对象调用valueForKeyPath:方法。NSArray实现valueForKeyPath:的方法是循环遍历它的内容并向每个对象发送消息。因此,NSArray向每个在自身之中的tire对象发送了参数以pressure作为键路径的valueForKeyPath:消息,结果就会将tire对象的pressure变量封装到NSNumber对象中并返回。非常方便。

  • 不幸的是,不能在键路径中索引这些数组,例如使用tires[0].pressure来获取第一个轮胎的压力值。

4.整体操作

4.1休息一下

  • 在介绍键/值编码的下一个优点之前,我们将添加一个名为Garage的新类,用于存放各种不同类型的car对象。下面是Garage类的接口类容。
#import 

@class Car;

@interface Garage : NSObject {
    NSString *name;
    NSMutableArray *cars;
    NSMutableDictionary *stuff;
}

@property (readwrite, copy) NSString *name;

- (void) addCar: (Car *) car;

- (void) print;

@end // Garage
  • 此处没有涉及新内容。我们在一开始就声明了Car类,因为需要知道这个对象类型被用作-addCar:方法的参数。name是一个属性值,而@property语句表示使用Garage类的人可以访问和更改name属性值。并且代码中还有用来输出对象内容的方法。为了实现一个cars对象集合,我们添加一个cars对象集合,我们添加一个可变数组的实例变量。

  • 实现代码的内容同样很简单。

#import "Garage.h"

@implementation Garage

@synthesize name;

- (void) addCar: (Car *) car {
    if (cars == nil) {
        cars = [[NSMutableArray alloc] init];
    }
    [cars addObject: car];
    
} // addCar


- (void) dealloc {
    [name release];
    [cars release];
    [stuff release];
    [super dealloc];
} // dealloc


- (void) print {
    NSLog (@"%@:", name);
    
    for (Car *car in cars) {
        NSLog (@"    %@", car);
    }
    
} // print


- (void) setValue: (id) value  forUndefinedKey: (NSString *) key {
    if (stuff == nil) {
        stuff = [[NSMutableDictionary alloc] init];
    }
    [stuff setValue: value forKey: key];
} // setValueForUndefinedKey


- (id) valueForUndefinedKey:(NSString *)key {
    id value = [stuff valueForKey: key];
    return (value);
} // valueForUndefinedKey

@end  // Car
  • 像往常一样,我们包含了Garage.h的头文件并使用@synthesize合成了name属性的存取方法。

  • -addCar:是cars数组懒性初始化的一个示例,我们仅在需要时才创建它。-dealloc用于清理name属性和数组,而-print遍历数组并输出各种类型汽车的信息。

  • main函数里面,我们先使用一个函数构造汽车的各种属性。我们可以创建一个Car类的类方法,也可以创建多种类型的工厂类,因为Objective-C仍然是一种C语言,所以可以使用函数。再次,我们使用函数是因为组装汽车的函数代码与实际组装汽车的方式比较接近。

Car *makeCar (NSString *name, NSString *make, NSString *model,
              int modelYear, int numberOfDoors, float mileage,
              int horsepower) {
    Car *car = [[[Car alloc] init] autorelease];
    
    car.name = name;
    car.make = make;
    car.model = model;
    car.modelYear = modelYear;
    car.numberOfDoors = numberOfDoors;
    car.mileage = mileage;
    
    Slant6 *engine = [[[Slant6 alloc] init] autorelease];
    [engine setValue: [NSNumber numberWithInt: horsepower]
              forKey: @"horsepower"];
    car.engine = engine;
    
    
    // Make some tires.
    // int i;
    for (int i = 0; i < 4; i++) {
        Tire * tire= [[[Tire alloc] init] autorelease];
        [car setTire: tire  atIndex: i];
    }
    
    return (car);
    
} // makeCar
  • 现在,上面的代码你基本都已经熟悉了。按照Cocoa的惯例构造并自动化释放一个新Car对象,因为通过这个函数获得的car对象没有调用new,copy或alloc方法。然后,我们设置了一些属性。请记住,这项技术与KVC不同,我们没有使用setValue:forKey方法。接下来,我们创建了一个engine对象,因为我们没有为它创建存取方法,所以使用KVC设置马力值。最后,构造一些tire对象并将它们安置在car对象中。最后会返回新的car对象。

  • 以下是新版的main()函数。

int main (int argc, const char * argv[])
{
    @autoreleasepool
    {
        Garage *garage = [[Garage alloc] init];
        garage.name = @"Joe's Garage";
        
        Car *car;
        car = makeCar (@"Herbie", @"Honda", @"CRX", 1984, 2, 110000, 58);
        [garage addCar: car];
        
        car = makeCar (@"Badger", @"Acura", @"Integra", 1987, 5, 217036.7, 130);
        [garage addCar: car];
        
        car = makeCar (@"Elvis", @"Acura", @"Legend", 1989, 4, 28123.4, 151);
        [garage addCar: car];
        
        car = makeCar (@"Phoenix", @"Pontiac", @"Firebird", 1969, 2, 85128.3, 345);
        [garage addCar: car];
        
        car = makeCar (@"Streaker", @"Pontiac", @"Silver Streak", 1950, 2, 39100.0, 36);
        [garage addCar: car];
        
        car = makeCar (@"Judge", @"Pontiac", @"GTO", 1969, 2, 45132.2, 370);
        [garage addCar: car];
        
        car = makeCar (@"Paper Car", @"Plymouth", @"Valiant", 1965, 2, 76800, 105);
        [garage addCar: car];
        
        [garage print];
        
        [garage release];
    }
    return (0);
} // main

//运行结果
2018-12-23 03:04:55.811624-0800 Car-Value-Garaging[570:12976] Joe's Garage:
2018-12-23 03:04:55.811864-0800 Car-Value-Garaging[570:12976]     Herbie, a 1984 Honda CRX, has 2 doors, 110000.0 miles, 58 hp and 4 tires
2018-12-23 03:04:55.811891-0800 Car-Value-Garaging[570:12976]     Badger, a 1987 Acura Integra, has 5 doors, 217036.7 miles, 130 hp and 4 tires
2018-12-23 03:04:55.811906-0800 Car-Value-Garaging[570:12976]     Elvis, a 1989 Acura Legend, has 4 doors, 28123.4 miles, 151 hp and 4 tires
2018-12-23 03:04:55.811919-0800 Car-Value-Garaging[570:12976]     Phoenix, a 1969 Pontiac Firebird, has 2 doors, 85128.3 miles, 345 hp and 4 tires
2018-12-23 03:04:55.811931-0800 Car-Value-Garaging[570:12976]     Streaker, a 1950 Pontiac Silver Streak, has 2 doors, 39100.0 miles, 36 hp and 4 tires
2018-12-23 03:04:55.811943-0800 Car-Value-Garaging[570:12976]     Judge, a 1969 Pontiac GTO, has 2 doors, 45132.2 miles, 370 hp and 4 tires
2018-12-23 03:04:55.811956-0800 Car-Value-Garaging[570:12976]     Paper Car, a 1965 Plymouth Valiant, has 2 doors, 76800.0 miles, 105 hp and 4 tires
  • main()函数进行了一些信息输入,创建了一个garage对象,还创建了一些car对象并保存在garage对象中。最后,main函数输出了garage对象的信息并将其释放。

4.整体操作

4.2快速运算

  • 键路径不仅能引用对象值,还可以引用一些运算符来进行一些运算,例如能获取一组值得平均值或返回这组值中的最小值和最大值。

  • 举个例子,通过以下代码可以计算汽车的数量。

NSNumber *count;
count = [garage valueForKeyPath:@"count"];
NSLog(@"We have %@ cars",count);

//运行结果
We have 7 cars
  • 我们将键路径cars.@count拆开来理解。cars用于获取cars属性,它是取自garage对象的NSArray类型的值。我们知道,它是一个NSMutableArray,但如果我们不打算更改数组的任何内容,可以将其视为NSArray类型。接下来是@count,其中的@符号意味着后面将进行一些运算。对编译器来说,@"blah"是一个字符串,而@interface用于声明类。此处的@count用于通知KVC机制计算键路径左侧值的对象总数。

  • 此外,我们还可以计算某些值的总和,例如,汽车行驶的总英里数。以下代码段

NSNumber *sum;
sum = [garage valueForKeyPath:@"[email protected]"];
NSLoag(@"We have a grand total of %@ miles",sum);
  • 运行后将输出
We have a grand total of 601320.6 miles
  • 这项功能是如何做到的呢?@sum运算符将键路径分成两部分。第一部分可以看成一对多关系的键路径,在本例中代表cars数组。另一部分可以看成包含一对多关系的键路径。它被当做用于关系中每个对象的键路径。因此mileage消息被发送给了cars关系中所有的对象,然后将结果值相加。当然,每个键路径的长度可以是任意的。

  • 如果需要得到平均每辆汽车行驶的距离,可以用总数除以汽车数量,但还有一种更简单的方法。以下几行代码

NSNumber *avgMileage;
avgMileage = [garage valueForKeyPath:@"[email protected]"];
NSLog(@"average is %.2f",[avgMileage floatValue]);

//运行结果
average is 85902.95
  • 如果没有键/值编码的这个优点,我们还需要写一段不短的代码来完成。

  • 现在我们将键路径[email protected]分开。和@sum一样,@avg运算符将键路径分成了两部分。在本例中,@avg之前的部分为cars,是汽车一对多关系的键路径;@avg之后是另一个键路径,它仅表示距离。在后台,KVC能够轻松地进行循环,将值累加,并计算总数,然后再进行除法运算。

  • 还有@min和@max运算符,它们的功能很明显。

NSNumber *min,*max;
min = [garage valueForKeyPath:@"[email protected]"];
max = [garage valueForKeyPath:@"[email protected]"];
NSLog(@"minmax: %@/%@",min,max);
  • 输出结果为:
minmax: 28123.4/217036.7
  • 不要滥用KVC:既然KVC能够非常轻松地处理集合类,为什么不用它来处理所有对象,抛弃存取方法和其他代码的编写呢?KVC需要解析字符串来计算你需要的答案,因此速度比较慢。此外,编译器还无法对它进行错误检查。你可以想要处理[email protected],但编译器不能判断它是否是错误的键路径。因此,当你尝试使用它时,就会出现运行错误。

  • 有时你使用的变量的值只有几个,例如上面构造的所有汽车。即便我们有100万辆汽车,品牌的种类也会很少。通过使用键路径[email protected],就可以从集合中只获取各个品牌的名称。

NSArray *manufacturers;manufacturers = [garage valueForKeyPath:@"[email protected]"];
NSLog(@"makers: %@",manufacturers);
  • 运行以上代码,将得到以下结果:
maker:(
   Honda,
   Plymouth,
   Pontiac,
   Acura
)
  • 键路径中间的运算符名称为@distinctUnionOfObjects,它看上去很复杂,但由名称就能了解它的功能。它和其他运算符的应用原理相同:获取左侧指定的集合,对该集合中的每个对象使用右侧的键路径,然后将结果转换为一个集合。名称中的union指一组对象的并集,distince用于删除重复内容。还有很多其他运算符也沿用了这种工作方式,这些内容留给你们自己去探索。不过,你无法添加自己的运算符,这一点比较遗憾。

5.批处理

  • KVC包含两个调用,可以使用它们为对象进行批量更改。第一个调用是dictionaryWithValuesForKeys:方法,它接受一个字符串数组。该调用获取键的名称,并对每个键使用valueForKey:方法,然后为键字符串和刚刚获取的值构建一个字典。

  • 我们从garage对象中挑选一个car对象,并使用其中一些变量来创建一个字典。

car = [[garage valueForKeyPath:@"cars"] lastObject];
NSArray *keys = [NSArray arrayWithObjects:@"make",@"model",@"modelYear",nil];
NSDictionary *carValues = [car dictionaryWithValuesForKeys:keys];
NSLog(@"Car values : %@",carValues);
  • 运行以上代码,我们将获取一些相关信息。
Car values :{
make = Plymouth;
model = Valiant;
modelYear = 1965;
}
  • 我们还可以更改这些值,将Valiant变成新的型号,升级为Chevy Nova。
NSDictionary *newValues = [NSDictionary dictionaryWithObjectsAndKeys:@"Chevy",@"make",@"Nova",@"model",[NSNumber numberWithInt:1964],@"modelYear",nil];
[car setValuesForKeysWithDictionary:newValues];
NSLog(@"car with new values is %@",car);
  • 运行以上这些代码后,我们会发现它确实变成了一辆新车。
car with new values is Paper Car,a 1964 Chevy Nova,has 2 doors,76800.0 miles, and 4 tires.
  • 请注意,某些值(品牌、型号和年份)发生了变化,但是名称和行驶距离等没有变

  • 在本程序中,这个工具不是特别有用,不过它还支持在用户界面代码中实现一些不错的功能。例如,通过苹果公司的Aperture程序中的Lift and stamp工具,可以把对某一张图片的部分修改同样用在其他图片上。可以使用dictionaryWithValuesForKeys方法获取所有变量,并将字典中的内容全部显示在用户界面上。用户可以使用setValuesForKeysWithDictionary方法获取字典内容并对其他图片进行更改。如果你正确地设计了你的用户界面类,也可以对其他对象(比如图片,cars对象或食谱)使用相同的lift and stamp面板。

6.nil仍然可以用

  • 字典不能包含nil值,但如果出现nil值会怎样呢(例如一辆没有名字的汽车)?回想一下第七章的内容,我们使用[NSNull null]表示nil值。同样地,当调用dictionaryWithValuesForKeys时,对于没有名称的汽车,@"name"键下将返回[NSNull null]。你也可以为setValuesForKeysWithDictionary提供的[NSNull null],这样汽车就会没有名字了。

  • 对nil值的讨论引出了一个有趣的问题。标量值(例如mileage)中的nil表示什么?0?-1?圆周率?Cocoa无法知道它代表什么。你可以尝试以下代码。

[car setValue: nil forKey:@"mileage"];
  • 不过Cocoa会给出以下警告信息。
[ setNilValueForKey]: could not set nil as the value for the key mileage.';
  • 为了解决该问题,可以重写项目-setNilValueForKey方法的实现,并提供逻辑上有意义的任何值。我们先约定好,nil值表示行驶零距离,而不是像-1之类的其他值。
- (void) setNilValueForKey : (NSString *) key {
     if([key isEqualToString:@"mileage"]){
        mileage = 0;
     }
     else {
        [super setNilValueForKey:key];
     }
}
  • 请注意,如果得到一个意料之外的键,我们将调用超类方法。这样的话,如果某人视图对键/值编码使用了我们不能理解的键,调用者将会得到警告信息。一般来说,除非你有某些特殊的原因(比如我不想执行某个操作),否则应该总是在重写的代码中调用超类的方法。

7.处理未定义的键

  • 如果你使用了KVC,并且输入了错误的键你可能会看到以下消息。
[ valueForUnderfinedKey:]: this class is not key value coding-compliant for the key garbanzo.'
  • 以上消息的主要含义是,Cocoa不能识别你使用的这个键,因此放弃了操作。

  • 如果仔细分析错误消息,你会注意到,它提到了valueForUnderfinedKey:方法。你也许能够猜到,我们可以通过重写该方法来处理未定义的键。也许你还能猜到,如果要更改未知键的值,还可以使用相应的setValue:forUnderfine的Key:方法。

  • 如果KVC机制无法找到处理方式,会退回并询问类该如何处理。默认的实现会取消操作,就像你在前面所看到的。但是我们可以更改默认的行为,将Garage转换成一个非常灵活的对象,通过它可以设置和获取任何键。我们首先添加一个可变字典。

@interface Garage : NSObject {
    NSString *name;
    NSMutableArray *cars;
    NSMutableDictionary *stuff;
}
...
@end
  • 接下来添加valueForUnderfinedKey:方法的实现
- (void) setValue: (id) value  forUndefinedKey: (NSString *) key {
    if (stuff == nil) {
        stuff = [[NSMutableDictionary alloc] init];
    }
    [stuff setValue: value forKey: key];
} // setValueForUndefinedKey


- (id) valueForUndefinedKey:(NSString *)key {
    id value = [stuff valueForKey: key];
    return (value);
} // valueForUndefinedKey
  • 并在-dealloc方法中释放字典。

  • 现在可以设置garage对象上的任何值:

 [garage setValue: @"bunny" forKey: @"fluffy"];
 [garage setValue: @"greeble" forKey: @"bork"];
 [garage setValue: [NSNull null] forKey: @"snorgle"];
 [garage setValue: nil forKey: @"gronk"]; 
  • 然后将它们的内容输出:
NSLog (@"values are %@ %@ %@ and %@",[garage valueForKey: @"fluffy"], [garage valueForKey: @"bork"],[garage valueForKey: @"snorgle"], [garage valueForKey: @"gronk"]);
  • 这个NSLog将输出以下结果:
values are bunny greeble  and (null)
  • 请注意和(null)之间的区别。是一种[NSNull null]对象,而(null)是一个真正的nil值。由于字典没有键为gronk的值,所以到此处我们得到了nil值。还要注意,在使用stuff字典时,我们使用了KVC的setValue:forKey:方法。通过这种方法,调用者可以直接传入nil值,我们不必在代码中检查它。而如果为NSDictionary类的setObjective:forKey:提供nil值,它将会给出警告信息。此外,如果在字典中对setValue:forKey:方法传入nil值,可能会把对应键的值从字典中删除。

你可能感兴趣的:(第十八章、键/值编码)