Objective-C 中的键值编码与静态分析器使用指南
键值编码(Key-Value Coding)
键值编码(KVC)是一种强大的机制,它允许我们通过字符串来间接访问和修改对象的属性。下面我们来详细了解 KVC 的相关知识。
1. 获取唯一值集合
有时候,一个属性可能只有一小部分取值,比如所有汽车的品牌。即使有一百万辆汽车,独特的品牌数量也是有限的。我们可以使用键路径
"cars.@distinctUnionOfObjects.make"
从集合中获取所有独特的品牌:
NSArray *manufacturers;
manufacturers = [garage valueForKeyPath: @"cars.@distinctUnionOfObjects.make"];
NSLog (@"makers: %@", manufacturers);
运行上述代码,输出结果如下:
makers: (
Honda,
Plymouth,
Pontiac,
Acura
)
键路径中的
"@distinctUnionOfObjects"
操作符的作用正如其名。它会对左边指定的集合进行操作,使用右边的键路径对集合中的每个对象进行处理,然后将结果值转换为一个集合。“union” 表示取一组对象的并集,“distinct” 则会去除所有重复的值。
2. 批量操作对象属性
KVC 提供了一对方法来对对象进行批量更改。第一个是
dictionaryWithValuesForKeys:
,它接受一个字符串数组作为参数,使用
valueForKey:
方法获取每个键对应的值,并构建一个包含这些键值对的字典。
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;
}
我们还可以使用
setValuesForKeysWithDictionary:
方法来更改这些值:
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.
需要注意的是,有些值(如品牌、型号和年份)已经改变,但其他值(如名称和里程数)没有改变。
3. 处理 nil 值
字典不能包含
nil
值,对于
nil
值,我们可以使用
[NSNull null]
来表示。例如,对于没有名称的汽车,当调用
dictionaryWithValuesForKeys
时,
@"name"
对应的返回值将是
[NSNull null]
;在使用
setValuesForKeysWithDictionary
时,也可以传入
[NSNull null]
来达到相同的效果。
如果尝试将
nil
值赋给一个标量值(如里程数),会遇到问题:
[car setValue: nil forKey: @"mileage"];
这会导致如下错误:
'[<Car 0x105740> setNilValueForKey]: could not set nil as the value for the key mileage.'
为了解决这个问题,我们可以重写
setNilValueForKey
方法:
- (void) setNilValueForKey: (NSString *) key
{
if ([key isEqualToString: @"mileage"])
{
mileage = 0;
} else {
[super setNilValueForKey: key];
}
}
这里,如果键是
"mileage"
,我们将里程数设置为 0;否则,调用父类的
setNilValueForKey
方法。
4. 处理未定义的键
当使用 KVC 时,如果输入了错误的键,会得到如下错误信息:
'[<Car 0x105740> valueForUndefinedKey:]: this class is not key value coding-compliant for the key garbanzo.'
这表明 Cocoa 无法识别该键。我们可以通过重写
valueForUndefinedKey:
和
setValue:forUndefinedKey:
方法来处理未定义的键。
首先,在
Garage
类中添加一个可变字典:
@interface Garage : NSObject
{
NSString *name;
NSMutableArray *cars;
NSMutableDictionary *stuff;
}
@end
然后,添加处理未定义键的方法:
- (void) setValue: (id) value forUndefinedKey: (NSString *) key
{
if (stuff == nil)
{
stuff = [[NSMutableDictionary alloc] init];
}
[stuff setValue: value forKey: key];
}
- (id) valueForUndefinedKey:(NSString *)key
{
id value = [stuff valueForKey: key];
return (value);
}
最后,在
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"]);
输出结果如下:
values are bunny greeble <null> and (null)
这里,
<null>
表示
[NSNull null]
对象,
(null)
表示真正的
nil
值。
静态分析器(Static Analyzer)
静态分析器是一个强大的工具,它可以在不运行代码的情况下,逻辑地检查代码,查找可能导致运行时错误的问题。下面我们来了解如何使用静态分析器。
1. 静态分析器的作用和原理
静态分析器了解 Objective-C 程序的工作方式,并根据这些知识检查我们的程序。它会遍历应用程序的代码路径,查找逻辑错误并报告给我们,这样我们可以在构建和运行代码之前修复这些问题。
静态分析器可以发现以下几种类型的错误:
-
安全问题
:如内存泄漏和缓冲区溢出。
-
并发问题
:如竞态条件(当两个或多个任务可能因时间安排而失败)。
-
逻辑问题
:包括死代码和各种不良编码习惯。
然而,使用静态分析器也有一些缺点:
- 它会减慢构建过程,因为分析需要时间。
- 有时会产生误报,指出一些实际上不是问题的问题。
- 它会改变我们熟悉的工作流程,需要我们适应它的使用方式。
2. 使用静态分析器
使用静态分析器非常简单。首先,打开一个项目,然后点击 “Product” 菜单,选择 “Analyze”,或者按下
⌘+shift+B
快捷键。
以
19-01 CarParts Error
项目为例,当我们对其进行静态分析时,会发现一些之前不知道的问题。下面我们来逐一分析这些问题。
-
Dead Store
:第一个问题是 “Dead store”,表示我们创建了一个对象(如
pool),但在代码中从未直接访问它。虽然程序在技术上是正确的,但分配和释放内存会消耗时间和存储空间,特别是在 iOS 开发中。通过静态分析器,我们可以将这个变量从应用程序中移除,使程序更加高效。 -
潜在的对象泄漏
:第二个问题是 “潜在的对象泄漏”,具体是
garage对象。我们在main函数的末尾释放了garage对象,但静态分析器显示代码路径从未到达释放garage的那一行。进一步检查发现,在第 177 行有一个多余的return语句,导致函数提前结束,无法清理内存。这种错误很常见,通常是在方法或函数中过早返回而没有释放分配的对象导致的。 -
对象泄漏
:第三个问题是
carsCopy对象的泄漏。我们创建了carsCopy作为cars的可变副本,但从未释放这个副本。我们可以在main函数的末尾释放carsCopy来解决这个问题。 -
内存泄漏
:最后一个问题是
AllWeatherRadial中的内存泄漏。description方法分配了一个字符串desc,但在返回之前没有释放它。我们可以通过编辑返回语句为return [desc autorelease]来解决这个问题。
3. 辅助静态分析器
静态分析器虽然强大,但并不完美。为了帮助它更好地工作,我们可以在方法中使用一些关键字来避免误报。
-
返回保留对象
:使用
NS_RETURNS_RETAINED标记一个返回保留计数大于零的对象的方法。例如:
- (NSMutableArray *)superDuperArrayCreator NS_RETURNS_RETAINED
{
NSMutableArray *myArray = [[NSMutableArray alloc] init];
// … process the myArray
return myArray;
}
对于 Core Foundation 对象,可以使用
CF_RETURNS_RETAINED
关键字。
-
返回非保留对象 :使用
NS_RETURNS_NOT_RETAINED和CF_RETURNS_NOT_RETAINED关键字,当我们尝试返回一个非保留对象时,静态分析器会发出警告。 -
返回空值 :使用
CLANG_ANALYZER_NORETURN关键字确保一个方法返回void。如果尝试返回一个值,静态分析器会显示一个问题。
4. 其他常见问题
-
比较错误
:在 Objective-C 程序中,常见的模式是在条件语句(如
if和while)中同时获取和测试一个值。例如:
if(myValue = [self getValue])
{
// do something
}
这个
if
语句有两种解释方式:
-
myValue
被赋值一个值,然后测试是否为
nil
。
-
myValue
等于方法返回的值。
由于存在歧义,静态分析器会将其标记为一个问题。如果我们真正想要的是第一种含义,可以将其重写为
if((myValue = [self getValue]))
或
if(nil != (myValue = [self getValue]))
;如果是第二种含义,则需要正确比较两个值:
if(myValue == [self getValue])
。
- 内存泄漏 :以下代码看起来没有问题,但静态分析器会揭示其中的内存泄漏问题:
- (void)myMethod
{
NSString *string = [[NSString alloc] initWithFormat:@"%d, %d", 1, 2];
if(nil == string)
{
return;
}
NSArray *array = [[NSArray alloc] initWithObjects:string, nil];
if(nil == array)
{
return;
}
// do some stuff
// Much later
[array release];
[string release];
}
如果
array
的内存分配失败,方法会立即返回,但此时
string
已经分配了内存,并且不会被释放,因为代码不会执行到释放
string
的那一行。
综上所述,键值编码和静态分析器是 Objective-C 开发中非常有用的工具。键值编码可以让我们更方便地访问和修改对象的属性,而静态分析器可以帮助我们在开发过程中发现和解决潜在的问题,提高代码的质量和稳定性。
总结与操作建议
键值编码总结
键值编码(KVC)为我们提供了一种灵活的方式来访问和修改对象的属性。以下是对 KVC 要点的总结:
| 操作类型 | 描述 | 示例代码 |
| — | — | — |
| 获取唯一值集合 | 使用
@distinctUnionOfObjects
操作符从集合中获取唯一值 |
NSArray *manufacturers = [garage valueForKeyPath: @"cars.@distinctUnionOfObjects.make"];
|
| 批量获取属性值 | 使用
dictionaryWithValuesForKeys:
方法获取对象的多个属性值 |
NSDictionary *carValues = [car dictionaryWithValuesForKeys: keys];
|
| 批量设置属性值 | 使用
setValuesForKeysWithDictionary:
方法设置对象的多个属性值 |
[car setValuesForKeysWithDictionary: newValues];
|
| 处理 nil 值 | 使用
[NSNull null]
表示字典中的
nil
值,重写
setNilValueForKey
方法处理标量值的
nil
赋值 |
objc - (void) setNilValueForKey: (NSString *) key { if ([key isEqualToString: @"mileage"]) { mileage = 0; } else { [super setNilValueForKey: key]; } }
|
| 处理未定义的键 | 重写
valueForUndefinedKey:
和
setValue:forUndefinedKey:
方法处理未定义的键 |
objc - (void) setValue: (id) value forUndefinedKey: (NSString *) key { if (stuff == nil) { stuff = [[NSMutableDictionary alloc] init]; } [stuff setValue: value forKey: key]; } - (id) valueForUndefinedKey:(NSString *)key { id value = [stuff valueForKey: key]; return (value); }
|
静态分析器总结
静态分析器是一个强大的工具,能够在不运行代码的情况下发现潜在的问题。以下是静态分析器的相关总结:
| 功能 | 描述 | 操作步骤 |
| — | — | — |
| 发现错误 | 可以发现安全问题、并发问题和逻辑问题 | 打开项目,点击 “Product” 菜单,选择 “Analyze”,或按下
⌘+shift+B
|
| 辅助关键字 | 使用关键字避免误报 | -
NS_RETURNS_RETAINED
:标记返回保留对象的方法
-
CF_RETURNS_RETAINED
:用于 Core Foundation 对象
-
NS_RETURNS_NOT_RETAINED
和
CF_RETURNS_NOT_RETAINED
:标记返回非保留对象的方法
-
CLANG_ANALYZER_NORETURN
:确保方法返回
void
|
操作建议
为了更好地使用键值编码和静态分析器,以下是一些操作建议:
1.
键值编码操作建议
- 在使用 KVC 时,注意性能问题。由于 KVC 需要解析字符串,速度相对较慢,并且编译器不会进行错误检查,因此要确保键路径的正确性。
- 对于
nil
值的处理,遵循使用
[NSNull null]
的原则,避免字典操作时出现错误。
- 在重写
setNilValueForKey
和处理未定义键的方法时,要调用父类的方法,确保代码的健壮性。
2.
静态分析器操作建议
- 定期使用静态分析器检查代码,特别是在代码进行重大修改后。
- 当静态分析器提示问题时,仔细分析问题的原因,不要盲目忽略误报,但也不要被误报误导。
- 使用辅助关键字时,要确保自己清楚方法的返回语义,避免滥用关键字导致分析器失去作用。
流程图:静态分析器使用流程
graph TD;
A[打开项目] --> B[点击 “Product” 菜单];
B --> C[选择 “Analyze” 或按下 ⌘+shift+B];
C --> D[静态分析器开始检查代码];
D --> E{发现问题?};
E -- 是 --> F[分析问题原因];
F --> G[修复问题];
G --> D;
E -- 否 --> H[代码无问题];
总结
键值编码和静态分析器是 Objective-C 开发中不可或缺的工具。键值编码提供了灵活的属性访问和修改方式,而静态分析器则帮助我们提前发现并解决潜在的问题,提高代码的质量和稳定性。通过掌握这两个工具的使用方法和注意事项,我们可以更加高效地进行 Objective-C 开发。希望本文的内容能够帮助你更好地理解和应用键值编码与静态分析器。
超级会员免费看
8

被折叠的 条评论
为什么被折叠?



