文件加载与保存
Cocoa提供了两个通用的文件处理类:属性列表和对象编码。
1. 属性列表
在 Cocoa中,有一类名为属性列表的对象,常简写为 plist。这些列表包含 Cocoa知道如何操作的一组对象。具体来讲,Cocoa知道如何将它们保存到文件中并进行加载。属性列表类包括NSArray、NSDictionary、NSString、NSNumber、NSDate和NSData,以及它们的变体(如果存在变体的话)。
1.1 NSDate
NSDate是Cocoa中用于处理日期和时间的基础类。可以使用[NSDate date]获取当前的日期和时间,它是一个自动释放的对象。因此,以下代码:
1 NSDate *date = [NSDate date]; 2 NSLog(@"Today is %@", date);
将输出如下结果:
1 Today is 2013-07-11 09:53:06 +0800
你可以使用一些方法比较两个日期,从而对列表进行排序。还可以获取与当前时间相隔一定时差的日期。例如,你可能需要24小时之前的确切日期:
1 NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(24 * 60 * 60)]; 2 NSLog(@"Yesterday is %@", yesterday);
+dateWithTimeIntervalSinceNow: 接受一个NSTimeInterval参数,该参数是一个双精度值,表示以秒为单位的时间间隔。通过该参数可以指定时间偏移的方式:对于将来的时间,使用正的时间间隔。对于过去的时间,使用负的时间间隔。
1.2 NSData
NSData包装了大量字节。你可以获得数据的长度和指向字节起始位置的指针。因为NSData是一个对象,适用于常规的内存管理行为。因此,如果将数据库传递给一个函数或方法,可以通过传递一个自动释放的NSData来实现,无需担心内存清楚问题。下面的NSData对象将保存一个普通的C字符串(一个字节序列),然后输出数据:
1 const char *string = "Hi there, this is a C string!"; 2 NSData *data = [NSData dataWithBytes: string length: strlen(string) + 1]; 3 NSLog(@"data is %@", data);
上述将输出字符串的16禁止数据块。-length方法给出字节数,-bytes方法给出指向字符串起始位置的指针。注意到+dataWithBytes:中的 +1 了吗?它用于包含C字符串所需的尾部的零字节。你还会注意到NSLog结果末尾的00.通过包含零字节,可以使用 %s 格式的说明符输出字符串:
1 NSlog(@"%d byte string is '%s'", [data length], [data bytes]);
NSData对象是不可改变的。它们被创建后不能改变。可以使用它们,但不能更改其中的内容。但是 NSMutableData支持在数据内容中添加和删除字节。
1.3 写入和读取属性列表
集合属性列表类(NSArray、NSDictionary)具有一个-writeToFile:atomically:方法,用具将属性列表写入文件。NSString和NSData也具有writeToFile:atomically:方法,但它只能写出字符串或数据块。因此,可以将字符串存入一个数组,然后保存该数组:
1 NSArray *phrase; 2 phrase = [NSArray arrayWithObjects: @"I", @"seem", @"to", @"be", @"a", @"verb", nil]; 3 [phrase writeToFile:@"/tmp/verbiage.txt" atomically: YES];
该文件的内容为:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3 <plist version="1.0"> 4 <array> 5 <string>I</string> 6 <string>seem</string> 7 <string>to</string> 8 <string>be</string> 9 <string>a</string> 10 <string>verb</string> 11 </array> 12 </plist>
以上代码虽然繁琐,但它证实我们要保存的呃内容:一个字符串数组。这些属性列表文件可以为任意负责的形式,可以是包含字符串、数字和日起数组的字典数组。Xcode还包含一个属性列表编辑器,所以可以查看plist文件并进行编辑。有些属性列表文件(特别是首选项文件)是以压缩的二进制格式存储的。通过使用plutil命令:plutil -convert xml1 filename.plist,可以将这些文件转换为可读的形式。
上述文件,可以使用 +arrayWithContentsOfFile:方法读取该文件。代码如下:
1 NSArray *phrase2 = [NSArray arrayWithContentsOfFile: @"/tmp/verbiage.txt"];
2 NSLog(@"%@", phrase2);
说明
atomically:参数的值为BOOL类型,用于通知Cocoa是否应该手续i安将文件内容保存在临时文件中,当文件成功保存后,再将该临时文件和原始文件交换。这是一种安全机制:如果在保存过程中出现意外,不会破坏原始文件。但这种安全机制需要付出一定的代价:在保存过程中,由于原始文件仍然保存在磁盘上,所以需要使用双倍的磁盘空间。除非保存的文件非常大,否则应该自动保存文件。
如果能将数据精简为属性列表类型,则可以使用这些非常便捷的调用来讲内容保存到磁盘中。
NSData处理函数的缺点是:它们不会返回任何错误信息。如果不能加载文件,只能从方法中得到 nil 指针,而不能确定出现了何种错误。
2. 编码对象
无法将所有对象信息表示为属性列表。如果能将所有对象都表示为数组字典,就没必要使用自己的类了。Cocoa具备一种机制来讲对象自身转换为某种格式并保存到磁盘中。对象可以将它们的实例变量和其它数据编码为数据块,然后保存到磁盘中。以后将这些数据块都会到内存中,并且还能基于保存的数据创建新对象。这个过程称为编码和解码或称为序列化和反序列化。
通过采用NSCoding协议,可以使用自己的对象实现相同功能。该协议与下面的代码类似:
1 @protocol NSCoding
2 -(void) encodeWithCoder:(NSCoder *)aCoder;
3 -(id)initWithCoder:(NSCoder *)aDecoder;
4 @end
通过采用该协议,可以实现这两种方法。当对象需要保存自身时,-encodeWithCoder方法将被调用;当对象需要加载自身时,-initWithCoder:方法将被调用。
NSCoder是一个抽象类,定义一些有用的方法来在对象与NSData之间来回转换,完全不需要创建新的NSCoder。实际上我们要使用NSCoder的一些具体的子类来编码和解码对象,如其中的两个子类:NSKeyedArchiver 和 NSKeyedUnarchiver。
键/值编码
许多人将键/值编码亲切的称为KVC,它是一种间接更改对象状态的方式,其实现方法是使用字符串描述要更改的对象状态部分。
1. KVC 简介
键/值编码中基本调用包括 -valueForKey: 和 -setValue:forKey:。以字符串的形式向对象发送消息,这个字符串是需要关注的属性的关键。
因此,请求car的名称示例如下:
1 NSString *name = [car valueForKey:@"name"];
2 NSLog(@"%@", mame);
以上代码将输出 Herbie.
valueForKey:的功能非常简单,它计算车的品牌并将其返回。其执行流程为:首先查找以键 -key或-isKey命名的getter方法。对于这两种调用,valueForKey:查找-name和-make。如果不存在getter方法,它将在对象内部查找名为_key或key的实例变量。如果没有通过@synthesize提供存取方法,valueForKey将会查找实例变量_name和name,或_make和make。
最后一点非常重要:-valueForKey:在Objective-C运行时中使用元数据打开对象并进入其中查找需要的信息。在C或C++语言中不能执行这种操作。通过使用KVC,可以获取不存在getter方法的对象值,无需通过对象指针直接访问实例变量。
可以对型号年份使用相同的技术:
1 NSLog(@"Model year is %@", [car valueForKey: @"modelYear"]);
注意,NSLog中的%@输出一个对象,但modelYear是一个int值,而不是对象。该如和处理?对于KVC,Cocoa自动放入和取出标量值。也就是说,当使用setValueForKey:时,它自动将标量值(int float struct)放入NSNumber或NSValue中;当使用-setValueForKey:时,它自动将标量值从动从对象中取出。仅KVC具有这种自动包装功能,常规方法调用和属性语法不具备该功能。
除用于检索值外,还可以使用-setValue:forKey:按名称设置值:
1 [car setValue: @"Harold" forKey: @"name"];
这个方法的共组方式和-valueForKey:相同。它首先查找名称的setter方法,例如-setName,并使用参数@”Harold”调用它。如果不存在setter方法,它将再类中查找名为name或_name的实例变量,然后为它赋值。
谨记这条规则
编译器都以下划线开头的形式保存实例变量名称。
如果在调用 -setValue:forKey:之前设置一个标量值,需要将它包装起来:
1 [car setValue: [NSNumber numberWithFloat: 25062.4] forKey: @"mileage"];
同时,-setValue:forKey:在调用-setMileage:或更改mileage实例变量之前会取出该值。
2. 路径
键/值编码还支持指定键路径。
键路径表示:可以指定圆点分割的不同属性名称。这样,通过查询car的“engine.horsepower”就能获取马力值。例如:
1 [car setValue: [NSNumber numberWithInt: 155] forKeyPath: @"engine.horsepower"];
2 NSLog(@"%@", [car valueForKeyPath: @"engine.horsepower"]);
键路径的深度是任意的,具体取决于对象图(对象图是一种表示相关对象集合的有趣方式)的复杂度。在某种程度上,使用键路径比使用一系列嵌套方式调用更容易访问对象。
3. 处理未定义的键
使用setValue:forUndefinedKey:方法更改未知键的值。
如果KVC机制无法找到处理方式,它会返回询问类如何处理,默认实现会取消操作。
示例:
首先创建一个可变字典:
1 @interface Garage : NSObject 2 { 3 NSString *name; 4 NSMutableArray *cars; 5 NSMutableDictionary *stuff; 6 } 7 ... 8 @end
接下来添加valueForUndefinedKey方法:
1 -(void)setValue:(id)value forUndefinedKey:(NSString *)key 2 { 3 if(stuff == nil) { 4 stuff = [[NSMutableDictionary alloc] init]; 5 } 6 [stuff setValue: value forKey:key]; 7 } 8 -(id)valueForUndefinedKey:(NSString *)key{ 9 id value = [stuff valueForKey:key]; 10 return (value); 11 }
并使用-dealloc释放字典。
与(null)的区别
是一种[NSNull null]对象,而(null)是一个真实存在的nil值。
NSPredicate
编写软件时,经常需要获取一个对象集合,并通过某些已知条件计算该集合的值。你需要保留复合某个条件的对象,删除那些不满足条件的对象,从而提供一些有意义的对象。Cocoa提供了一个名为NSPredicate的类,它用于指定过滤器的条件。可以创建NSPredicate对象,通过该对象准确地描述所需的条件,对每个对象通过谓词进行筛选,判断它们是否与条件相匹配。注意,这里的谓词通常用在数学和计算机科学概念中,表示计算真值或假值的函数。
1. 创建谓词
在将NSPredicate应用于某个对象之前,首先需要创建它。可以通过两种基本方式来实现。第一种是创建许多对象,并将它们组合起来。这需要使用大量代码,如果正在构建通用用户接口来指定查询,采用这种方式比较简单。另一种方式是查询代码中的字符串。对初学者来说,这种方式比较简单。常见的面向字符串的API警告信息适用于查询字符串,特别适用于缺少编译器错误检查及有时出现奇怪的运行时错误等情况
谓词创建示例:
1 NSPredicate *predicate; 2 predicate = [NSPredicate predicateWithFormat: @"name == 'Herbie'"];
predicate是一个常用的Objective-C对象指针,它将指向NSPredicate对象。使用NSPredicate类方法+predicateWithFormat:来实际创建谓词。将某个字符串赋给谓词,+predicateWithFormat:使用该字符串在后台构建对象树,这些树将用来计算谓词的值。
这种谓词字符串看上去向是标准的C表达式。他的左侧是键路径name,随后是一个等于运算符“==”,右侧是一个引用字符串。如果位子字符串中的文本块未被引用,则该谓词字符串被看作是键路径;如果应用了文本块,则认为它是文本字符串。
计算谓词
通过以上步骤就可以得到一个谓词。接下来将通过某个对象计算谓词。
1 BOOL match = [predicate evaluateWithObject: car]; 2 NSLog(@"%s", (match) ? "YES" : "NO");
-evaluateWithObject:通知对象接收对象(谓词)根据指定的对象计算自身的值。在本例中,接收对象为car,使用name作为键路径,应用valueForKeyPath:方法获取名称。然后,它将自身的值(即名称)与“Herbie”相比较。如果相同,则返回YES,否则返回NO。
以下是另一个谓词:
1 predicate = [NSPredicate predicateWithFormat: @"engine.horsepower > 150"]; 2 match = [predicate evaluateWithObject:car];
这里比较大小的,大于150的将被显示。
2. 燃料过滤器
-filteredArrayUsingPredicate:是NSArray数组中的一种类别方法,它将玄幻过滤数组内容,根据谓词计算每个对象的值,并将值为YES的对象累计到将被返回的新数组中:
1 NSArray *results; 2 results = [car filteredArrayUsingPredicate: predicate]; 3 NSLog(@"%@", results);
3. 格式说明符
如果需要知道那些汽车的马力高于200,稍后又需要知道那些汽车的马力高于50,可以使用谓词字符串。可以通过两种方式将不同的内容放入谓词格式字符串中:格式说明符和变量名。
1 predicate = [NSPredicate predicateWithFormat: @"engine.horsepower > %d", 50];
新的格式字符串:通过NSPredicate字符串,可以使用 %k 指定键路径。该谓词和其他谓词相同,如:
1 predicate = [NSPredicate predicateWithFormat: @"%k == %@", @"name", @"Herbie"];
另一种方式:江边两名放入字符串中,类似于环境变量:
1 NSPredicate *predicateTemplate = [NSPredicate predicateWithFormat: @"name == $NAME"];
接下来,使用predicateWithSubstitutionVariables调用来构造新的专用谓词。创建一个键/值对字典,其中键是变量名(不含$),值是插入谓词的内容:
1 NSDictionary =varDict; 2 varDict = [NSDictionary dictionaryWithObjectsAndKeys: @"Herbie", @"NAME", nil];
这里使用字符串“Herbie”作为键“NAME”的值。因此,构造以下形式的新谓词:
1 predicate = [predicateTemplate predicateWithSubstitutionVariables: varDict];
可以使用不同的对象作为变量名称,如NSNumber.
4. 运算符
4.1 比较运算符
> 大于
>=或=> 大于或等于
< 小于
<=或=< 小于护等于
!=或<> 不等于
谓词字符串语法还支持括号表达式和AND OR NOT逻辑运算符或者C样式的等效表达式 && !! !。
谓词字符串中的运算符不区分大小写。
4.2 数组运算符
使用某个运算符来查找介于两个值之间的数值,如下:
1 predicate = [NSPredicate predicateWithFormat: @"engine.horsepower BETWEEN {50, 200}"];
花括号表示数组,BETWEEN讲述组中的第一个元素看成是数组的下届,第二个元素看成是数组的上界。
可以使用%@格式说明符插入自己的NSArray对象,也可以使用变量。
还可以使用 IN 运算符查找数组中是否含有某个特定值,如 IN {‘A’,’B’,’C’}。
5. SELF 足够了
某些时候,可能需要将谓词应用于简单的值(如那些纯文本老式字符串),而并非那些可以通过键路径进行操作的对象。假设有一个汽车名称数组,并且需要应用前面相同的过滤器,从NSString对象中查询name时,将不能起到预期效果,那么,用什么来代替name呢?
用SELF来解决!SELF可以引用用于谓词计算的对象。事实上,可以将谓词中所有的键路径表示成对应的SELF。此谓词和前面的谓词完全相同,代码如下所示:
predicate = [NSPredicate predicateWithFormat: @"SELF.name IN {'Herbie', 'Snugs', 'Badger', 'Flap'}"];
6. 字符串运算符
BEGINSWITH 检查某个字符串是否以另一个字符串开头,如name BEGINSWITH ‘Bad’
ENDSWITH 检查某个字符串是否以另一个字符串结尾
CONTAINS 检查某个字符串是否在另一个字符串内部
上述匹配是区分大小写的,可以通过修饰符 [d] [cd]来控制
c 表示不区分大小写
d 不区分发音符号(即没有重音符)
cd 上述合二为一
如 name BEGINSWITH[cd] ‘HERB’
7. LIKE 运算符
? 表示于一个字符匹配
* 表示与任意个字符匹配
示例:
name LIKE ‘??er*’
LIKE 同样可以使用修饰符 c d cd
如果你热衷于正则表达式,可以使用 MATCHES 运算符