├── 1.介绍Introduction.md ├── 10.命名规则Conventions.md ├── 2.定义类Defining-Classes.md ├── 3.对象的使用Working-with-Objects.md ├── 4.数据的封装Encapsulating-Data.md ├── 5.定制已有的类Customizing-Existing-Classes.md ├── 6.协议的使用Working-with-Protocols.md ├── 7.值与集合类型Values-and-Collections.md ├── 8.使用块Working-with-Blocks.md ├── 9.错误处理Dealing-with-Errors.md └── README.md /1.介绍Introduction.md: -------------------------------------------------------------------------------- 1 | #关于 Objective-C 2 | Objective-C是你写OS X(maxOS)程序和iOS程序的首选语言。它作为C语言的一个超集(即C语言是它的一部分)提供了面对对象的特性和动态运行时`runtime`。它继承了C语言的语法,基本类型,和控制流并加入了定义`类`和`方法`的语法。他提供了`动态类型`和`动态绑定`,同时对`对象图`管理,`object literals`(极其方便地定义数组等对象的方法——一个语法糖) 提供了语言层面上的支持。 3 | 4 | ## 概述 5 | 这个文档引入了大量的用法例程来介绍Objective-C语言。你将会学到如何创建你自己的类来描述自定义对象和怎样使用`Cocoa`和`Cocoa Touch`提供的一些框架类。尽管这些框架类并不是语言的一部分,但使用Objective-C编程时,这些类的使用是不可避免的。许多语言级别的特性也依赖于这些类所提供的行为。 6 | 7 | ### 程序是由一个对象网络构建的 8 | 当你为OS X和iOS构建程序时,你大部分时间都在于对象打交道。这些对象是Objective-C类的实例。这些类可以是由`Cocoa`或`Cocoa Touch`提供的,也可以是你自己写的类。 9 | 如果你要写你自己的类,要在开始处提供对这个类的描述,详细写出它的`public interface`。`interface`包括数据封装的公共`特性`` properties`(`properties` 用于相关数据的封装),和一系列的`方法`。方法声明是指,一个对象可以收到的信息,包括当使用这个方法时所需要参数。你还需要提供一个类的实现`implementation`,包括每一个你公开的方法的可执行代码。 10 | 11 | > 相关章节:[定义类Defining Classes](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/2.定义类Defining-Classes.md),[对象的使用Working with Objects](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/3.对象的使用Working-with-Objects.md), [数据的封装Encapsulating Data](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/4.数据的封装Encapsulating-Data.md) 12 | 13 | ### 用类别`Categories`扩展已经存在的类 14 | 15 | 如果你想要给一个已存在的类添加很少的特性,最好不要构建一个全新的类。更好的方法是定义一个类别`category`来给一个已经存在的类添加自定义特性。你可以用类别来给任何一个类添加方法,甚至那些你并没有原始的实现`implementation`代码的类,比如像`Nsstring`这样的框架类。 16 | 17 | 如果你有你想修改的类的源代码,你可以通过类扩展`class extension`来添加特性,或者修改原有特性的属性。类扩展一般用于隐藏私有行为,用在有源代码文件或私有的自定义框架的实现中。 18 | 19 | > 相关章节:[定制已有的类Customizing-Existing-Classes]() 20 | 21 | ### 用协议`protocols`定义信息传输方式 22 | 大部分Objective-C app 的工作都是由对象间的数据的传输而产生的。这些信息(即方法)一般是被在类的`interface`中显示声明的方法定义的了。但是,有时候定义一组不直接绑定到某个特定类的相关的方法也是很有用的。 23 | 24 | Objective-C用协议`protocol`来定义一组相关的方法。例如一个对象调用它的委托`delegate`,这些方法可以是可选的`optional`或者要求的`required`。任何一个类都可以声明它遵循这个协议`protocol`,但是这个类的实现`implementation`中必须包含所有在这个`protocol`中的标记为`required`的方法。 25 | 26 | > 相关章节:[协议的使用Working-with-Protocols](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/6.协议的使用Working-with-Protocols.md) 27 | 28 | ### Objective-C的对象通常是值`values`和集合`Collections` 29 | 30 | 在Objective-C中经常用`Cocoa`和`Cocoa Touch`类来代表值。类`Nesstring` 31 | 用于字符串;类`NSNumber`用于不同类型的数字,包括整数,浮点数等等;类`NSValue`用于其他的值,比如C的结构体。你还可以使用任何C中的原始类型,比如`int`,`float`,`char`。 32 | 33 | 集合通常是一个集合类的实例,比如集合类`NSArray`,`NSSet`,`NSDictionary`。这些集合类都是用于收集其他Objective-C类的。 34 | 35 | > 相关章节:[值与集合类型Values-and-Collections](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/7.值与集合类型Values-and-Collections.md) 36 | 37 | ### 块`blocks`简化了一般的任务 38 | 39 | 块`Blocks`是C,Objective-C和C++的一个语言特性,用来表示一个工作单元。Blocks压缩一块代码和它被捕获的状态,这使它们更像是其他编程语言中的闭包`closures`。块`Blocks`经常用于简化一般的任务,比如枚举集合,分类和测试。Blocks还能更容易地使用中央调度等技术(GCD)安排任务并发或异步执行。 40 | 41 | > 相关章节: 42 | [使用块Working-with-Blocks](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/8.使用块Working-with-Blocks.md). 43 | 44 | 45 | ### 错误对象`Error Objects`用在运行时错误中 46 | 47 | 虽然Objective-C语法中包含异常处理,但`Cocoa` and `Cocoa Touch`只在处理编程错误(比如数组越界访问)时提示异常。这些错误应该在你的app上架之前解决。 48 | 49 | 其他的一切错误,包含运行时问题,比如耗尽磁盘空间,或者连不上一个网页。这些问题都是由`NsError`类的实例来显示的。为了良好的用户体验,你应该提前预判这些可能发生的错误,并且在错误发生时提供最好的解决方案给用户。 50 | 51 | > 相关章节:[错误处理Dealing-with-Errors](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/9.错误处理Dealing-with-Errors.md) 52 | 53 | ### Objective-C编程应遵守命名规则 54 | 55 | 当你在进行Objective-C编程时,你应当记住一些已建立的命名规则。比如方法的命名:以小写字母开头,使用驼峰式大小写多个单词,例如,`doSomething`或者`doSomethingElse`。并不只是大写一些字母很重要。重要的是尽可能的让你的代码有更好可读性。这就意味着,你的方法名要很有表现力,但是不能太冗长。 56 | 57 | 另外,如果你想充分利用语言和框架的优势,还有一些其他的命名规则是你要遵守的。比如属性访问器方法必须严格遵守命名规则,这样才能用[Key-Value Coding(KVC)]() 或者[Key-Value Observing(KVO)]()这样的技术。 58 | 59 | > 相关章节: [命名规则Conventions](https://github.com/Yaoyaoi/Programming-with-Objective-C-in-Chinese/blob/dev/10.命名规则Conventions.md) 60 | 61 | ## 预备知识 62 | 63 | 如果你是iOS或者OS X开发的新手,你在看这篇文档之前应该先通过看*Start Developing iOS Apps Today*或者*Start Developing Mac Apps Today*来了解开发iOS和OS X应用程序的一般过程。另外,为了完成每一章节后面的练习,你应该熟悉Xcode的使用方法。Xcode 是开发iOS和OS X应用程序的集成开发环境(IDE,Integrated Development Environment)。你将会用它来写你的代码,设计你app的用户界面,测试你的应用程序,并且调试任何的问题。 64 | 65 | 尽管这个文档包含一些基础C语言特性的例程,比如流程控制语句。但是如果你有一定的C语言基础或者熟悉其中一种以C语言为基础的语言,例如Java或者C#,学习该文档的效果会更好。如果你有其他高级编程语言的基础,比如Ruby或者Python,你应该可以看懂这个文档的内容。 66 | 67 | 这篇文档合理的包含了一些面对对象编程的原则,尤其是那些适用于Objective-C环境的,但是,你至少要有一点基础的面对对象的观念。如果你不熟悉这些观点,你应该读一下相关章节[*Concepts in Objective-C Programming*](https://developer.apple.com/library/content/documentation/General/Conceptual/CocoaEncyclopedia/Introduction/Introduction.html#//apple_ref/doc/uid/TP40010810)(生肉)。 68 | 69 | ## 其他 70 | 71 | 这篇文章的内容适用于Xcode4.4和它以后的版本,并且我们假定你的目标是OS Xv10.7或者更新的版本。想要了解更多有关Xcode的知识,可以看*Xcode User Guide*。想知道关于语言特性的可用性,请移步[*Objective-C Feature Availability Index*](https://developer.apple.com/library/content/releasenotes/ObjectiveC/ObjCAvailabilityIndex/index.html#//apple_ref/doc/uid/TP40012243)(生肉)。 72 | 73 | Objective-C apps 用引用计数的方法来决定一个对象的声明周期。大多数情况下,编译器的自定引用计数(ARC)功能会帮你管理好这些。如果你觉得在某些情况下用ARC并不好,或者需要转换或者维护关系到对象储存的遗留代码,你应该读[*Advanced Memory Management Programming Guide*](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011i)(生肉)。 74 | 75 | 除了编译器,Objective-C语言用一种运行时系统来支持它的动态性和面向对象性。虽然你不需要经常关心Objective-C是如何运作的,你还是可以直接与这个运行时系统交互,通过学习[*Objective-C Runtime Programming Guide*](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008048)和[*Objective-C Runtime Reference*](https://developer.apple.com/reference/objectivec/1657527-objective_c_runtime)(生肉)。 76 | -------------------------------------------------------------------------------- /10.命名规则Conventions.md: -------------------------------------------------------------------------------- 1 | #命名规则 2 | 当我们在使用一些框架类(framework classes)的时候,我们会发现 *Objective-C* 的代码可读性很高。类和方法的名字跟 *C* 语言中一般的函数代码或者 *C* 语言中的标准程序库比起来更加简单易懂。当我们用多个单词去命名时,我们会使用 *骆驼命名法*。我们应该使用和 *Cocoa and Cocoa Touch* 相同的命名规则去命名我们自己定义的类,这样会使我们代码的可读性更强,以方便其它需要阅读或者维护我们代码的程序员更好地进行操作。 3 | 4 | ##命名的唯一性 5 | 每一次我们创建一个新的类型,符号,或者标识符的时候,首先要考虑的就是我们的这个命名在它所作用的范围内是唯一的。有时,这个范围可能是整个应用,包括它的链接库;而有时这个范围仅仅只是一个类或者一小段代码。 6 | 7 | ###在整个应用中类的名字必须唯一 8 | 在 *Objective-C* 中,类的名字不仅仅要求在我们所写的整个工程的代码中是唯一的,还要求在可能包括在内的库或者代码包(frameworks or bundles)中也具有唯一性。举个例子,我们应该避免使用通用类的名字,比如`ViewController`或者`TextParser`。因为这很有可能是包括在我们的工程中的一个库函数的名字,这会导致创建类失败。 9 | 10 | 为了保证类名的唯一性,我们的命名规则要求:在所有类的命名上都使用前缀。我们应该已经注意到,在 *Cocoa and Cocoa Touch* 中,类的名字都是以`NS`或者`UI`开头的。苹果公司规定了一系列两个字母的前缀用于命名框架类。当我们学习了更多有关 *Cocoa and Cocoa Touch* 的知识时,会了解到更多其它的前缀: 11 | 12 | | Prefix | Frameworks | 13 | | --------| :----------------------------------------------------| 14 | | NS | Foundation (OS X and iOS) and Application Kit (OS X) | 15 | | UI | UIKit (iOS) | 16 | | AB | Address Book | 17 | | CA | Core Animation | 18 | | CI | Core Image | 19 | 20 | 21 | 22 | 而我们自己定义的类应该使用三个字母的前缀,这个前缀应该和你的公司名称,你的应用名称甚至于你的应用的某个特性相关。举个例子,如果你的公司名字是Whispering Oak,而你正在开发一款名叫Zebra Surprise的游戏,那么你应该选择`WZS`或者`WOZ`作为类名的前缀。 23 | 同时,我们应该使用一个名词来命名一个类,这样会使这个类的意义更加清晰,比如这个来自 *Cocoa and Cocoa Touch* 的例子: 24 | 25 | | | | | | 26 | | ---------|:-----------:|:------------------:|:---------------------:| 27 | | NSWindow | CAAnimation | NSWindowController | NSManagedObjectContext| 28 | 29 | 如果在类的命名中需要使用多个单词,则应该使用 *骆驼命名法* ,即将每一个独立单词的首字母变为大写。 30 | 31 | 32 | ###方法名必须保证唯一性和准确性 33 | 当我们为一个类确定了名称后,在类中声明的方法的名字也必须保证唯一性。在不同的类中,使用同一个名字命名相同功能的某个方法是可行的。举个例子,当我们复写一个父类的方法或者利用多态性时,在不同类中体现同一功能的方法应该使用相同的名字,相同的返回类型以及相同的参数类型。 34 | 35 | 方法名无需使用前缀,并且以小写字母开头。当然,使用多个单词命名一个方法时也要使用 *驼峰命名法* 36 | 举例如下(这些方法都来自类`NSString`): 37 | 38 | | | | | 39 | | -------|:-----------------:|:----------------------------: 40 | | length | characterAtIndex: | lengthOfBytesUsingEncoding: 41 | 42 | 如果方法带有一个或者多个参数,那么在方法名中需要表示出来,例如: 43 | 44 | | | | | 45 | | -------------------|:------------------------------------:|:----------: 46 | | substringFromIndex:| writeToURL:atomically:encoding:error:|enumerateSubstringsInRange:options:usingBlock:| 47 | 48 | 方法名的第一部分应该包括该方法的主要功能或者调用这个方法的结果。如果一个方法返回一个值,举个例子,方法名的第一个单词应该表明返回的值是什么东西,比如`length,character`以及 49 | `substring...`等前文提及过的方法。当我们需要表明返回值的一些重要特征时,我们可以使用多个词语来命名该方法,比如来自`NSString`类的方法`mutableCopy`,`capitalizedString`以及 50 | `lastPathComponent`。如果某个方法是用来实现一个类似写入磁盘或者枚举的功能的,命名的第一个单词应该直接表明这个动作,比如`write`以及`enumerate`。 51 | 52 | 如果一个方法包含了一个这样的指针:*error* 指针参量,这应该是这个方法的最后一个参量。如果一个方法带了一个 *块* ,那么这个块参数因该被设置成最后一个参数,这样当我们在指定一个块内联时,能够使方法的可读性很强。出于同样的原因,我们最好能避免带有多个块参数的方法。 53 | 54 | 清楚且简明地命名一个方法也很重要。方法名过于细致可能会导致名称太冗长,而太简短可能会使方法名表意不清,因此我们应该两者兼顾,例如: 55 | 56 | |stringAfterFindingAndReplacingAllOccurrencesOfThisString:withThisString:|Too verbose| 57 | |------------------------------------------------------------------------|-------------| 58 | |strReplacingStr:str: |Too concise| 59 | |stringByReplacingOccurrencesOfString:withString: |Just right| 60 | 61 | 在命名一个方法时应该避免使用缩略词,除非我们能肯定在大多数的语言和文化中这个缩略词都是很容易理解的。一些常用的缩略词可以查看:[Acceptable Abbreviations and Acronyms]() 62 | 63 | ####当使用category来创建方法时 方法名需要加上前缀 64 | 65 | 当我们使用category来向一个已存在的框架类中添加方法时,方法名中应该加上前缀,这样可以避免混淆。在[ Avoid Category Method Name Clashes]()中有详细介绍。 66 | 67 | 68 | ###在同一作用域中请保证局部变量的唯一性 69 | 70 | 因为 *Objective-C* 是 *C* 语言的超集,*C* 语言中变量的作用域规则在 *Objective-C* 中同样适用。一个局部变量的名称在它的作用域中必须是独一无二的。 71 | 72 | ``` 73 | - (void)someMethod { 74 | int interestingNumber = 42; 75 | ... 76 | int interestingNumber = 44; // not allowed 77 | } 78 | ``` 79 | *C* 语言中确实允许我们在同一作用域中命名两个相同名字的变量,例如: 80 | 81 | ``` 82 | - (void)someMethod { 83 | int interestingNumber = 42; 84 | ... 85 | for (NSNumber *eachNumber in array) { 86 | int interestingNumber = [eachNumber intValue]; // not advisable 87 | ... 88 | } 89 | } 90 | ``` 91 | 不过,这使得我们的代码变得不具可读性。因此,我们应当尽量避免这种情况的出现。 92 | 93 | ##方法的命名必须遵从命名规则 94 | 除了要考虑方法名的唯一性,严格遵从命名规则也是非常重要的。这些命名规则被 *Objective-C* 的编译器和运行时库所使用,同样也被 *Cocoa and Cocoa Touch* 中的类使用。 95 | 96 | ###存取方法的名称必须遵从命名规则 97 | 98 | 当我们使用`@property`语法来为对象声明属性时(详情参见:[Encapsulating Data]()),编译器会自动合成getter和setter方法(除非我们另作声明)。如果要定义我们自己的存取方法时,确保方法名的正确性才能让我们顺利地使用点语法。 99 | 100 | 除非特别声明,否则一个getter方法的名字应该和它对应的属性的名字相同。对于一个名字是`firstName`的属性来说,存取方法的名字也应该是`firstName`。对布尔值属性来说,getter方法应该以`is`开始。例如`paused`属性,那么它对应的getter方法的名字应该是`isPaused` 101 | 102 | 对于一个属性来说,要定义它的setter方法,我们应该使用格式:`setPropertyName:`,对于一个名叫`firstName`的属性来说,setter的方法的名字应该是`setFirstName:`,对于一个名叫`paused`的布尔值属性来说,它的setter方法名应该是`setPaused:` 103 | 104 | 即使`@property`允许我们制定不同的存取方法名,但也只能在有布尔值属性的情况时使用。其他时候我们必须遵从命名规则,否则一些很重要的技术,比如KVC技术(使用`valueForKey:`和`setValue:forKey:`来存取一个属性的技术)就不能使用了。关于KVC更多的信息,请参考[Key-Value Coding Programming Guide]() 105 | 106 | ###对象创建的方法名必须遵从命名规则 107 | 108 | 我们在之前的章节中已经了解到,为一个类创建实例有很多种方法。我们可以使用分配内存和初始化相结合的方法来创建实例,比如: 109 | 110 | ``` 111 | NSMutableArray *array = [[NSMutableArray alloc] init]; 112 | ``` 113 | 114 | 也可以使用`new`这种便利的方法,直接调用`alloc`和`init`这两种方法,例如: 115 | 116 | ``` 117 | NSMutableArray *array = [NSMutableArray new]; 118 | ``` 119 | 120 | 一些类甚至可以使用工厂方法模式来创建实例: 121 | 122 | ``` 123 | NSMutableArray *array = [NSMutableArray array]; 124 | ``` 125 | 126 | 除了在工厂模式的类中已经存在的子类,类工厂方法往往使用类的名字来作为自己名字的开头(没有前缀)。就`NSArray`这个类来说,举个例子,工厂方法的名字都会以`array`开头。而`NSMutableArray`这个类不规定任何特定于类的工厂方法,所以一个可变数组的工厂方法名仍然以`array`开头。 127 | 128 | *Objective-C* 中有各种各样的内存管理机制,用于确保对象的生命周期足够长。我们不需要过多的担心这些规则,编译器会基于所创建的这些方法的名称来判断它需要遵守哪一条规则。由于自动释放池(autorelease pool blocks)的使用,通过方法工厂创建的对象的管理和通过传统方式(包括分配内存和初始化,`new`方法)创建的对象的管理有些细微地不同。想了解更多关于自动释放池以及内存管理机制的信息,请参考[ Advanced Memory Management Programming Guide]() 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /2.定义类Defining-Classes.md: -------------------------------------------------------------------------------- 1 | #定义类 2 | 当你为OS X或iOS系统编写程序时,你会把大多数时间花在 *对象(object)* 上。在Objective-C的对象和*面向对象语言中(object-oriented programming languages)* 的对象一样:它们封装数据及其相关的方法。 3 | 4 | 一个APP是一个巨大的生态系统,它将内部对象互相联系起来,解决特定的问题,例如展示一个界面,响应用户的操作,或者存储信息等等。在OS X和iOS应用的开发中,你不必为了解决各种问题花很多心思创造各种对象,而可以使用一个由OS X的`Cocoa`和iOS的`Cocoa Touch`提供的巨大的、已经存在的对象库。 5 | 6 | 在这个对象库中,有些对象是可以直接用的,例如一些基本数据类型:字符串和数字,或者用户界面的基本元素:*按钮(button)* 和 *表视图(table view)* 。另一些对象则是专门给你用自己的代码定制的,然后这些对象会按照你的规定执行工作。在APP的开发过程实际上就是在已提供的基本框架下自定义你的对象并且把各种对象组合起来,以赋予你的APP与众不同的特征和功能。 7 | 8 | 在面向对象编程中,一个对象是 *类(class)* 的一个 *实例(instance)* 。在这一章节中,我们会展示如何在Objective-C中通过声明 *接口interface* 的方式定义一个类。类的接口(interface)描述了你需要的类,及该类中你需要使用的成员变量和方法。接口包含这个类可以接受的信息列表,因此你必须提供这个类的 *实现(implementation)* 。实现包括一些可执行的代码,这些代码会在收到相应信息时执行。 9 | 10 | ##类是对象的蓝图(Blueprints) 11 | 类描述了这个类所有实例对象的共同行为和属性。比如一个字符串对象(在Objective-C中,这是一个 `NSString` 类的实例),`NSString`类提供了各种方法来处理和转换字符。相似地,用来描述数字对象的类——`NSNumber` ,则提供各种关于数字类型的方法,例如把数值转换成各种数据类型。 12 | 13 | 就像很多用相同的蓝图建设的建筑一样,这些建筑有着相同的结构,类似的,类的每一个对象都拥有相同的属性和方法。无论字符串对象所装载的字符是什么,这些`NSSrting`类的对象都拥有相同的方法。 14 | 15 | 每一个特定的对象用于解决特定的问题。你只需要知道一个字符串对象可以表现一些字符的集合,而不必知道这些字符串在内部是怎样被储存起来的。你不需要知道任何关于这个对象内部是怎么操作字符的,但你要知道怎样操作这些字符串对象,例如获取字符串中某个特定的字符,或者把字符串中所有的字符转换成大写。 16 | 17 | 在Objective-C中,类的声明部分(Class interface)具体声明了它的对象有哪些方法供外部使用。换句话来说,声明部分定义了外部环境和类的实例对象之间的接口。 18 | 19 | ###对象的可变性 20 | 一些类的对象是不可变的。这意味着对象的内部属性在创建时必须被初始化,而且以后再也不能被改变。在Objective-C中,所有基本的`NSString`和`NSNumber`类的对象都是不可变的。如果你要表示一个不同的数字,你必须创建一个新的`NSNumber`类的实例对象。 21 | 22 | 一些不可变的类有与之类似的可变的类。如果你在程序运行的时候想要改变字符串的内容,例如在它后面再加上一些从网络上获取的字符,你可以使用 `NSMutableString` 类的对象。除了提供一些可以改变字符的方法外,这个类的对象和`NSString`类的对象没有什么不同。 23 | 24 | 尽管`NSString`类和`NSMutableString`类是不同的类,但是它们有很多相似的地方。相对于构造两个完全独立的类,使用 *继承(inheritance)*特性是更好的选择。 25 | 26 | ###类的继承 27 | 在自然世界中,分类学把各种动物分类到不同的种、属、科中。这些分类都是分等级的,就好像很多种属于同一个属,很多属又属于同一个科。 28 | 29 | 举个例子,大猩猩、人和猩猩,有很多明显相同的地方。尽管它们属于不同的种,甚至不同的属、群、亚科,但它们在生物学上都是有关联的,他们都属于同一个科“人科”。图1-1表示了它们的关系。 30 | ![](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/humansgorillas.png 31 | ) 32 | 在面向对象编程中,对象被分类成有等级的集合。对象以 *类* 为单位编成小组,而不是像生物学一样用直接的专业术语分类。就像人类从父母身上继承了一些属性一样,类也可以从它的 *父类(superclass)* 中继承属性。 33 | 34 | 当一个类继承另一个类时, *子类(subclass)* 能继承父类定义的所有的属性和方法。子类也可以定义自己的属性和方法,也可以 *重载(override)* 父类的方法。 35 | 36 | 在Objective-C的字符串类中,`NSMutableString`的声明部分定义了`NSMutableString`这个类继承自`NSString`类。图1-2说明表示了这个关系。`NSString`所有的属性和方法都能在`NSMutableString`中使用,但`NSMutableString`增加了一些方法,使你可以加长、插入、替换或删除字符串的单个字符或者 *子串(substrings)* 。 37 | 38 | ![](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/nsstringmutablestring.png) 39 | 图1-2 40 | 41 | ###提供最基本的功能的基类 42 | 就像所有活的有机体有着相同的"活"的属性一样,所有Objective-C的对象有着一些相同的最基本的功能。 43 | 44 | 当一个Objective-C对象需要和另一个类的对象共同工作时,它们就需要提供一些基本的属性和方法。为此,Objective-C定义了一个所有类都继承自它的类————基类`NSObject`。当一个对象遇到另一个对象时,它们可以用来自`NSObject`声明的方法进行交互。 45 | 46 | 当你定义一个Objective-C类时,这个类最起码要继承自`NSObject`。而且,这个类要继承和它需要的功能最接近的Cocoa 或 Cocoa Touch的类。 47 | 48 | 例如,你想给你的iOS app自定义一个按钮,但是Objective-C中提供的`UIButton`类并不能满足你的需求,你就可以创造一个继承自`UIButton`的新的类,而不是继承自`NSObject`。如果这个新的类是继承自`NSObject`的,你就要重复写那些`UIButton`已经提供了的功能。如果这个新的类是继承自`UIButton`的,那么这个类就拥有了`UIButton`的所有属性,只需要加上自己需要的功能就可以了。 49 | 50 | `UIButton`这个类本身是继承自`UIControl`的,`UIControl`描述了iOS系统中所有相同的用户界面操作。`UIControl`又是继承自`UIView`的,它定义了所有在屏幕上展示的对象的共有的属性和方法。`UIView`又继承自`UIResponder`,`UIResponder`能够响应用户的操作,例如点(tap),手势(gesture)和摇晃(shake)。最后,在继承树的最底端,`UIResponder`继承自`NSObject`类。图1-3表示这些继承关系。 51 | ![](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/buttoninheritance.png) 52 | 图1-3 53 | 54 | 这一连串的继承意味着一个新建的自定义的`UIButton`的子类不但你能继承`UIButton`声明的属性和方法,还能继承它每一个父类的属性和方法。这个新类的对象有着按钮的功能,可以在屏幕上展示,响应用户的操作,还能与所有Cocoa Touch的对象交互。 55 | 56 | 在使用一个类时,把它继承的关系链记在心中是非常重要的,这样做可以使我们明白这些类都能做些什么。在Cocoa 和 Cocoa Touch的类的参考文件中,我们可以很快地找到一个类和它的父类们。如果你在一个类的声明部分或者参考文件中找不到它的一个属性或者方法,或许你可以在它的父类中找到这个属性和方法。 57 | 58 | ##类的声明部分定义了它和外界的交互 59 | 面向对象编程有很多好处,其中一个就是当你使用一个类时你只需要知道怎样和这个类的实例交互就可以了。准确来说,一个对象的内部实现方法是应该被隐藏起来的。 60 | 61 | 例如,如果你在你的iOS app中使用`UIButton`,你不用担心按钮的像素是怎样显示的,你只需要知道怎么去改变这个按钮的一些属性,例如按钮的标题和颜色,当你把这个按钮加到屏幕上时,它就会准确地按照你的要求显示出来。 62 | 63 | 当你定义你自己的类时,你需要从定义这个类的公有属性和方法(Public)。哪些属性是可以被公开访问的?这些属性能被修改吗?其他对象怎么和你的对象交互? 64 | 65 | 这些信息需要在你的类的声明部分中说明,它一般用来定义这个类的公有方法。公有方法和私有方法是分开定义的。在Objective-C中,类的声明部分和实现部分一般是放在不同的文件中的(.h文件和.m文件),因此你只需要把 *类的声明部分(interface)* 公有就行了。 66 | 67 | ###基本语法 68 | 在Objective-C语法中,类是这样定义的: 69 | 70 | ``` 71 | @interface SimpleClass : NSObject 72 | 73 | @end 74 | ``` 75 | 76 | 这个例子定义了一个继承自`NSObject`的名叫`SimpleClass`的类。 77 | 78 | 公有的属性和方法需要被定义在`@interface`和`@end`中间这个声明部分里面。在这个例子中,没有定义和父类不一样的属性和方法,所以`SimpleClass`的对象所拥有的功能和`NSObject`的一模一样。 79 | 80 | ###属性(property) 81 | 对象通常都会有属性来让外界访问。举个例子,如果你要需要在你的app内表示一个人,你需要字符串来表示这个人的名字。 82 | 83 | 这些属性的声明需要写在**@interface**里面。 84 | 85 | ``` 86 | @interface Person : NSObject 87 | 88 | @property NSString *firstName; 89 | @property NSString *lastName; 90 | 91 | @end 92 | ``` 93 | 94 | 在这个例子里面,继承自`NSObject`的类`Person`拥有两个公有属性,而且两个都是`NSString`类的对象。 95 | 96 | 这些属性都是Objective-C的对象,所以它们都用一个`*`号(和C语言的指针一样)来指向它的地址。就像C语言的声明语句一样,这些声明语句都需要分号`;`结尾。 97 | 98 | 你可能需要增加一个属性来表示这个人的生日年份,所以你可以使用一个数字类型的对象。 99 | 100 | ``` 101 | @property NSNumber *yearOfBirth; 102 | ``` 103 | 104 | 但对于一个简单的年份数字来说,用`NSNumber`类的对象来表示可能有点过了。我们还有一种方法,就是用C语言中的基本数据类型来存储这些简单的数据,例如整型: 105 | 106 | ``` 107 | @property int yearOfBirth; 108 | ``` 109 | 110 | ####属性的可读性和存储类型 111 | 目前为止,我们的例子展示了如何声明一个完全公有的属性。这意味着其他对象可以读和写这些属性的值。 112 | 113 | 在一些情况中,你需要声明一些不能被改变的属性。在现实世界中,一个人要改变他的名字是非常困难的。那么在我们的app中,我们可以把名字这个属性的可访问性设置为**read-only**,这样外部就无法直接改变这个属性的值,这个操作需要一个 *中间对象(intermediary object)* 间接进行。 114 | 115 | Objective-C中可以声明 *属性的特性property attributes*,用来指示这个属性可读性和其他特性。在我们的app中,我们可以把`Person`类的interface部分修改成这样: 116 | 117 | ``` 118 | @interface Person : NSObject 119 | @property (readonly) NSString *firstName; 120 | @property (readonly) NSString *lastName; 121 | @end 122 | ``` 123 | 124 | 属性的属性需要写在**@property**关键字的后面,用括号包起来。关于这部分的更多内容请参考**Declare Public Properties for Exposed Data**这一节。 125 | 126 | 127 | ###方法的声明 128 | 目前为止,我们的例子描述了一个典型的对象,或者换句话说,一个简单的用来封装数据的对象。在`Person`类中,不需要方法来操作它的属性。但是大多数的类,除了声明它的属性,还需要声明它的方法。 129 | 130 | 用Objective-C写的程序都是建立在巨大的各种对象的联系网之上的,因此对象通过发送信息来和其他对象交互是非常重要的。在Objective-C中,一个对象可以通过调用另一个对象的方法来传递信息。 131 | 132 | Objective-C的方法(函数)和C语言以及其他语言的方法(函数)在概念上是相似的,但是定义的语法有点不同。在C语言中,方法的定义是这样的: 133 | 134 | ``` 135 | void SomeFunction(); 136 | ``` 137 | 138 | 同样的方法在Objective-C中是这样定义的: 139 | 140 | ``` 141 | -(void)someMethod; 142 | ``` 143 | 144 | 这个方法没有参数。C语言的关键字`void`在Objective-C中被放在了括号里面,而且放在方法声明的开始部分,用来表示这个方法在结束时不返回值。 145 | 146 | 减号(`-`)用在方法声明的最开头位置,用来表示这是一个 *实例方法(instance method)*,只能被类的实例对象所调用。你也可以声明一个 *类方法(factory method)*,可以被类本身调用,详情请参考**Objective-C Classes Are also Objects**这一节。 147 | 148 | 和C语言一样,Objective-C的方法声明也需要一个分号`;`结尾。 149 | 150 | ####带参数的方法 151 | 如果你需要声明一个带一个或者多个参数的方法,Objective-C的语法和C语言的语法有着很大的区别。 152 | 153 | 在C语言中,参数在括号内具体说明,例如: 154 | 155 | ``` 156 | void SomeFunction(SomeType value); 157 | ``` 158 | 159 | 在Objective-C中,只有参数类型用括号包起来,整个参数部分放在一个冒号`:`后面,例如: 160 | 161 | ``` 162 | -(void)someMethodWithValue:(SomeType)value; 163 | ``` 164 | 165 | 在这个例子中,`SomeType`是参数的类型,`value`是参数的名字。 166 | 167 | 如果你需要传入多个参数,语法和C语言就更加不一样了。C语言的多个参数都放在一个括号里面,用括号隔开;而在Objective-C中,带两个参数的方法的声明是这样的: 168 | 169 | ``` 170 | - (void)someMethodWithFirstValue:(SomeType)value1 secondValue:(AnotherType)value2; 171 | ``` 172 | 173 | 在这个例子中,`value1`和`value2`是方法在实现时的内部变量名,它们和你调用这个方法时传入的参数一一对应。 174 | 175 | 在一些编程语言中,允许在方法的定义中加入 *命名参数(named arguements)* ;但是在Objective-C中这是不允许的。调用方法时参数的顺序必须和方法在声明的时候参数的顺序一致,事实上`secondValue:`是方法名字的一部分: 176 | 177 | ``` 178 | someMethodWithFirstValue:secondValue: 179 | ``` 180 | 181 | 这种写法使得Objective-C成为一门可读性很强的语言,因为方法的名字中会指出下一个传入的参数的名字,详情请参考**You Can Pass Objects for Method Parameters**这一节。 182 | 183 | **注意**:`value1`和`value2`这两个变量名并不是严格要求出现在方法名里的,也不要求方法名里出现的指示和参数名完全一致,只要能起到一个标志指示的作用就行了。但是在写方法的实现的时候,要求方法名、参数类型、返回类型和声明时完全一致。 184 | 185 | 下面的方法和之前的方法是一样的: 186 | 187 | ``` 188 | - (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(AnotherType)info2; 189 | ``` 190 | 191 | 下面这两个方法和上面的方法是不一样的: 192 | 193 | ``` 194 | - (void)someMethodWithFirstValue:(SomeType)info1 anotherValue:(AnotherType)info2; 195 | ``` 196 | 197 | ``` 198 | - (void)someMethodWithFirstValue:(SomeType)info1 secondValue:(YetAnotherType)info2; 199 | ``` 200 | 201 | ###类的名字必须是唯一的 202 | 在一个app中,就算在不同的 *类库(library)* 和 *框架(framework)* 中,类的名字都必须是唯一的。如果你打算用工程中已存在的类的名字创造一个新的类,你会收到一个编译错误。因此,你最好在你这创造的类前用三到四个字母加上你的前缀,是你的类看起来是唯一的。这些字母可以和你的app有关,也可以和你的框架有关,还可以是你名字的首字母。接下来我们的例子都会在类名中使用前缀,就像这样: 203 | 204 | ``` 205 | @interface XYZPerson : NSObject 206 | @property (readonly) NSString *firstName; 207 | @property (readonly) NSString *lastName; 208 | @end 209 | ``` 210 | 211 | **历史小知识:**如果你想知道为什么你遇到的类中为什么很多都有`NS`的前缀,那就要从Cocoa 和 Cocoa Touch的历史说起了。Cocoa是NeXT公司(乔布斯在第一次离开苹果公司后创立的公司)为了在NeXTStep操作系统上写程序而开发的框架。乔布斯回归苹果后,苹果公司在1996年收购了NeXT公司,并且把大部分NeXTStep操作系统合并到了OS X操作系统,其中就包括了很多已完成的类。而Cocoa Touch则是作为Cocoa的替代物被引入到iOS操作系统的;尽管两个框架间有很多类都是不一样的,但仍有一些类在Cocoa 和 Cocoa Touch都能使用。像`NS`和`UI`(代表iOS的用户界面User Interface)这种两个字母的前缀就被苹果保留了起来。 212 | 213 | 相比较之下,方法名和属性名只需要在类里面保持唯一就可以了。尽管在C语言中函数名必须在一个工程中唯一,但是在Objective-C中我们鼓励在不同的类中使用相同的方法名。在同一个类的声明中,一个方法我们只能声明一次,但是如果你想重载一个继承而来的方法的话,你必须使用和父类中与该方法完全一致的方法名。 214 | 215 | 像方法一样,一个对象的属性和 *实参(instace variable)*只需要在同一个类中保持唯一就可以了(详情请见**Most Properties Are Backed by Instance Variables**)这一节。但是,如果你想使用全局变量,这些变量名就必须在app或者工程中是唯一的。 216 | 217 | 更多关于命名传统和建议请参考**Conventions**这一节。 218 | 219 | ##方法的实现 220 | 当你写好了类的公有属性和方法之后,你就要开始写这个类的实现代码了。 221 | 222 | 一个类的声明部分通常放在一个叫 *头文件(header file)* 的文件中,这个文件一般以`.h`作为扩展名。而你需要在以`.m`为拓展名的文件里为你的类写实现的代码。 223 | 224 | 当类的声明代码写在头文件时,你需要在编译实现的代码前告诉编译器去读取这个类的声明代码。这就要使用Objective-C提供的预处理指令`#import`了。这和C语言的`#include`指令很像,但你要确保一个文件在编译时只能被include一次。 225 | 226 | 要注意预处理指令和传统的C语句不一样,是不需要在结尾加分号的。 227 | 228 | ###基本语法 229 | 实现一个类的基本语法是这样写的: 230 | 231 | ``` 232 | #import "XYZPerson.h" 233 | 234 | @implementation XYZPerson 235 | 236 | @end 237 | ``` 238 | 239 | 如果在.h声明部分声明了一些方法,你需要在这个文件里面实现它们。 240 | 241 | ###实现一个方法 242 | 这是一个类有一个方法的类的声明部分: 243 | 244 | ``` 245 | @interface XYZPerson : NSObject 246 | - (void)sayHello; 247 | @end 248 | ``` 249 | 250 | 那么这个类的实现就应该长这样: 251 | 252 | ``` 253 | #import "XYZPerson.h" 254 | 255 | @implementation XYZPerson 256 | 257 | - (void)sayHello { 258 | NSLog(@"Hello, World!"); 259 | } 260 | 261 | @end 262 | ``` 263 | 264 | 这个例子使用了`NSLog()`这个方法来在控制台打印一些信息。这个方法和C语言中的`printf()`函数是一样的。`NSLog()`方法可以传入一些参数,但第一个参数必须是Objective-C的字符串。 265 | 266 | 就像C语言的函数一样,Objective-C的方法用花括号`{}`把方法的实现代码包起来。而且方法的名字、参数类型和返回类型都必须和声明时一模一样。 267 | 268 | Objective-C和C语言一样,对方法名是敏感的,所以像这个方法: 269 | 270 | ``` 271 | - (void)sayhello { 272 | 273 | } 274 | ``` 275 | 276 | 和方法 277 | 278 | ``` 279 | -(void)sayHello{ 280 | 281 | } 282 | ``` 283 | 284 | 会被编译器当作不一样的方法来处理。 285 | 286 | 总的来说,方法名的第一个字母应该是小写的。Objective-C和C语言的命名传统不一样,Objective-C会使用更多描述性的语言来为这个方法命名。如果一个方法名中有很多个单词,就应该使用 *驼峰命名法(camel case)* 来增强这个方法名的可读性。 287 | 288 | 289 | 要注意空格在Objective-C里是非常灵活的。我们要经常在代码块中用tab键或者space键来进行缩进,而且你会经常看到一个左花括号独占一行的写法: 290 | 291 | ``` 292 | - (void)sayHello 293 | { 294 | NSLog(@"Hello, World!"); 295 | } 296 | ``` 297 | 298 | 苹果公司的官方开发工具(IDE)Xcode会根据开发者的偏好设置来自动缩进你的代码。如果你想知道怎么改变自动缩进和设置tab键的宽度,请参考**Xcode Workspace Guide**。 299 | 300 | 在下一章**Working with Objects**中你会看到更多方法实现的例子。 301 | 302 | ##Objective-C的类也是一种对象 303 | 在Objective-C中,一个类本身就是这个类的一个对象,这种对象类型叫做`class`。类本身不能像之前那样声明属性,但是它们却可以接收信息。 304 | 305 | 当一个类要调用方法时,它只能调用这个类的类方法(factory method)。这里典型的用法就是为对象 *分配内存(allocation)* 和 *初始化(initialization)*,详情请参考**Objects Are Created Dynamically**。举个例子,在`NSString`中有几个类方法可以创建一个空的字符串对象,或者有几个字符的字符串对象。这些方法有: 306 | 307 | ``` 308 | + (id)string; 309 | + (id)stringWithString:(NSString *)aString; 310 | + (id)stringWithFormat:(NSString *)format, …; 311 | + (id)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error; 312 | + (id)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc; 313 | ``` 314 | 315 | 从上面的例子中我们可以发现,类方法是用一个加号`+`声明的,这和实例对象的方法用减号`-`来声明不一样。 316 | 317 | ##练习 318 | **注意:**为了完成下面的练习,你将要在Xcode中创建一个新的工程,这样可以确保你写的代码不会出现编译错误。 创建Xcode新工程时在OS X Application模版中选择创建*Command Line Tool*。 319 | 320 | 1.用Xcode新建一个Objective-C语言的类的interface和implementation文件,类名为`XYZPerson`,继承自`NSObject`。 321 | 2.在这个类的声明部分加入属性,来表示这个人的名(first name)和姓(last name),还有出生的日期(用NSDate类型)。 322 | 3.声明一个叫`sayHello`的方法并且实现它。 323 | 4.声明一个叫`person`的类方法。不用实现这个方法。 324 | **注意:**如果你编译你的代码,你会得到一个Incomplete implementation的警告,那是因为你没有实现这个类方法。不用理会它。 325 | 326 | 327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /3.对象的使用Working-with-Objects.md: -------------------------------------------------------------------------------- 1 | #对象的使用: 2 | 在 *Objective-C* 的应用中,多数工作是通过消息的送回和直接传达到对象群的形式实现的。对象群中的部分对象是由 *Cocoa(Cocoa Touch)* 所创建的类的实例,而另一部分则是由我们自己创建的类生成的实例。 3 | 前一章描述了类的实现方法以及在类中定义接口的语法,包括用于响应消息的方法的语法规则。这一章我们将学习怎样向对象发送这样一个消息,包括 *Objective-C* 的一些动态特性(这些特性包括:动态类型、决定在运行时中哪一种方法应该被调用的能力)。 4 | 在一个对象被使用之前,它的创建必须满足:具有分配给属性的内存空间、内部数据进行一切必要的初始化这两点。这一章节描述了:为了确保一个对象被正确配置,我们如何将这个分配内存和初始化一个对象的方法嵌套其中的语法规则。 5 | ##Objects接受、发送消息 6 | 即使 *Objective-C* 中有多种在对象之间发送消息的方法,但目前最主流的方法仍然是运用方括号,例如: 7 | 8 | ``` 9 | [someObject doSomething]; 10 | ``` 11 | 12 | 左边的`someObject`在这个例子中,是作为消息的接收者。右边的`doSomething`,是消息的名称。换句话说,当上面的这行代码被执行,`someObject`将会收到 `doSomething` 这条消息。 13 | 14 | 前一章像我们介绍了如何为一个类创建接口,例如: 15 | 16 | ``` 17 | @interface XYZPerson : NSObject 18 | - (void)sayHello; 19 | @end 20 | ``` 21 | 22 | 23 | 以及如何实现一个类,例如: 24 | 25 | ``` 26 | @implementation XYZPerson 27 | - (void)sayHello { 28 | NSLog(@"Hello, world!"); 29 | } 30 | @end 31 | ``` 32 | 33 | >注意:这个例子使用了一个 *Objective-C* 的字符串,字符串是 *Objective-C* 中一种可以直接使用速记语法创建的类型。我们需要明白,`@"Hello, world!"`在概念上等同于说“一个 *Objective-C* 字符串型对象代表了 *Hello,World!* 这句话。” 34 | 35 | >字符和对象的创建在本章的下一小节[Objects Are Created Dynamically]()中会有更详细地介绍。 36 | 37 | 假设你已经创建了`XYZPerson`这个对象,你可以像这样向它发送消息`Say Hello`,例如: 38 | 39 | ``` 40 | [somePerson sayHello]; 41 | ``` 42 | 43 | 在 *Objective-C* 中发送一条消息在概念上和在 *C* 语言中调用一个函数是一样的。图片2-1展示了发送消息`sayHello`的过程。 44 | 45 | **图片2-1** 消息发送的基本方法的程序流程 46 | 47 | ![programflow1](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/programflow1.png) 48 | 49 | 为了明确消息的接受者,必不可少的一点是:我们需要理解在 *Objective-C* 中指针指向对象的工作原理。 50 | 51 | ###使用指针跟踪对象 52 | 就像其他编程语言那样,*C* 语言和 *Objective-C* 中使用变量记录值。 53 | 这里记录了一些在标准 *C* 中最基本的数值变量类型,包括 *整型* 变量,*浮点型* 变量,*字符型* 变量。声明并赋初始值的方法如下: 54 | 55 | ``` 56 | int someInteger = 42; 57 | float someFloatingPointNumber = 3.14f; 58 | ``` 59 | 60 | 局部变量,在方法或函数中声明的变量,举例如下: 61 | 62 | ``` 63 | - (void)myMethod { 64 | int someInteger = 42; 65 | } 66 | ``` 67 | 68 | 那么它(上例中的变量)的作用范围是受限制的。 69 | 70 | 在这个例子中,`someInteger`被声明成一个在`myMethod`中的局部变量;一旦程序执行到了这个方法的大括弧,即{ }中的代码,`someInteger`将不能使用(?)。 71 | 当一个局部数值变量(例如`int`或者`float`)被释放,它的数值也会随之消失。 72 | 73 | *Objective-C* 中的对象,与其它编程语言相比,在初始化时有一些细微的不同。一个对象往往不会只仅仅调用一个方法后就不再使用。尤其是,对象常常需要存在比它的原始变量更长的时间,因此,我们常常创建的是一个动态的对象(在分配内存和初始化时都是动态的)。 74 | 75 | >注意:如果你十分了解 *堆* 和 *栈* 的话,我们可以这样解释:局部变量定义在 *栈* 上,而对象定义在 *堆* 中。 76 | 77 | 这就要求你使用一个 *C* 语言中的指针(一个用来记录变量在内存中的地址的量)来记录对象在内存中的位置。例如: 78 | 79 | ``` 80 | - (void)myMethod { 81 | NSString *myString = // get a string from somewhere... 82 | [...] 83 | } 84 | ``` 85 | 86 | Although the scope of the pointer variable myString (the asterisk indicates it’s a pointer) is limited to the scope of myMethod, the actual string object that it points to in memory may have a longer life outside that scope. It might already exist, or you might need to pass the object around in additional method calls, for example.(?) 87 | 88 | ###使用对象作为方法的参数 89 | 当你发送消息时需要传递一个参数,而这个参数是对象的话,你应该提供给这个对象一个指针,这个指针指向方法的某个参数。在前一章中,我们提供了创建只有一个参数的方法的代码示例: 90 | 91 | ``` 92 | - (void)someMethodWithValue:(SomeType)value; 93 | ``` 94 | 95 | 举一反三,现在我们能够写出这样的代码:仅使用一个指针型对象作为参数的函数。示例如下: 96 | 97 | ``` 98 | - (void)saySomething:(NSString *)greeting; 99 | ``` 100 | 101 | 接下来我们尝试实现`saySomething`这个函数,示例如下: 102 | 103 | ``` 104 | - (void)saySomething:(NSString *)greeting { 105 | NSLog(@"%@", greeting); 106 | } 107 | ``` 108 | 109 | `greeting`指针在这里可以当作一个局部变量,并且它(`greeting`指针)只在`saySomething`的作用域中。当这个函数(`saySomething`)被调用时,这个指针指向的对象会被优先处理,并且在方法执行完成后这个对象仍然继续工作。(The greeting pointer behaves like a local variable and is limited in scope just to the saySomething: method, even though the actual string object that it points to existed prior to the method being called, and will continue to exist after the method completes.) 110 | 111 | >注意:`NSLog()`使用了格式说明符来表明替换标记(sustitution tokens),就像标准 *C* 中的库函数`printf{}`。这个字符串之所以能连接到控制台,是因为格式化这个格式化字符串(第一个参数)通过插入一个提供给我们的值(其余参数)。 112 | (NSLog() uses format specifiers to indicate substitution tokens, just like the C standard library printf() function. The string logged to the console is the result of modifying the format string (the first argument) by inserting the provided values (the remaining arguments.) 113 | 114 | >在 *Objective-C* 中,我们增加了一个在 *C* 语言中没有的格式转换符,`%@`,它被用于指示一个对象。在程序运行时,调用方法`descriptionWithLocale`(如果这个函数存在)以及方法`description`作用于对象将代替这个格式说明符(`%@`)。方法`description`被类`NSObject`调用,来返回对象的类以及对象的内存地址。但是许多 *Cocoa* 和 *Cocoa Touch* 的类方法会覆写方法`description`(该方法不被执行),来获取更多有用的信息(屏蔽无用的信息)。比如在类`NSString`中,方法`description`只返回它的特征值。 115 | 116 | ###方法可以返回值 117 | 一个方法(函数)不仅能传递参数,同时也能返回一个参数。到目前为止,这一章中我们列举的每一个方法(函数)都有一个`void`类型的返回值。这个 *C* 语言中的关键字`void`表明这个方法(函数)不返回任何值。 118 | 119 | 定义一个`int`类型的返回值表明这个方法(函数)返回一个整型值,例如: 120 | 121 | ``` 122 | - (int)magicNumber; 123 | ``` 124 | 125 | 在这个方法(函数)的实现代码中我们使用了一个 *C* 语言中的保留字`return`,表明当这个方法(函数)被执行后应该传回一个值(`return`后紧跟的那个数值),代码示例如下: 126 | 127 | ``` 128 | - (int)magicNumber { 129 | return 42; 130 | } 131 | 132 | ``` 133 | 134 | 这个例子完美地解释了我们之前一直忽略的一件事:函数可以返回一个值。(It’s perfectly acceptable to ignore the fact that a method returns a value.)在这个例子中,方法` magicNumber`除了返回一个值以外什么也没实现,但是像这样调用这个方法(函数)也没有任何问题: 135 | 136 | ``` 137 | [someObject magicNumber]; 138 | ``` 139 | 140 | 如果你要跟踪这个返回值,你可以定义一个变量,并且将这个函数调用的结果赋值给这个变量,例如: 141 | 142 | ``` 143 | int interestingNumber = [someObject magicNumber]; 144 | ``` 145 | 146 | 同样地,我们也能让函数返回一个对象。举个例子,`NSString`这个类,提供了一个方法`uppercaseString`,代码示例如下: 147 | 148 | ``` 149 | - (NSString *)uppercaseString; 150 | ``` 151 | 152 | 使用相同的方法我们也能让函数返回一个标量值(scalar value),并且需要使用一个指针来跟踪这个结果,示例如下: 153 | 154 | ``` 155 | NSString *testString = @"Hello, world!"; 156 | NSString *revisedString = [testString uppercaseString]; 157 | 158 | ``` 159 | 160 | 当这个方法调用返回时,`revisedString `指针将会指向一个代表字符`HELLO WORLD!.`的字符串对象。 161 | 162 | 记住,当方法返回一个对象时,像这样: 163 | 164 | ``` 165 | - (NSString *)magicString { 166 | NSString *stringToReturn = // create an interesting string... 167 | 168 | return stringToReturn; 169 | } 170 | ``` 171 | 172 | 即使`stringToReturn`指针已经不在作用域中,这个作为返回值的字符型对象依然存在。 173 | 174 | 显然,在这种情况下我们需要思考此过程中的内存管理:一个被返回的对象(在堆中创建)需要一个相当长的寿命以保证它可以被这个方法的调用者使用,但这个对象也不能永久的存在,否则会造成内存泄漏。大多数情况下,*Objective-C* 中的 *自动引用计数(ARC)* 机制会帮我们管理内存,所以我们不必太过担心。 175 | 176 | ###对象可以给自己发送消息 177 | 无论何时我们实现一个方法,我们都需要访问一个非常重要的值`self`。从`self`的字面意思来看,它是指“收到这条消息的对象”。事实上,`self`是一个指针,就像我们之前提到过的`greeting`,并且能够用来调用一个方法。 178 | 179 | 我们可以选择重构`XYZPerson`这个类的实现,将`sayHello`这个方法改为`saySomething`(我们在上文使用过),在新创建的这个方法中调用`NSLog()`,这意味着我们可以添加更多的方法,比如: 180 | `sayGoodbye`。that would each call through to the saySomething: method to handle the actual greeting process. If you later wanted to display each greeting in a text field in the user interface, you’d only need to modify the saySomething: method rather than having to go through and adjust each greeting method individually. 181 | 182 | 使用`self`像对象发送消息的代码示例如下: 183 | 184 | ``` 185 | @implementation XYZPerson 186 | - (void)sayHello { 187 | [self saySomething:@"Hello, world!"]; 188 | } 189 | - (void)saySomething:(NSString *)greeting { 190 | NSLog(@"%@", greeting); 191 | } 192 | @end 193 | ``` 194 | 195 | 如果你向一个`XYZPerson`的对象发送一条`sayHello`消息,流程如图所示: 196 | 197 | ***图片2-2*** 向自己发送消息的程序流程 198 | 199 | ![programflow2](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/programflow2.png) 200 | 201 | ###对象能调用由超类创建的方法 202 | 在 *Objective-C* 中有一个我们非常熟悉的关键字`super`。通过向super发送消息的这种方式来调用一些由继承链之上的父类定义的方法。`super`最常见的用法是用于重写一个方法。 203 | 204 | 让我们假设现在要创建一个新型人类的类,一个“射击者类”,在这个新定义的类中,每一个“greeting”都需要用大写来表示。我们当然可以复制整个`XYZPerson`类,然后修改每个方法中的每一个“greeting”,但是最简单的方法显然是创建一个新的类,而这个新的类继承于`XYZPerson`,此时我们只需要重写 205 | `saySomething`这个方法,即可达到目的。代码示例如下: 206 | 207 | ``` 208 | @interface XYZShoutingPerson : XYZPerson 209 | @end 210 | ``` 211 | 212 | ``` 213 | @implementation XYZShoutingPerson 214 | - (void)saySomething:(NSString *)greeting { 215 | NSString *uppercaseGreeting = [greeting uppercaseString]; 216 | NSLog(@"%@", uppercaseGreeting); 217 | } 218 | @end 219 | ``` 220 | 221 | 在这个例子中,我们定义了一个额外的字符串型指针:`uppercaseGreeting `,并将向原始指针 222 | `greeting`所指向的对象发送消息` uppercaseString `的返回值赋给这个我们额外定义的指针。就像我们之前所看到的那样,这个新的字符串型的对象就是将原始的字符串中的每一个字母都变成大写而形成的。 223 | 224 | 因为`sayHello`这个方法是由类`XYZPerson`实现的,而同时,`XYZShoutingPerson`是继承于 225 | `XYZPerson`的子类,所以`XYZShoutingPerson`类中的对象也能调用`sayHello`这个方法。当我们在`XYZShoutingPerson`中调用方法`sayHello`时,`[self saySomething:...]`这个调用将会使用 *重写* 并且将“greeting”全部改写成大写的形式。程序流程如图2-3所示: 226 | 227 | ***图片2-3*** 方法重写的程序流程 228 | 229 | ![programflow3](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/programflow3.png) 230 | 231 | 我们发现这个新的方法并不是非常的理想,然而,因为我们决定之后将会修改`XYZPerson`中`saySomething`的实现:使用用户界面元素来展现而不是`NSLog()`函数,因此我们同样也需要修改 232 | `XYZShoutingPerson`的实现。 233 | 234 | 我们提出一个更好的想法,直接改变`XYZShoutingPerson`中的方法`saySomething`:调用超类(`XYZPerson`)中的`saySomenthing`来处理这个“greeting”,代码示例如下: 235 | 236 | ``` 237 | @implementation XYZShoutingPerson 238 | - (void)saySomething:(NSString *)greeting { 239 | NSString *uppercaseGreeting = [greeting uppercaseString]; 240 | [super saySomething:uppercaseGreeting]; 241 | } 242 | @end 243 | ``` 244 | 程序流程如下: 245 | 246 | ***图片2-4*** 向超类发送消息的程序流程 247 | 248 | ![programflow4](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/programflow4.png) 249 | 250 | ##对象的创建是动态的 251 | 就像我们在此章之前的篇幅中向大家介绍过的那样,对于 *Objective-C* 中的对象来说,内存的分配是动态的。创建一个对象的第一步是确保有足够多的内存不仅能分配给对象所属的类定义的属性,也有内存能够分配给在继承链中的每一个超类所定义的属性。 252 | 253 | `NSObject`这个根类提供了一个类方法,`alloc`,这个类方法为我们处理了这个问题: 254 | 255 | ``` 256 | + (id)alloc; 257 | ``` 258 | 请注意,这个方法的返回值类型是`id`。这在 *Objective-C* 中是一个非常特殊的关键字,表示“某种类型的对象”。这是一个指向对象的指针,就像`(NSObject *)`,不过对于`id`来说比较特殊的是它并没有使用星号(*)。我们将会在本章的下一个板块:[Objective-C Is a Dynamic Language]()中详细的论述。 259 | 260 | 类方法`alloc`还有另外一个非常重要的任务,清理掉分配给对象的特征的内存,通过将内存设置为零的方式。这样做避免了内存垃圾的产生,但是这对于完全地初始化一个对象来说还远远不够。 261 | 262 | 我们需要将方法`alloc`和`init`结合起来,这其中的`init`是`NSObject`的另一个类方法: 263 | 264 | ``` 265 | - (id)init; 266 | ``` 267 | 我们使用`init`这个方法来确保当一个对象被创建时,它拥有合适的初始值。本文档将会在下一个章节中对此进行详细地介绍。 268 | 269 | 注意`init`的返回值类型也是`id`。 270 | 如果一个方法的返回值是一个对象指针,则将方法1嵌套在方法2中,并且作为方法2所发送的消息的接受者是被允许的,因此我们在一个声明中可以发送多条消息。到这里,我们应该学会了分配内存并初始化一个对象的正确方法:在`init`中嵌套一个`alloc`方法,代码示例如下: 271 | 272 | ``` 273 | NSObject *newObject = [[NSObject alloc] init]; 274 | ``` 275 | 在这个例子中,我们将`newObject`设置成一个指向`NSObject`的实例的指针。 276 | 277 | 在上例的代码中,内层括号里的方法将首先被执行,因此`NSObject`这个类首先调用方法`alloc`,而这个方法会返回一个新创建的`NSObject`的实例。这个由方法`alloc`返回的对象将继续作为`init`发送的消息的接受者。而这条消息返回给这个对象的值将被赋给指针`newObject`,流程如图2-5所示: 278 | 279 | ***图片2-5*** 将`alloc`和`init`两个方法嵌套 280 | 281 | ![programflow5](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/nestedallocinit.png) 282 | 283 | >注意:`init`的返回值可以是一个不同的对象(不是`alloc`返回的那个对象),所以最好像示例中那样嵌套两个方法。千万不要在没有分配一个指针指向那个对象时初始化它,像这样: 284 | 285 | ``` 286 | NSObject *someObject = [NSObject alloc]; 287 | [someObject init]; 288 | ``` 289 | 如果`init`返回了一个其它的对象,我们将会拥有一个被分配了内存但没有初始化的对象。 290 | 291 | ###初始化方法时可以带参数 292 | 一些对象在初始化时需要带上某些特定的参数。举个例子,类`NSNumber`的一个对象在创建时必须带上一个它代表数值作为参数。 293 | 294 | 类`NSNumber`定义了几个初始化的形式,包括: 295 | 296 | ``` 297 | - (id)initWithBool:(BOOL)value; 298 | - (id)initWithFloat:(float)value; 299 | - (id)initWithInt:(int)value; 300 | - (id)initWithLong:(long)value; 301 | ``` 302 | 举个实际的例子:(A factory method is used like this) 303 | 304 | ``` 305 | NSNumber *magicNumber = [NSNumber numberWithInt:42]; 306 | ``` 307 | 这个例子和我们之前使用过的`alloc] initWithInt:]`是一样的。类方法常常直接通过`alloc`和与之配套的`init`来调用,并且使用这些类方法是非常方便的。 308 | ###如果初始化时不需要带参数 使用`new`创建一个对象 309 | 我们同样可以使用`new`这个类方法来创建一个类的实例。这个方法是由`NSObject`提供的并且在我们自己的子类中不需要被重写。 310 | 311 | 这和调用不带参数的`alloc`和`init`的用法是一样的,代码示例如下: 312 | 313 | ``` 314 | XYZObject *object = [XYZObject new]; 315 | // is effectively the same as: 316 | XYZObject *object = [[XYZObject alloc] init]; 317 | 318 | ``` 319 | 320 | ###常量提供了一个创建对象的简洁语法 321 | 某些类允许我们使用一种更简洁的语法创建实例,用 *literal* 来实现。 322 | 323 | 举个例子,我们可以使用一个文字常量来创建类`NSString`的一个实例,代码示例如下: 324 | 325 | ``` 326 | NSString *someString = @"Hello, World!"; 327 | ``` 328 | 这和分配内存并初始化一个对象或者使用类方法中的一种来实现这个实例的创建是相同的: 329 | 330 | ``` 331 | NSString *someString = [NSString stringWithCString:"Hello, World!" 332 | encoding:NSUTF8StringEncoding]; 333 | 334 | ``` 335 | 类` NSNumber`同样也允许一系列常量的使用: 336 | 337 | ``` 338 | NSNumber *myBOOL = @YES; 339 | NSNumber *myFloat = @3.14f; 340 | NSNumber *myInt = @42; 341 | NSNumber *myLong = @42L; 342 | 343 | ``` 344 | 再次强调,我们举的每一个例子和我们使用与`alloc`配套的`init`或者类方法来初始化一个对象是相同的。 345 | 346 | 我们同样也可以使用一个计算式(boxed expression)来创建`NSNumber`的实例,代码示例如下: 347 | 348 | ``` 349 | NSNumber *myInt = @(84 / 2); 350 | ``` 351 | 在这个例子中,我们使用了一个表达式,并且用了这个表达式的结果来创建一个类的实例。 352 | 353 | *Objective-C* 同样支持使用常量来创建静态数组或者字典的对象,这一点我们将会在[Values and Collections]()作进一步地讨论。 354 | 355 | ##*Objective-C* 是一种动态语言 356 | 之前曾经提到过,我们需要使用一个指针来跟踪对象在内存中的位置。因为 *Objective-C* 的动态特性,这个指针的数据类型并不做要求————当发送一条消息时正确的方法总是会被调用。`id`这个数据类型定义了一个通用对象指针。我们可以定义一个`id`类型的变量,但此时我们会失去对象在编译时的一些信息。 357 | 358 | 现在让我们思考接下来的两行代码: 359 | 360 | ``` 361 | id someObject = @"Hello, World!"; 362 | [someObject removeAllObjects]; 363 | ``` 364 | 在这个例子中,`someObject`指向一个`NSString`的实例,但是我们的编译器并不知道这个实例是某种类型的对象这件事情。`removeAllObjects`这条消息是由 *Cocoa or Cocoa Touch* 上的某些对象定义的(例如`NSMutableArray`),因此我们的编译器并不会报错,即使这两行代码在运行时会产生异常。因为一个`NSString`对象不会响应`removeAllObjects`这条消息。 365 | 366 | 接下来我们使用一个静态类型重写这俩行代码: 367 | 368 | ``` 369 | NSString *someObject = @"Hello, World!"; 370 | [someObject removeAllObjects]; 371 | ``` 372 | 这样一来,编译器在编译时就会报错了,因为编译器在任何一个公有类的接口中都找不到 373 | `removeAllObjects `的定义。 374 | 375 | 因为一个对象所属的类在程序运行时才能被确定(Because the class of an object is determined at runtime),所以我们在创建或使用一个实例时将变量定义成什么类型是无差别的。使用我们在此章节的前半部分定义过的类`XYZPerson`和`XYZShoutingPerson`,我们应该向这样编码: 376 | 377 | ``` 378 | XYZPerson *firstPerson = [[XYZPerson alloc] init]; 379 | XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init]; 380 | [firstPerson sayHello]; 381 | [secondPerson sayHello]; 382 | ``` 383 | 尽管`firstPerson`和`secondPerson`被静态地定义成`XYZPerson`上的两个对象,在程序运行时, 384 | `secondPerson`将会指向一个`XYZShoutingPerson`对象。当`sayHello`这个方法被每一个对象调用时,正确的实现将会被使用。对于`secondPerson`来说,是`XYZShoutingPerson`这个类调用了方法`sayHello`。 385 | 386 | ###确定对象的等同性 387 | 388 | 如果我们想判断两个对象是否是相同的,记住我们是使用指针来工作的是恨重要的一件事情。在标准 *C* 中,操作符`==`被用来判断两个变量的值是否相等。比如: 389 | 390 | ``` 391 | if (someInteger == 42) { 392 | // someInteger has the value 42 393 | } 394 | ``` 395 | 当我们处理对象时,操作符`==`被用来判断两个不同的指针是否指向同一个对象,比如: 396 | 397 | ``` 398 | if (firstPerson == secondPerson) { 399 | // firstPerson is the same object as secondPerson 400 | } 401 | ``` 402 | 如果我们想判断两个数据是否相等,我们应该调用一个函数,比如`isEqual`:可以从`NSObject`这个类中获得: 403 | 404 | ``` 405 | if ([firstPerson isEqual:secondPerson]) { 406 | // firstPerson is identical to secondPerson 407 | } 408 | ``` 409 | 如果我们想比较两个对象的值的大小关系,与上面判断是否相等不同的是,我们不能使用标准 *C* 中的操作符`>`和`<`。基础的函数类型,例如`NSNumber`,`NSString`以及`NSDate`为我们提供了一个方法 410 | `compare:`,代码示例如下: 411 | 412 | ``` 413 | if ([someDate compare:anotherDate] == NSOrderedAscending) { 414 | // someDate is earlier than anotherDate 415 | } 416 | 417 | ``` 418 | 419 | ###`nil`的使用 420 | 当我们定义一个纯量的同时就初始化它是一个非常优秀的习惯,否则这个量的初始值就会包括前一个栈中的内容,而这些内容往往是无用的: 421 | 422 | ``` 423 | BOOL success = NO; 424 | int magicNumber = 42; 425 | ``` 426 | 这对于对象指针来说并不是十分必要的,因为如果我们不设置任何其它的初始值,编译器就会自动将变量的值设置为`nil`: 427 | 428 | ``` 429 | XYZPerson *somePerson; 430 | // somePerson is automatically set to nil 431 | ``` 432 | 当我们没有其它值可用时,使用`nil`值是初始化一个变量指针最安全的方法,因为在 *Objective-C*中,向`nil`发送一条消息是非常合理的。如果我们真的向`nil`发送了一条消息,很明显任何事情都不会发生。 433 | 434 | >注意:如果你需要发送给`nil`的这条消息返回一个值,对于对象类型来说,这个值是`nil`;对于数值类型来说,返回值是`0`;对于`BOOL`类型来说,返回值是`NO`。Returned structures have all members initialized to zero. 435 | 436 | 如果我们需要确认一个对象的值不是`nil`(在内存中是一个变量指针),也可以使用标准 *C* 中的操作符 437 | `!=`,代码示例如下: 438 | 439 | ``` 440 | if (somePerson != nil) { 441 | // somePerson points to an object 442 | } 443 | ``` 444 | 或者仅仅只是提供一个变量: 445 | 446 | ``` 447 | if (somePerson) { 448 | // somePerson points to an object 449 | } 450 | ``` 451 | 如果变量`somePerson`的值是`nil`,它的逻辑值是`0`(false)。如果它有一个地址,那么它的值就是非零,会被当做正确的来执行。 452 | 453 | 相似地,如果我们要判断一个对象的值是不是`nil`,我们也能使用操作符`==`: 454 | 455 | ``` 456 | if (somePerson == nil) { 457 | // somePerson does not point to an object 458 | } 459 | ``` 460 | 461 | 或仅使用 *C* 语言中的否定预算符: 462 | 463 | ``` 464 | if (!somePerson) { 465 | // somePerson does not point to an object 466 | } 467 | ``` 468 | ##练习: 469 | 1.打开我们在上一章的练习中写过的工程,找到`mian.m`文件中的`main()`函数。就像在 *C* 语言中一样,这个函数代表了工程的接口。 470 | 471 | 使用`alloc`和`init`创建一个新的`XYZPerson`的实例,并且调用`sayHello`这个方法。 472 | 473 | >注意:如果编译器没有自动提示你,你需要在`main.m`文件的开头引入头文件(包括`XYZPerson`的接口)。 474 | 475 | 2.实现在此章的前半部分提到过的方法`saySomething:`,重写方法`sayHello`并且使用它。添加一些其它的greetings指针,使用上一题你所创建的实例来依次调用这些greetings。 476 | 477 | 3.为类`XYZShoutingPerson`创建新的类文件,并把它设置成继承于`XYZPerson`的子类。 478 | 重写方法`saySomething:`来显示大写的`greeting`,并且在`XYZShoutingPerson`的实例中测试这个函数。 479 | 480 | 4.实现我们在此章节的之前部分定义过的`XYZPerson`类的类工厂方法`person`,返回一个被正确地分配内存及初始化的`XYZPerson`的实例。接着,在`main()`中使用这个方法来替代`alloc`和`init`地嵌套。 481 | 482 | >提示:在类工厂方法中不要使用`[[XYZPerson alloc] init]`,请尝试使用 483 | `[[self alloc] init]` 484 | 在类工厂方法中使用`self`意味着你指向的是这个类本身。 485 | 也就是说,在实现`XYZShoutingPerson`时你不需要复写方法`person`来创建实例。尝试下列代码来确 定这是正确的: 486 | 487 | ``` 488 | XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person]; 489 | ``` 490 | 491 | 5.创建一个`XYZPerson`的指针,但不赋值。使用一个语句分支来确认这个变量是否被自动地赋值为`nil` 492 | 493 | 494 | 495 | 496 | -------------------------------------------------------------------------------- /4.数据的封装Encapsulating-Data.md: -------------------------------------------------------------------------------- 1 | #数据的封装 2 | 之前章节我们描述了可以通过向一个对象发送消息让他工作。对象还通过 *属性* 封装了一些数据。 3 | 这个章节告诉你:为一个对象声明一些属性的Objective-C语法是怎样的?这些属性会默认地被存取方法合成器 *synthesis of accessor* 实现,那么这种默认的实现方法又是遵循什么规则?如果一个属性实际上是一个变量,这个变量必须在这个对象的初始化方法中被正确地设置。 4 | 如果一个对象通过一个属性来维持一个指向至另一个对象的链接,考虑两个对象之间的关系的本质就很重要了。即使Objectivc-C的内存管理基本上都已经被 *自动引用计数Automatic Reference Counting(ARC)* 系统安排妥当了,你也应该清楚如何去避免一些诸如 *强循环引用strong reference cycles* 等会引起内存泄露的问题。这个章节阐明了一个对象的生命周期并且教你如何根据对象之间的关系去管理 *对象图 graph of objects* 。 5 | ## 属性封装了一个对象的值 6 | 为了完成他们各自的任务,大多数的对象要记录跟踪一些信息。一些对象实际上是一个数据或者多个数据的模型,比如说Cocoa框架中的`NSNumber`类保存了一个数字值;一个自定义的`XYZPerson`类建立了一个模型——一个拥有姓和名的人类。一些更加普遍的对象是这样的——他们仅仅管理了UI和其上显示的信息之间的关系,即使是这样的对象,依然需要跟踪UI元素或者UI相关的模型对象。 7 | ###为暴露在外的数据声明一个公共属性 8 | 如果一个类需要封装一些信息,Objective-C提供了一种声明这些信息的方式——属性。就像你在 *Properties Control Access to an Object’s Values* 那章看到的那样,属性像这样被声明在一个类的接口部分: 9 | 10 | ``` 11 | @interface XYZPerson : NSObject 12 | @property NSString *firstName; 13 | @property NSString *lastName; 14 | @end 15 | ``` 16 | 17 | 在这个例子中,`XYZPerson`声明了两个字符串属性来保存一个人的姓名。 18 | 在面对对象编程中一条首要规则就是对象需要把它内部的工作过程隐藏在对外公开的接口之后,所以使用对象暴露在外的行为接口来操作对象的属性要好于直接操作属性内部的值。 19 | ###使用 *存取方法Accessor Methods* 来读取或设置属性的值 20 | 你通过存取方法读取或者设置一个对象的属性: 21 | 22 | ``` 23 | NSString *firstName = [somePerson firstName]; 24 | [somePerson setFirstName:@"Johnny"]; 25 | ``` 26 | 27 | 默认情况下,编译器会为你自动生成这些存取方法,所以你除了在类接口处使用`@property`来声明一个属性之外什么都不需要做。 28 | 自动生成存取方法采取以下命名规则: 29 | 30 | - 用来获得属性值的方法( *getter方法* )和属性的名字一模一样。`firstName`属性的获取方法也被叫做`firstName`。 31 | - 用来设置一个属性的值的方法( *setter方法* )是以`set`开头的,并且跟上首字母大写的属性名。`firstName`属性的存储方法被叫做`setFirstName`。 32 | 33 | 如果想禁止属性被存取方法修改,你应在声明属性的时候为它添加一个 *特征 attribute* ,比如下面这个特征标明了这个属性是 *只读readonly* 的: 34 | 35 | ``` 36 | @property (readonly) NSString *fullName; 37 | ``` 38 | 39 | 特征告诉其他对象与这个对象交流的权限,此外,它告诉编译器如何生成属性相应的存取方法。 40 | 在上面的例子里,编译器就只会生成一个`fullName`的获取方法,而不生成`setFullName:`方法了。 41 | >注意:和`readonly`相对的就是`readwrite`(可读可写)。不需要特意写出`readwrite`特征,因为一个属性默认就是可读可写的。 42 | 43 | 如果你想要改变存取方法的名字,你就可以在声明属性特征时自定义一个名字。在这个声明一个布尔值(属性值要么是`YES`要么是`NO`)属性的例子里,我们可以把获取方法的名字改成以“is”开头。因为这个属性叫做`finished`,所以他的获取方法应该叫做`isFinished`。 44 | 那么我们就为这个属性增加一个特征吧: 45 | 46 | ``` 47 | @property (getter=isFinished) BOOL finished 48 | ``` 49 | 50 | 如果你需要增加多个特征,只需要用逗号分隔他们即可,如下: 51 | 52 | ``` 53 | @property (readonly, getter=isFinished) BOOL finished; 54 | ``` 55 | 56 | 在这个例子里,编译器只会生成一个`isFinished`方法而不是生成一个`setFinished:`方法。 57 | >注意:一般来说,属性存取方法是服从(Key-Value Coding)KVC,这也意味着这些属性也遵从这复杂的名称转换规则,若想详细了解请查阅 [Key-Value Coding Programming Guide](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/KeyValueCoding/index.html#//apple_ref/doc/uid/10000107i) 。 58 | 59 | ###点语法让存取方法的调用更加简洁 60 | 除了精确地调用存取方法,Objective-C还提供了一种方法——点语法,去操作一个对象的属性。 61 | 点语法允许你像这样操作一个属性: 62 | 63 | ``` 64 | NSString *firstName = somePerson.firstName; 65 | somePerson.firstName = @"Johnny"; 66 | ``` 67 | 68 | 点语法只是对存取方法做了一个方便的包装。实际上你使用点语法的时候,依然是通过上面提到的存取方法来获得属性或者设置属性的值的: 69 | 70 | - 使用`somePerson.firstName`来获得属性的值和使用`[somePerson firstName]`完全一致。 71 | - 通过类似`somePerson.firstName = @"Johnny`的语法来设置属性的值和使用`[somePerson setFirstName:@"Johnny"]`完全一致。 72 | 73 | 这也就是说,无论你直接调用属性存取方法还是使用点语法,属性特征依然控制着属性存取方法。如果你为属性设置了`readonly`特征,当你使用点语法来为属性设置值的时候编译器会报错。 74 | ###所有的属性背后都存在着一个实例变量 75 | 76 | 默认情况下,一个可读可写的(readwrite)的属性将会拥有着一个对应的 *实例变量(instance variables)* ,这个变量也会被编译器自动生成(synthesized)。 77 | 78 | 实例变量是指在对象的整个生命周期存在并保存他本身的值的变量。当你在为对象分配内存时(通过`alloc`),这些变量的内存也会被分配,而在对象生命周期结束时,对象占用的内存被释放,这些实例变量也就随之被释放了。 79 | 80 | 如果你没有特意规定的话,自动生成的实例变量和它对应属性的名字是相同的,但是前面会带上一个下划线。比如说一个叫做`firstName`的属性,自动生成的实例变量将会被叫做`_firstName`。 81 | 82 | 虽然利用存取方法或者点语法来操作一个对象中的属性是比较合适的,但是在类实现中的任何一个方法都可以直接操作实例变量。一个下划线开头让你能够清楚地看到,你操作的是一个实例变量,而不是其他的,比如说一个局部变量: 83 | 84 | ``` 85 | - (void)someMethod { 86 | NSString *myString = @"An interesting string"; 87 | 88 | _someString = myString; 89 | } 90 | ``` 91 | 92 | 在这个例子里,我们能够清晰地看到,`myString`是一个局部变量,而`_someString`是一个实例变量。 93 | 94 | 一般来讲我们会遵守一个规则,即使是这种情况下——在一个对象的方法实现中去访问一个实例变量,仍然应该使用存取方法或者点语法。这个时候你应该使用`self`: 95 | 96 | ``` 97 | - (void)someMethod { 98 | NSString *myString = @"An interesting string"; 99 | 100 | self.someString = myString; 101 | // or 102 | [self setSomeString:myString]; 103 | } 104 | ``` 105 | 106 | 但是,当你写一个对象的初始化、释放空间或者自定义一个存取方法的时候,就不再遵守这个规则了,我们会在之后描述相关内容。 107 | 108 | ####你可以定制自动生成的实例变量的名字 109 | 110 | 我们之前提到,根据属性自动生成的实例变量的默认名字为下划线加属性名`_propertyName`。 111 | 如果你想要这些实例变量拥有不同的名字,你需要在类实现中这样命令编译器: 112 | 113 | ``` 114 | @implementation YourClass 115 | @synthesize propertyName = instanceVariableName; 116 | ... 117 | @end 118 | ``` 119 | 120 | 比如说: 121 | 122 | ``` 123 | @synthesize firstName = ivar_firstName; 124 | ``` 125 | 126 | 在这个情况下,属性仍然还是叫`firstName`的,使用点语法和存取方法的情况下,你仍然能够通过`firstName`和`setFirstName`访问它,但是他对应的实例变量名字为`ivar_fisrtName`。 127 | 128 | >重要:如果你使用了`@synthesize`却没有为其规定一个实例变量名,像这样: 129 | 130 | ``` 131 | @synthesize firstName; 132 | ``` 133 | > 实例变量的名字将会和属性 *相同* ,为`firstName`,没有下划线。 134 | 135 | ####你可以不通过属性来定义一些实例变量 136 | 137 | 最好是无论何时你的对象需要追踪一个值或其他对象的时候,都使用一个属性。 138 | 139 | 如果你需要不声明一个属性就定义一个实例变量的话,你可以把它们放在一对花括号中,这对花括号需要位于类接口部分或实例部分顶部,像这样: 140 | 141 | ``` 142 | @interface SomeClass : NSObject { 143 | NSString *_myNonPropertyInstanceVariable; 144 | } 145 | ... 146 | @end 147 | 148 | @implementation SomeClass { 149 | NSString *_anotherCustomInstanceVariable; 150 | } 151 | ... 152 | @end 153 | ``` 154 | 155 | >注意:你也可以在类拓展处增加实例变量,详情请看:[Class Extensions Extend the Internal Implementation. 156 | ](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/CustomizingExistingClasses/CustomizingExistingClasses.html#//apple_ref/doc/uid/TP40011210-CH6-SW3) 157 | 158 | ### 在初始化方法中直接访问实例变量 159 | setter方法会导致一些副作用。它可能会触发KVC通知,如果你编写了自定义方法,它也许还会执行本应未来才能执行的任务。 160 | 「在对象的初始化方法里,你应该直接访问实例变量,因为当你配置属性时这个对象的其他部分可能还没被完全初始化。即使你不提供任何自定义的存取方法,并且了解一些来自你自己类中的副作用,以后它的子类也可以可能会重载(override)这些行为。 」 161 | 162 | 一个典型的`init`方法像这样: 163 | 164 | ``` 165 | - (id)init { 166 | self = [super init]; 167 | 168 | if (self) { 169 | // initialize instance variables here 170 | } 171 | 172 | return self; 173 | } 174 | ``` 175 | 176 | `init`方法会在做自己的初始化工作之前,先调用父类的初始化方法并把其返回值赋给`self`。父类可能没有成功的初始化,返回了一个nil值,所以你一定要记得在开始自己的初始化工作之前检查一下`self`是不是`nil`。 177 | 178 | 通过在初始化方法的第一行调用`[super init]`这种模式,一个对象是按照从 *根类(root class)* 开始,依次序调用每个子类的`init`实现来初始化的。图3-1展示了一个初始化`XYZShoutingPerson`对象的过程。 179 | 180 | 图3-1 初始化的过程 181 | ![图3-1](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/initflow.png) 182 | 183 | 正如之前的章节描述的那样,一个对象要么是通过`init`初始化的,要么就是通过调用一个方法,这个方法能够初始化一个带有特殊值的对象。 184 | 185 | 在那个`XYZPersion`类的例子中,我们可以为其提供一个初始化方法来设置这个人的姓名: 186 | 187 | ``` 188 | - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName; 189 | ``` 190 | 191 | 应该像这样实现这个方法: 192 | 193 | ``` 194 | - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName { 195 | self = [super init]; 196 | 197 | if (self) { 198 | _firstName = aFirstName; 199 | _lastName = aLastName; 200 | } 201 | 202 | return self; 203 | } 204 | ``` 205 | 206 | #### *指定初始化器 (Designated Initializer)* 是首选的初始化方法 207 | 如果一个对象声明了一个或多个初始化方法,你需要决定哪个方法是 *指定初始化器(designated initializer)* 。它通常是那个为初始化提供最多选项的方法(比如说带有最多参数的方法),并且会被其他出于方便编写的方法调用。你一般应该重载`init`,在其中调用你的指定初始化器,并为这个调用提供合适的默认值。 208 | 209 | 如果一个`XYZPerson`多了一个表示生日的属性,指定初始化器可能是这样的: 210 | 211 | ``` 212 | - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName 213 | dateOfBirth:(NSDate *)aDOB; 214 | ``` 215 | 216 | 这个方法会像上面所示的那样设置好相关的实例变量。如果你仍然想提供一个方便的初始化器仅初始化姓名,你应该通过调用指定初始化器的方式来实现这个方法,像这样: 217 | 218 | ``` 219 | - (id)initWithFirstName:(NSString *)aFirstName lastName:(NSString *)aLastName { 220 | return [self initWithFirstName:aFirstName lastName:aLastName dateOfBirth:nil]; 221 | } 222 | ``` 223 | 224 | 像开头说的那样,你也许会这样实现标准`init`来提供合适的默认值: 225 | 226 | ``` 227 | - (id)init { 228 | return [self initWithFirstName:@"John" lastName:@"Doe" dateOfBirth:nil]; 229 | } 230 | ``` 231 | 232 | 当你需要继承一个拥有多个`init`方法的类时,你有两种方式为这个类制作初始化方法,第一种是重载父类的指定初始化器并在其中制定你自己的初始化工作,第二种是添加你自己额外的初始化方法。无论哪种方法,你都应该在你做任何自己的初始化工作之前先调用父类的指定初始化器(作为[super init]的代替)。 233 | 234 | ###你可以实现自定义的存取方法 235 | 不是所有属性都拥有一个实例变量的。 236 | 237 | 举个例子吧,`XYZPerson`类可能声明了一个只读属性,用来代表一个人的全名: 238 | 239 | ``` 240 | @property (readonly) NSString *fullName; 241 | ``` 242 | 243 | 与其每次姓名改变的时候都去更新`fullName`属性(拥有的实例变量),还不如写一个自定义的存取方法来直接生成需要全名字符串简单一些: 244 | 245 | ``` 246 | - (NSString *)fullName { 247 | return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName]; 248 | } 249 | ``` 250 | 251 | 这个简单的例子使用了格式化字符串和占位符(之前章节有描述)来生成了一个包含姓名的字符串(由空格分割)。 252 | 253 | > 注意:即使这是一个很便利的例子,当然你得注意这个例子却和地理区域相关,这个例子只适合那些名字在姓之前的国家。 254 | 255 | 如果你需要为一个使用实例变量的属性写一个自定义存取方法,你定义要在方法里直接访问实例变量。举个例子,我们一般会延迟到这个属性第一次被请求的时候才会初始化这个属性,使用一个“lazy 访问器”,像这样: 256 | 257 | ``` 258 | - (XYZObject *)someImportantObject { 259 | if (!_someImportantObject) { 260 | _someImportantObject = [[XYZObject alloc] init]; 261 | } 262 | 263 | return _someImportantObject; 264 | } 265 | ``` 266 | 267 | 在返回这个值之前,方法会先检查`_someImportantObject`实例变量是不是nil,如果是的话,就实例化一个对象。 268 | 269 | > 注意:当编译器自动生成了至少一个存取方法时,它便会自动帮你生成一个实例变量。如果你为`readwrite`属性实现了getter和setter或为`readonly`属性实现了getter,编译器就假定你希望完全控制这个属性的实现,便不会自动帮你生成实例变量了。 270 | > 如果你依然需要一个实例变量,你需要请求合成一个: 271 | > `@Synthesize property = _property` 272 | 273 | ###属性默认是原子性的 274 | 默认情况下,一个Objective-C属性是原子性的: 275 | 276 | ``` 277 | @interface XYZObject : NSObject 278 | @property NSObject *implicitAtomicObject; // 默认就是原子性的 279 | @property (atomic) NSObject *explicitAtomicObject; // 当然你也可以特别标示出它是原子性的 280 | @end 281 | ``` 282 | 283 | 这意味着生成的存取器(存取方法)保证永远是完整地通过getter方法取回或完整地通过setter方法设置一个值,即使这些存取器被不同的线程同时调用。 284 | 285 | 因为原子性存取方法的内部实现和同步是私有的,所以不可能把自动生成的存取器和你自己实现的存取方法结合起来。如果你这么做,你会得到一个编译器警告,举个例子,给一个可读写的原子性属性提供了一个自定义的setter,却让编译器自己生成getter。 286 | 287 | 你可以使用`nonatomic`(非原子性)特征来规定生成的存取器仅仅直接设置或返回一个值,并不保证同一个值被多个线程同时访问时会发生什么。也正因如此,访问一个非原子性的属性会比访问原子性的速度快一些,并且还可以结合使用生成的存取器和你自己的存取器,比如一个getter。 288 | 289 | ``` 290 | @interface XYZObject : NSObject 291 | @property (nonatomic) NSObject *nonatomicObject; 292 | @end 293 | ``` 294 | 295 | ``` 296 | @implementation XYZObject 297 | - (NSObject *)nonatomicObject { 298 | return _nonatomicObject; 299 | } 300 | // setter会自动生成 301 | @end 302 | ``` 303 | 304 | > 注意:属性原子性并非等价于对象的线程安全性。考虑在一个线程上通过原子性的存取器修改了一个`XYZPerson`的姓名,这时,另一个线程访问了姓名,原子性的getter方法会返回完整的字符串(不会崩溃),但是并不能保证姓和名是对应的,如果名字是在修改之前获取的,而姓是在修改之后获取的,你将会得到一个反常的,不对应的姓名。 305 | > 这个例子很简单,但考虑到对象关系网络时,线程的安全性就变得更加复杂了。关于线程安全性的细节描述在[Concurrency Programming Guide](https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008091) 306 | 307 | ##通过拥有关系和责任关系来管理对象图 308 | 正如你已经看到的,Objective-C(在堆上)动态地分配对象的内存,这也意味着你需要用指针来追踪一个对象的地址。和纯量不同,通过一个指针变量的作用域来判断对象的生命周期不一定可能。作为替代,当一个对象被其他对象需要,这个对象就要一直保持存在在内存中。 309 | 310 | 与其担心如何手动管理每个对象的生命周期,还不如考虑一下对象之间的关系。 311 | 312 | 举个例子,在`XYZPerson`的例子里,`firstName`和`lastName`这两个字符串属性,是被那个`XYZPerson`实例有效“拥有”的。这也就意味着,只要`XYZPerson`的这个对象还在内存里,他们也必须要待在内存里。 313 | 314 | 当一个对象按这种方式依赖于其他对象,即有效地拥有了其他对象,我们就说,这个对象拥有了其他对象的 *强引用(strong reference)* (或者说强引用了其他对象)。在Objective-C中,只要一个对象被其他对象强引用了,就要一直存在。`XYZPerson`实例和两个`NSString`对象的关系如图3-2所示。 315 | 316 | ![图3-2](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongpersonproperties.png) 317 | 318 | 当一个`XYZPerson`实例从内存中释放时,假如两个字符串对象上没有其他强引用,也会被释放。 319 | 320 | 我们再增加一点这个例子的复杂性,考虑一个如图3-3所示程序一般的对象图。 321 | 322 | ![图3-3](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/namebadgemaker.png) 323 | 324 | 当用户点击Update键时,这个badge preview会把相关的名字消息更新上去。 325 | 326 | 当第一次名字被输入并且按下update键时,一个简化的对象图如图3-4所示。 327 | 328 | ![图3-4](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/simplifiedobjectgraph1.png) 329 | 330 | 当用户修改了这个人的名字,对象图就会变成图3-5的样子。 331 | 332 | ![图3-5](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/simplifiedobjectgraph2.png) 333 | 334 | 即使`XYZPerson`对象现在已经有了不同的`firstName`,badge展示界面依然保持着对原来的`@"John"`字符串对象的强引用,`@"John"`对象依然还需要存在内存里,被badge view用来打印名字。 335 | 336 | 一旦用户第二次点击Update按键,就告诉了badge view根据person对象来更新他内部的属性,所以对象图就像图3-6那样了。 337 | 338 | ![图3-6](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/simplifiedobjectgraph3.png) 339 | 340 | 在这时,就没有任何对象强引用原来的`@"John"`字符串了,他会从内存中移走。 341 | 342 | 默认情况下,Objective-C的属性和变量都会保持一个对他们对象的强引用,这一般没什么问题,但是确实会引起一个潜在的强引用循环问题。 343 | 344 | ###避免强引用循环 345 | 对于对象之间的单向关系,使用强引用是很不错的,但是一旦要处理一组对象的相互连通关系就要小心了。如果一组对象被一个循环的强引用关系联系在了一起,他们会保持这一组的每一个人保持存在即使外部没有任何对象强引用施加于其上。 346 | 347 | 一个显而易见的潜在循环引用问题就存在于一个table view(表视图——对于iOS是`UITableView`对于OSX来说是`NSTableView`)和其委托(delegate)之间。普通的table view为了能胜任多种情况,把他的一些决定委托给了外部的对象。这意味着其他的对象将会决定他本身显示的内容,也会决定当用户操作了table view中的特殊条目时会发生什么。 348 | 349 | 一个普遍的情况就是table view强引用了他的委托,并且这个委托又强引用了这个tableview,就如同图3-7那样。 350 | 351 | ![图3-7](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongreferencecycle1.png) 352 | 353 | 当其他对象解除了对table view及其委托的强引用时,问题就发生了。如图3-8。 354 | 355 | ![图3-8](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongreferencecycle2.png) 356 | 357 | 即使这些对象已经不需要存在于内存中了,因为除了他们两者之间的强引用关系,再也没有其他强引用施加于这两个对象之上。然而就是他们之间的强引用关系使他们两者依然保持存在。这就是我们所说的强引用循环。 358 | 359 | 解决这个问题的办法就是用弱引用来代替强引用。弱引用不会不隐含两个对象之间的拥有关系或责任关系并且也不会使一个对象保持存在。 360 | 361 | 如果上面的table view被修改为弱引用其委托(`UITableView`和`NSTableView`就是这么解决这个问题的),最开始的对象图就变成了图3-9这样。 362 | 363 | ![图3-9](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongreferencecycle3.png) 364 | 365 | 当其他对象释放了对table view及其委托的强引用,就没有强引用施加于委托对象了,如图3-10。 366 | 367 | ![图3-10](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongreferencecycle4.png) 368 | 369 | 这就意味着委托对象将会被释放,也因此接着释放了对于table view的强引用,如图3-11。 370 | 371 | !(图3-11)[https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/strongreferencecycle5.png] 372 | 373 | 一旦委托被释放了,就没有任何强引用施加于table view了,所以table view也被释放了。 374 | 375 | ###使用强、弱声明管理拥有关系 376 | 默认的对象类型属性的声明如下: 377 | 378 | ``` 379 | @property id delegate; 380 | ``` 381 | 382 | 对象会强引用自动生成的实例变量。如果想要声明一个弱引用,需要为这个属性添加吐下特征: 383 | 384 | ``` 385 | @property (weak) id delegate; 386 | ``` 387 | 388 | >注意:与`weak`相对的就是`strong`。但是不需要特地添加`strong`特征,因为默认就是它。 389 | 390 | 局部变量(以及非属性的实例变量)同样默认保持对于对象的强引用。这也就下面的这段代码会精准地完成你所期望的任务。 391 | 392 | ``` 393 | NSDate *originalDate = self.lastModificationDate; 394 | self.lastModificationDate = [NSDate date]; 395 | NSLog(@"Last modification date changed from %@ to %@", 396 | originalDate, self.lastModificationDate); 397 | ``` 398 | 399 | 在这个例子里,局部变量`originalData`强引用了最开头的`lastModificationDate`对象。当`lastModificationDate`属性改变了,这个属性就不再强引用原始的日期数据了,但是原始的日期数据不会被释放,因为它被`originalData`强引用了。 400 | 401 | 如果你不想让一个变量维持一个强引用关系,你应该把它声明为`__weak`,像这样: 402 | 403 | ``` 404 | NSObject * __weak weakVariable; 405 | ``` 406 | 407 | 因为一个弱引用不会使对象保持存在,当引用仍然还被使用着的时候,对象是可能被释放的。为了避免一个指针指向了一块已经被释放了的内存区域,当弱引用指向的对象被释放时,这个引用会被设置为指向`nil`。 408 | 409 | 这也就意味着如果在之前的那个日期例子中使用了弱(weak)变量的话: 410 | 411 | ``` 412 | NSDate * __weak originalDate = self.lastModificationDate; 413 | self.lastModificationDate = [NSDate date]; 414 | ``` 415 | 416 | 这个`originalDate`变量有指向`nil`的潜在可能。当`self.lastModificationDate`被重新赋值,属性就不再强引用原来的日期数据了,如果没有其他的强引用施加于其上,原始的日期数据就会被释放而`originalDate`设置成`nil`。 417 | 418 | 弱变量可能会引起一些困惑,尤其是下面这种代码: 419 | 420 | ``` 421 | NSObject * __weak someObject = [[NSObject alloc] init]; 422 | ``` 423 | 424 | 在这个例子里,新分配内存的对象并没有任何强引用施加于其上,所以立刻就会被释放,并且`someObject`也会被设置为`nil`。 425 | 426 | >注意: 与`__weak`相对应的就是`__strong`。你不需要特地声明`__strong`,因为默认情况就是它。 427 | 428 | 应该重点考虑一下一个需要多次访问一个弱引用属性多次的方法可能发生的结果,像这样: 429 | 430 | ``` 431 | - (void)someMethod { 432 | [self.weakProperty doSomething]; 433 | ... 434 | [self.weakProperty doSomethingElse]; 435 | } 436 | ``` 437 | 438 | 在这样的情况下,你可能想要把这个弱引用属性暂存(cache)在一个强引用变量中,以确保这个属性在你使用它的时候一直存在内存中: 439 | 440 | ``` 441 | - (void)someMethod { 442 | NSObject *cachedObject = self.weakProperty; 443 | [cachedObject doSomething]; 444 | ... 445 | [cachedObject doSomethingElse]; 446 | } 447 | ``` 448 | 449 | 在这个例子中,`cachedObject`变量保持了对原始弱引用属性数据值的强引用,所以原弱引用属性值不会被释放,直到程序运行离开`cachedObject`的作用域(并且`cachedObject`也没有被重新赋值为其他值)。 450 | 451 | 一定要记住,如果你在使用一个弱引用属性之前想要知道它是不是`nil`,仅仅在用之前像这样测试它是不够的: 452 | 453 | ``` 454 | if (self.someWeakProperty) { 455 | [someObject doSomethingImportantWith:self.someWeakProperty]; 456 | } 457 | ``` 458 | 459 | 因为在多线程应用中,属性可能会在测试和调用的中间被释放,导致这个测试失去作用。作为代替,你应该声明一个强引用局部变量来缓存这个值,像这样: 460 | 461 | ``` 462 | NSObject *cachedObject = self.someWeakProperty; // 1 463 | if (cachedObject) { // 2 464 | [someObject doSomethingImportantWith:cachedObject]; // 3 465 | } // 4 466 | cachedObject = nil; // 5 467 | ``` 468 | 469 | 在这个例子里,在第1行创建了一个强引用,意味着这个对象将会为了测试和后期的调用一直存在。在第5行,`cachedObject`被设置为了`nil`,因此放弃了对于之前对象的强引用。如果没有其他强引用施加于原来对象之上,原来对象会被释放,同时,`someWeakProperty`将会被设置为`nil`。 470 | 471 | ###使用对某些类不安全的Unretained引用 472 | 在Cocoa和Cocoa Touch中,有很少的类还不支持弱引用,这也就意味着你不可能声明一个弱引用属性或者一个弱引用变量去追踪他们。这些类包括`NSTextView`,`NSFont`,`NSColorSpace`;想要看更全面的列表,请查询[Transitioning to ARC Release Notes](https://developer.apple.com/library/content/releasenotes/ObjectiveC/RN-TransitioningToARC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011226) 473 | 474 | 如果你需要弱引用这些类的话,你必须使用不安全的引用。对于属性来说,这意味着使用`unsafe_unretained`特征: 475 | 476 | ``` 477 | @property (unsafe_unretained) NSObject *unsafeProperty; 478 | ``` 479 | 480 | 对于变量,你需要使用`__unsafe_unretained`: 481 | 482 | ``` 483 | NSObject * __unsafe_unretained unsafeReference; 484 | ``` 485 | 486 | 一个不安全的引用和弱引用类似,他不能保持其关联的对象持续存在。但是,当这个对象被释放的时候,这个指针变量不会被设置为`nil`。这也就意味着这个指针会指向一段被释放的内存,我们称其为 *野指针(dangling pointer)*,这就是为什么它被称作为“不安全”的。向一个野指针发送消息会导致崩溃。 487 | 488 | ###Copy属性保存一份它自己的副本 489 | 在一些情况下,一个对象可能希望把别人设置给它的属性保存一个属于自己的副本。 490 | 491 | 举个例子,之前图3-4展示的`XYZBadgeView`的类接口部分是这样的: 492 | 493 | ``` 494 | @interface XYZBadgeView : NSView 495 | @property NSString *firstName; 496 | @property NSString *lastName; 497 | @end 498 | ``` 499 | 500 | 两个`NSString`属性被声明了,也隐式地保持了对于其对应对象的强引用。 501 | 502 | 考虑一下,当其他对象创建了一个字符串,并把其设置为badge view的一个属性值会发生什么,像这样: 503 | 504 | ``` 505 | NSMutableString *nameString = [NSMutableString stringWithString:@"John"]; 506 | self.badgeView.firstName = nameString; //译者注:firstName属性指向了nameString指向的地方,也就是说badge view的firstName属性和nameString局部变量指向了同一段内存,这也解释了后面为什么firstName属性会随着nameString的变化而变成了Jonny。 507 | ``` 508 | 509 | 这种做法是可行的,因为`NSMutableString`是`NSString`的子类。即使badge view觉得它是在处理一个`NSString`,实际上,它处理的是一个`NSMutableString`。 510 | 511 | 这也就意味着这个字符串可以改变: 512 | 513 | ``` 514 | [nameString appendString:@"ny"]; 515 | ``` 516 | 517 | 在这个例子里,即使最开始badge view的`firstName`属性被设置为`John`,最后却变成了`Johnny`了,因为这个可变的字符串已经改变了。(译者注:此时 518 | 519 | 你可能选择让badge view为每一个设置给它属性的值保存一个副本,以便当属性值被设置时有效地捕获这个字符串。通过在属性声明前增加`copy`特征: 520 | 521 | ``` 522 | @interface XYZBadgeView : NSView 523 | @property (copy) NSString *firstName; 524 | @property (copy) NSString *lastName; 525 | @end 526 | ``` 527 | 528 | 现在view就会为这个两个字符串保存属于他自己的副本。即使被设置成了可变字符串并且这个可变字符串立刻被改变了,badge view仅捕获它属性值被设置时的值。举个例子: 529 | 530 | ``` 531 | NSMutableString *nameString = [NSMutableString stringWithString:@"John"]; 532 | self.badgeView.firstName = nameString; //译者注:这里实际上nameString的值被复制进了firstName属性指向的实例变量,现在值为John的量变成了两个,而后nameString局部变量变成了Johnny而firstName属性并不受影响。 533 | [nameString appendString:@"ny"]; 534 | ``` 535 | 这时,badge view的`firstName`属性是原来的“John”字符串的一个副本,不受其他影响。 536 | 537 | `copy`特征意味着属性将会使用一个强引用,因为他必须保持它创建的新对象存在。 538 | 539 | > 所有你希望设置为`copy`的属性必须支持`NSCopying`,就是说它必须遵从`NSCopying`协议。关于协议在[Protocols Define Messaging Contracts](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithProtocols/WorkingwithProtocols.html#//apple_ref/doc/uid/TP40011210-CH11-SW2)中有详细描述。想要详细了解`NSCopying`请看[NSCopying](https://developer.apple.com/reference/foundation/nscopying)或[Advanced Memory Management Programming Guide](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html#//apple_ref/doc/uid/10000011i)。 540 | 541 | 如果你希望直接设置一个属性的实例变量为`copy`,举个例子,比如说在初始化方法中,不要忘记设置最原来的那个对象: 542 | 543 | ``` 544 | - (id)initWithSomeOriginalString:(NSString *)aString { 545 | self = [super init]; 546 | if (self) { 547 | _instanceVariableForCopyProperty = [aString copy]; 548 | } 549 | return self; 550 | } 551 | ``` 552 | 553 | 554 | -------------------------------------------------------------------------------- /5.定制已有的类Customizing-Existing-Classes.md: -------------------------------------------------------------------------------- 1 | #四、定制已有的类 2 | 每一个对象都有它明确的任务,例如封装数据、在界面上展示它的内容、控制信息流等。之前也讲过,类的声明部分定义了它的对象与外部交互的方法,通过这些方法来完成它的任务。 3 | 4 | 在某些特定的场合,你希望拓展已经存在的类,为其增加一些针对特定场合的有用功能。举个例子,当你的app需要经常在屏幕上显示一个字符串的时候,比起每次都重复创建一个用来显示字符串的对象,更方便的做法是让`NSString`类本身就拥有将其包含的字符串打印在屏幕上的能力。 5 | 6 | 在这种情况下,直接修改原来的类接口,为其增加实际功能的做法并不合适。因为大多数情况下我们在程序中使用字符串类型,比如`NSString`类,并非是为了让其在屏幕上显示,而且`NSString`类是框架(framework)中的类,你也不能随意去更改它的声明部分和实现部分。 7 | 8 | 此外如果你想用类的继承特性来为`NSString`类的子类添加显示的功能,还是会遇到问题。因为如果你这样做,`NSString`类的其他子类,例如`NSMutableString`类,则无法使用这个新功能。而且,尽管`NSString`类可以同时在OS X(macOS)和iOS系统中使用,但是要使它显示在屏幕上的代码却是不同的,这就需要多个子类来适配不同的平台。 9 | 10 | 为了解决以上问题,Objective-C提供了 *类别(categories)* 和 *class extensions* 这两种方式来为已存在的类加入新的方法。 11 | 12 | ##类的类别 13 | 为了让一个已存在的类在你的程序中更加便于使用,你想为一个其添加一个实用的功能,使用类别(categories)会是一个很好的选择。 14 | 15 | 和普通的Objective-C类的声明一样,声明一个类别的语法要用到 `@interface`关键字,但不需要声明这个类别的继承关系,取而代之的是,我们需要在圆括号里具体说明这个类别的名字。如下: 16 | 17 | ``` 18 | @interface ClassName (CategoryName) 19 | 20 | @end 21 | ``` 22 | 23 | 任何一个类都可以声明一个类别,即使你不知道它实际代码是如何实现的(比如标准Cocoa或CocoaTouch类)。你在类别中声明的任何方法都可以在它原来类的实例对象和原来类的子类的实例对象中使用。在运行过程中,类别中声明和实现的方法和原来的类中声明的方法没有任何区别。 24 | 25 | 以前面的章节中提到的`XYZPerson`类作为例子,这个类有两个属性,分别是firstName和lastName。如果你打算做一个纪录成绩的app的话,你经常需要展示一个把姓放在名前面的清单: 26 | 27 | ``` 28 | Appleseed, John 29 | 30 | Doe, Jane 31 | 32 | Smith, Bob 33 | 34 | Warwick, Kate 35 | ``` 36 | 37 | 除了每次使用时产生一个`last name,first name`格式的字符串,你还可以为`XYZPerson`类添加一个类别,就像这样: 38 | 39 | ``` 40 | #import "XYZPerson.h" 41 | 42 | @interface XYZPerson (XYZPersonNameDisplayAdditions) 43 | 44 | - (NSString *)lastNameFirstNameString; 45 | 46 | @end 47 | ``` 48 | 49 | 50 | 在这个例子中,`XYZPersonNameDisplayAdditions`这个类别声明了一个新的方法,这个方法返回一个`last name,first name`格式的字符串。 51 | 52 | 一个类别通常和它的原类在不同的头文件中定义,并在不同的实现文件中实现。在上面的例子中,这个类别被定义在一个叫`XYZPerson+XYZPersonNameDisplayAdditions.h`头的文件中。 53 | 54 | 尽管类别中新定义的方法能被它及其所有子类的对象使用,但是在使用前你必须你必须在该文件中引入这个类别的头文件,否则你将遇到一个编译警告和错误。 55 | 56 | 一个类别的实现长这样: 57 | 58 | ``` 59 | #import "XYZPerson+XYZPersonNameDisplayAdditions.h" 60 | 61 | 62 | 63 | @implementation XYZPerson (XYZPersonNameDisplayAdditions) 64 | 65 | - (NSString *)lastNameFirstNameString { 66 | 67 | return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName]; 68 | 69 | } 70 | 71 | @end 72 | ``` 73 | 74 | 一旦你声明并且实现了新添的方法,你就可以在任何这个类的对象中使用这个方法,就好像这个方法是在类中原本就有的一样。 75 | 76 | ``` 77 | #import "XYZPerson+XYZPersonNameDisplayAdditions.h" 78 | 79 | @implementation SomeObject 80 | 81 | - (void)someMethod { 82 | 83 | XYZPerson *person = [[XYZPerson alloc] initWithFirstName:@"John" 84 | 85 | lastName:@"Doe"]; 86 | 87 | XYZShoutingPerson *shoutingPerson = 88 | 89 | [[XYZShoutingPerson alloc] initWithFirstName:@"Monica" 90 | 91 | lastName:@"Robinson"]; 92 | 93 | 94 | 95 | NSLog(@"The two people are %@ and %@", 96 | 97 | [person lastNameFirstNameString], [shoutingPerson lastNameFirstNameString]); 98 | 99 | } 100 | 101 | @end 102 | ``` 103 | 104 | 除了可以为已有类添加新的方法外,你还可以用类别,把那些实现起来很复杂的类分解成多个源代码文件。比方说,你可以把关于在界面上显示的代码,例如几何计算、颜色和倾斜度等等放在另一个文件中,和其他该类的实现代码分开存放。或者,你也可以根据你软件所在的平台(macOS或iOS),来对类别方法提供不同的实现。 105 | 106 | 实例方法和类方法都可以在类别中声明,但是一般不在类别中声明一个新的属性。尽管你能在类别中声明一个额外属性,但你却不能声明一个相应的实例变量。这意味着编译器既不会自动生成类别中新增加的属性对应的实例变量,也不会自动生成这些属性的存取方法。你可以在分类中实现自己的存取方法,但你写的存取方法只能读取原来类保存的属性。 107 | 108 | 为一个已有类添加新的属性的唯一方法就是使用*类的延伸(class extension)*,这将在接下来的章节介绍。 109 | 110 | >注意: Cocoa 和 Cocoa Touch类库中包括了很多原始类的类别。`NSString`类在OS X系统的屏幕上显示字符串的功能在`NSStringDrawing`这个类别中实现了。这个类别包含了`drawAtPoint:withAttributes:`和`drawInRect:withAttributes:`等方法。在iOS系统中,实现这个功能的类别是`UIStringDrawing`,其中包含了`drawAtPoint:withFont:`和`drawInRect:withFont:`等方法。 111 | 112 | ###避免类别中方法名的冲突 113 | 因为类别中的方法名是添加在原来的类中的,所以你需要很谨慎的为这些方法命名,避免它们与原来的类的方法发生冲突。 114 | 115 | 如果一个定义在类别中的方法名和原来的类中的一个方法名重复了,或者和该类的另一个类别的一个方法名重复了,在运行过程中具体调用哪个方法来实现会是随机决定的。这种情况很少在为自己的类添加类别时发生,而经常在为Cocoa 和 Cocoa Touch类库的类添加方法时发生。 116 | 117 | 举个例子,假如一个app需要从一个远程网络服务器中获取数据,这就需要一个方法来为字符串编码成Base64形式。这时候就需要为`NSString`类添加一个类别,并且在其中添加一个返回编码后的字符串的`base64EncodedString`方法。但当你把其他类库加进来时,问题就发生了。其他类库也许也会为`NSString`这个类添加一个叫`base64EncodedString`的方法。那么在运行的时候,只有一个方法会被使用,而这个方法到底是哪一个,是不确定的。 118 | 119 | 还有另一个问题,就是当一个类添加了一个方法之后,有可能在接下来的代码迭代版本中,又添加了另一个同名的方法,这样新旧两个方法就冲突了。举个例子,`NSSortDescriptor`这个类中有一个叫`initWithKey:ascending:`的初始化方法,这个方法在早期的OS X和iOS版本中是没有相关的类方法。按照命名传统,这个类方法应该叫做`sortDescriptorWithKey:ascending:`,所以你可以在`NSSortDescriptor`这个类上添加这个类方法。在早期的OS X和iOS版本中这样做是没有问题的,但是在Mac OS X 10.6和iOS4.0版本之后,官方类库中就为`NSSortDescriptor`这个添加了`sortDescriptorWithKey:ascending:`这个类方法,这就意味着在新版本的系统中你自己命名的方法和类库中的方法冲突了,必须要手动解决这个问题。 120 | 121 | 为了避免这些不必要的冲突,很有必要为新添加的方法名添加一个前缀,特别是那些在官方类库中的类。这种做法有点像为你自己的类名添加一个前缀。你可以使用你名字的首字母来作为你的前缀,包括方法名和类名的前缀。除此之外,还可以用下划线把前缀和方法名连接起来。在`NSSortDescriptor`这个例子中,你自定义的类别可以长这样: 122 | 123 | ``` 124 | @interface NSSortDescriptor (XYZAdditions) 125 | 126 | + (id)xyz_sortDescriptorWithKey:(NSString *)key ascending:(BOOL)ascending; 127 | 128 | @end 129 | ``` 130 | 131 | 这样做可以确保在运行的时候会调用正确的方法而没有产生歧义。你的方法是这样被调用的: 132 | 133 | ``` 134 | NSSortDescriptor *descriptor = 135 | 136 | [NSSortDescriptor xyz_sortDescriptorWithKey:@"name" ascending:YES]; 137 | 138 | ``` 139 | 140 | ##类的拓展 141 | 类的拓展和类别有点相似,不同的是,类的拓展只能在你有这个类的实现的源代码时使用(类的拓展和类本身是在同一时间编译的)。因为类的拓展中声明的方法需要在该类的`@implementation`代码块中实现,也就是说,你不能为Cocoa 和 Cocoa Touch类库中的类添加拓展,比如`NSString`类。 142 | 143 | 为一个类添加拓展的语法和添加类别有点相似,长这样: 144 | 145 | ``` 146 | @interface ClassName () 147 | 148 | 149 | 150 | @end 151 | ``` 152 | 153 | 因为括号中并没有添加其他名字,所以类的拓展经常被当作类的一个匿名的类别。 154 | 155 | 不像普通的类别,类的拓展可以添加它的属性和实例变量。用拓展定义一个属性的语法是这样的: 156 | 157 | ``` 158 | @interface XYZPerson () 159 | 160 | @property NSObject *extraProperty; 161 | 162 | @end 163 | ``` 164 | 165 | 编译器会自动实现拓展中出现的属性和实例变量的get方法和set方法。如果你在拓展中定义了一个新的方法,你必须在该类的原实现代码中实现这个方法。拓展中也可以增加新的实例变量,新增添的实例变量须在拓展中用花括号括起来: 166 | 167 | ``` 168 | @interface XYZPerson () { 169 | 170 | id _someCustomInstanceVariable; 171 | 172 | } 173 | 174 | ... 175 | 176 | @end 177 | ``` 178 | 179 | ###使用类的拓展隐藏私有信息 180 | 原有的类的声明部分定义了它和外界交互的方式,这些信息都是希望外部可以访问的。也就是说,这是类的公有部分。 181 | 182 | 类的拓展一般用来添加不希望出现在公有部分的 *私有方法* 和 *私有属性* ,这些私有成员只在类的内部使用。举个例子,在公有部分定义一个`readonly`的公有属性,再在类的拓展中定义一个`readwrite`的私有属性,这样,外部就可以访问到一个类内部经常变化的一个属性,却不会去改变它。 183 | 184 | 以`XYZPerson`类作为例子,为了标记能标记一个人的身份证号,我们为它添加一个叫`uniqueIdentifier`的属性,要更改一个人的身份证号是很难的,所以我们把这个属性设置为只读的,再声明一个为一个人分配身份证号的方法,代码这样写: 185 | 186 | ``` 187 | @interface XYZPerson : NSObject 188 | 189 | ... 190 | 191 | @property (readonly) NSString *uniqueIdentifier; 192 | 193 | - (void)assignUniqueIdentifier; 194 | 195 | @end 196 | ``` 197 | 198 | 这意味着`uniqueIdentifier`这个属性不能被外部直接改变。如果一个人还没有一个身份证号,这时候就要调用`assignUniqueIdentifier`为他分配一个身份证号。如果想要在内部改变这个人的身份证号,可以在类的拓展中重新声明一个可读写的属性,这需要写在这个类的.m文件的最上面。 199 | 200 | ``` 201 | @interface XYZPerson () 202 | 203 | @property (readwrite) NSString *uniqueIdentifier; 204 | 205 | @end 206 | 207 | 208 | 209 | @implementation XYZPerson 210 | 211 | ... 212 | 213 | @end 214 | ``` 215 | 216 | >注意:readwrite可读写性是可以省略的,因为声明一个属性时默认就是可读写的,这里加上是为了和公有部分的只读的属性区分清楚。 217 | 218 | 这意味着编译器会实现这个属性的set方法,所以在这个函数的实现代码中既可以用 *.语法* 来修改这个属性的值,也可以使用set方法来改变它。通过在`XYZPerson`类的.m文件中声明它的拓展,我们可以定义这个类的一些私有信息。如果其他的对象想要对这些私有变量使用set方法改变它的值,编译器就会报错。 219 | 220 | >注意:(关于set方法是什么)通过上面的代码我们在`XYZPerson`类中重新声明了一个可读写的`uniqueIdentifier`属性,这时编译器就会自动实现一个叫`setUniqueIdentifier: `的set方法,无论这个类的实现部分知不知道(be aware of)类的拓展,这个方法在`XYZPerson`的每一个对象中都能被调用。 221 | >当对象想要调用它的私有方法或者想要对读的属性使用set方法时,编译器会发出警告(complain),但是还是有方法可以调用一个私有方法,比如用`NSObject`提供的`performSelector:...`方法。在设计类时,我们要设计好那哪些方法是公有的,哪些是私有的,以免出现不必要的麻烦。 222 | >如果你想让私有方法和变量在不同的类中共用的话,你可以把类的拓展写在一个单独的.h文件中,再在需要的时候引用这个头文件。拥有两个头文件的类并不常见。如果一个类同时拥有如`XYZPerson.h` 和 `XYZPersonPrivate.h`两个头文件,你应该只把`XYZPerson.h`头文件暴露出来。 223 | 224 | ##定制类的其他方法 225 | 类别和拓展提供了为已有类添加方法的简单途径,但是有时候这两个不是最佳的选择。 226 | 227 | 面向对象编程其中一个目标就是要重复利用代码,这意味着类需要在大多数情况下都可重复利用。举个例子,如果你要设计一个把对象显示在屏幕上的类,那这个类最好最好要在多数的情况下都能使用。对于对象的布局和内容等这些很难写的代码,一种方法是使用类的继承特性,这需要在子类中重载一些方法。尽管这样相对地使得重用代码更简单,但是你仍然要在不同的情况设计不同的子类。 228 | 229 | 另一种方法是使用 *代理(delegate)* 。任何可能重复使用的操作都可以代理给另一个对象,这个对象将在运行的时候决定是否执行那些操作。一个常用的例子就是表视图(OS X系统下的`NSTableView`类和iOS系统的`UITableView`类)。为了使一个普通的表视图能在很多地方发挥作用,它把一些方法代理了给另一个使用它的对象,在下一章我们将详细讨论代理的使用,请见 Working with Protocols。 230 | 231 | ###Objective-C的运行时 232 | Objective-C提供一种非常灵活的模式——运行时系统。 233 | 234 | 就像一些方法在得到信息时才会被调用,很多的操作都不是在编译的时候决定的,而是在app运行的时候。Objective-C不仅仅是一门把代码编译成机器二进制码的高级语言,它还有一套运行时系统来执行那些代码。 235 | 236 | 我们可以直接操作运行时系统,例如为对象添加 *associative references*。不像类的拓展,associative references并不影响类的声明和实现,这意味着可以对类库中的类和你没有它实现代码的类使用它。 237 | 238 | associative references把一个对象和另一个对象连起来,与属性和实例变量是用的是相似的方式。想要了解更多的话请参考associative reference。如果想了解更多关于运行时系统的,请看Objective-C Runtime Programming Guide。 239 | 240 | ##练习 241 | 1.为`XYZPerson`类添加一个类别,并且实现一个新的方法,例如以不同的方式显示一个人的名字。 242 | 2.为`NSString`类添加一个类别,添加一个把字符串全部转换成大写字母的方法,并且调用已存在的方法`NSStringDrawing`把它在屏幕上显示出来。 243 | 3.为`XYZPerson`类添加两个只读的属性,用来表示一个人的身高的体重,以及两个叫`measureWeight`和`measureHeight`的方法。用类的拓展重新声明这两个属性为可读写型,并且实现上面两个方法。 244 | 245 | 246 | -------------------------------------------------------------------------------- /6.协议的使用Working-with-Protocols.md: -------------------------------------------------------------------------------- 1 | # Working-with-Protocols-协议的使用 2 | ##简述 3 | 在现实世界中,公务人员处理事件时需要遵循严格的流程。例如,执法部门在进行调查取证时必须遵守“章程”。 4 | 5 | 在面向对象编程中,为*某一对象* 的*某种特定情况* 定义*一组行为* 非常重要。例如:表视图(viewController)需要与数据源对象通信,以便找到显示内容。这意味着数据源必须能够响应表视图所有可能发送出的特定消息集合。 6 | 7 | 数据源可以是任何类的实例。例如:视图控制器(OS X上的NSViewController或iOS上的UIViewController的子类)、继承自NSObject的专用数据源类等。通过声明对象实现所必要的方法,使得表视图知道该对象是否可以用作其数据源。 8 | 9 | Objective-C允许自定义协议,声明预计用于某种特定情况的方法。本章描述了定义协议的语法,以及如何在一个类的接口中让其遵守定义的协议,这意味着类必须实现其所需的方法。 10 | ##协议的定义 11 | 我们已知类接口声明了与该类相互关联的方法和属性。与此相反,协议用于*声明独立于任何特定类的方法和属性* 。 12 | 13 | ``` 14 | @protocol ProtocolName 15 | // list of methods and properties 16 | @end 17 | ``` 18 | 19 | 协议可以包括实例方法、类方法以及属性的声明。 20 | 21 | 例如,用来显示一个饼状图的一个自定义的视图子类,如图所示: 22 | 23 | ![show examples](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/piechartsim.png) 24 | 25 | 26 | 为了尽可能提高视图重用性,所有关于显示信息内容的动作都被交由另一个对象负责,该对象被称为数据源。这意味着同一个视图类的多个对象可以通过不同的数据源显示相互独立的内容。 27 | 28 | 例如:显示饼状图所需的最基本信息包含每个部分的数值、相对大小、各自的标题等。因此,饼状图的数据源协议,应该类似于: 29 | 30 | ``` 31 | @protocol XYZPieChartViewDataSource 32 | - (NSUInteger)numberOfSegments; 33 | - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex; 34 | - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex; 35 | @end 36 | ``` 37 | 38 | >注:上面的协议中使用NSUInteger来表示整数纯量的值。该类型将在下一章做进一步讨论。 39 | 40 | 饼状图的视图类的接口需要一个属性来追踪数据源对象。这个对象可以属于任意类,所以基本的属性类型应该是`id`。而关于这个对象我们唯一已知的是它遵守相关的协议。 41 | 42 | **声明数据源属性的语法类似于:** 43 | 44 | ``` 45 | @interface XYZPieChartView : UIView 46 | @property (weak) id dataSource; 47 | ... 48 | @end 49 | ``` 50 | 51 | Objective-C用尖括号`< >`来表示所遵守的协议。这个例子中对一个指向通用类型对象的指针声明了一个弱引用,并且令其遵守`XYZPieChartViewDataSource`协议。 52 | 53 | >注:出于对象关系管理的需要,指向代理和数据源的属性通常被声明为弱引用。(详见章节:避免强引用循环) 54 | 55 | 在声明属性时指明其所遵守的协议。如果将视图属性指向一个不遵守协议的对象,即使基本的属性类型为通用型,也会受到编译器的警告。属性所指向的对象是`UIViewController`类还是`NSObject`类的实例对象并不重要。只要其遵守协议即可,这样饼状图视图就知道它可以请求需要显示的信息。 56 | 57 | ###非正式协议-拥有可选方法的协议 58 | >译者注:正式协议定义的方法均为必须方法,所有符合该协议的类都需要实现。非正式协议中的方法为可选方法,一个符合协议的类可以不实现该方法 59 | 60 | 正式协议中声明的所有方法都是必需的方法。 这意味着任何符合该协议的类都必须实现所有方法。 61 | 62 | 但我们可以在协议中指定可选方法(一个类只有在需要时才实现的方法)。 63 | 64 | 例如:我们可以认为饼图上的标题是可选的。 如果数据源对象不实现`titleForSegmentAtIndex:`方法,则在视图中不显示标题。 65 | 66 | 我们可以使用`@optional`指令将协议方法标记为可选方法,如下所示: 67 | 68 | ``` 69 | @protocol XYZPieChartViewDataSource 70 | - (NSUInteger)numberOfSegments; 71 | - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex; 72 | @optional 73 | - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex; 74 | @end 75 | ``` 76 | 77 | 在这个示例中,仅有`titleForSegmentAtIndex:`方法被标记为可选方法,在`titleForSegmentAtIndex:`之前的方法并未被标记为可选,所以在符合该协议的类中它们必须被实现。 78 | 79 | `@optional`指令适用于紧随其后的所有方法,直到协议的定义结束或者遇到另一个指令,例如:`@required`。 我们可以向协议添加更多的方法,如下所示: 80 | 81 | ``` 82 | @protocol XYZPieChartViewDataSource 83 | - (NSUInteger)numberOfSegments; 84 | - (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex; 85 | @optional 86 | - (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex; 87 | - (BOOL)shouldExplodeSegmentAtIndex:(NSUInteger)segmentIndex; 88 | @required 89 | - (UIColor *)colorForSegmentAtIndex:(NSUInteger)segmentIndex; 90 | @end 91 | ``` 92 | 93 | 该示例定义了一个具有三个必需方法和两个可选方法(这两个可选方法处于指令`@optional`和`@required`之间)的协议。 94 | 95 | ###检查可选方法是否在运行时实现 96 | 如果协议中的方法被标记为可选,我们必须检查对象在调用该方法 *之前* 该方法是否被实现。 97 | 98 | 例如:饼图视图可能会测试如下的段标题方法: 99 | 100 | ``` 101 | NSString *thisSegmentTitle; 102 | if ([self.dataSourcerespondsToSelector:@selector(titleForSegmentAtIndex:)]) 103 | { 104 | thisSegmentTitle = [self.dataSourcetitleForSegmentAtIndex:index]; 105 | } 106 | ``` 107 | 108 | `responsesToSelector:`方法使用一个选择器在编译后引用一个方法的标识符。 我们可以通过`@selector()`指令提供正确的标识符,指定方法的名称来提供正确的标识符。 109 | 110 | 如果该示例中的数据源实现了该方法,则使用标题;否则,标题仍然为`nil`。 111 | 112 | >注:本地对象变量自动初始化为`nil`。 113 | 114 | 如果试图调用一个符合协议的`id`的`respondingToSelector:`方法,例如上面定义的,编译器会报告错误:“没有已知的实例方法”。 若你使用协议来限定`id`,所有静态类型检查都会恢复; 如果您尝试调用未在该协议中定义的方法,将产生错误。 可以通过将自定义协议设置为采用`NSObject协议`来避免编译器产生错误。 115 | 116 | ###继承其他协议 117 | 就像一个Objective-C类可以继承一个超类,我们也可以指定一个协议符合另一个协议。 118 | 119 | 例如:最好的做法就是定义您的协议符合`NSObject协议`(一些NSObject行为从其类接口拆分为单独的协议; `NSObject类`采用`NSObject协议`)。 120 | 121 | 通过指定自己的协议符合NSObject协议,表明任何采用自定义协议的对象也将为每个`NSObject协议`的方法提供实现。 你可以使用`NSObject`的一些子类,而不需要为这些`NSObject`方法提供自己的实现。 对于如上所述的情况,协议的采用是有效的。 122 | 123 | 要指定一个协议符合另一个协议,需要在尖括号中提供其他协议的名称,如下所示: 124 | 125 | ``` 126 | @protocol MyProtocol 127 | ... 128 | @end 129 | ``` 130 | 131 | 在本示例中,采用`MyProtocol`的所有对象均可有效地采用`NSObject协议`中声明的所有方法。 132 | 133 | ##使用协议 134 | 表示类采用协议的语法使用尖括号,如下所示: 135 | 136 | ``` 137 | @interface MyClass : NSObject 138 | ... 139 | @end 140 | ``` 141 | 142 | 这意味着所有MyClass实例不仅将响应在接口中明确声明的方法,而且MyClass也提供MyProtocol中所需方法的实现。 无需重新声明类接口中的协议方法,只需要采用协议即可。 143 | 144 | >注意:编译器不会自动合成已采用协议中声明的属性。 145 | 146 | 如果一个类需要采用多个协议,可以将其指定为逗号分隔的列表,如下所示: 147 | 148 | ``` 149 | @interface MyClass : NSObject 150 | ... 151 | @end 152 | ``` 153 | 154 | >**译者注:可以以任意顺序列出所有要采用的协议,对于结果没有任何影响** 155 | 156 | >提示:如果你发现自己在类中采用了大量的协议,这可能是建立一个过度复杂的类的标志之一。我们需要通过分割现有类中必要的行为建立多个较小的拥有明确责任的类。 157 | 对于新的OS X和iOS开发人员有一个相对常见的陷阱:使用单个应用程序委托类来包含应用程序的大多数功能(管理底层数据结构,向多个用户界面元素提供数据,以及响应手势和其他用户交互)。 随着复杂度的提高,类的维护难度不断上升。 158 | 159 | 一旦类声明符合协议,该类必须为每个必需的协议方法和您选择的任何可选方法提供方法实现。 如果未能实现任一必需的方法,编译器将发出警告。 160 | 161 | >注意:协议中的方法声明与其他声明一样, 实现中的方法名称和参数类型必须与协议中的声明相匹配。 162 | 163 | **Cocoa和Cocoa Touch定义了大量的协议** 164 | 165 | Cocoa和Cocoa Touch对象会在各种不同的情况下使用协议。例如:表视图类(适用于OS X的`NSTableView`和适用于iOS的`UITableView`)都使用数据源对象为其提供必要的信息。两者都定义自己的数据源协议,其使用方式与上述`XYZPieChartViewDataSource`协议示例大致相同。两个表视图类也允许您设置一个委托对象,同时它也必须符合相关的`NSTableViewDelegate`或`UITableViewDelegate`协议。该委托对象负责处理用户交互,或定制某些条目的显示。 166 | 167 | 一些协议用于指示类之间的非分级相似性,而不是链接到特定的类要求(一些协议可实现比涉及多个不相关类更一般的Cocoa或Cocoa Touch通信机制) 168 | 169 | 例如:许多框架模型对象(例如`NSArray`和`NSDictionary`等集合类)支持`NSCoding`协议,这意味着它们可以对其属性进行编码和解码,以便存档或分发为原始数据。 若图中的每个对象都采用`NSCoding`协议,可以使得将整个图对象写入磁盘相对容易。 170 | 171 | 几个Objective-C语言级功能也依赖于协议。例如:为了使用`快速枚举`,集合必须采用`NSFastEnumeration`协议,这样可使枚举集合容易。此外,例如复制一些对象:在使用具有复制属性的属性时,(如“复制属性维护自己的副本”中所述)任何你试图复制的对象必须采用`NSCopying`协议,否则会产生运行时异常。 172 | 173 | ##用于匿名对象的协议 174 | 协议在对象的类未知或需要保持隐藏的情况下也是可用的。 175 | 176 | 作为示例,框架的开发者可以选择不为框架内的某个类发布接口。 因为类名是未知的,所以该框架的用户不可能直接创建该类的实例。 与此相反,框架中的一些其他对象通常被指定返回现成的实例,如下所示: 177 | 178 | ``` 179 | id utility = [frameworkObject anonymousUtility]; 180 | ``` 181 | 182 | 为了使这个anonymousUtility对象可用,即使没有提供原始的类接口,框架的开发人员也可以发布一个显示其某些方法的协议。 这意味着类保持匿名,但对象仍然可以以有限的方式使用其方法: 183 | 184 | ``` 185 | id utility = [frameworkObject anonymousUtility]; 186 | ``` 187 | 188 | 例如:你正在编写一个使用Core Data框架的iOS应用程序,你可能会遇到`NSFetchedResultsController`类。 该类旨在帮助数据源对象将存储的数据提供给iOS `UITableView`,从而提供诸如行数等信息。 189 | 190 | 如果您使用将内容分割为多个部分的表视图,您还可以向提取的结果控制器询问相关的部分信息。 `NSFetchedResultsController`类不会返回包含此节信息的特定类,而是返回一个符合`NSFetchedResultsSectionInfo`协议的匿名对象。 于是我们仍然可以通过如下方法查询对象所需的信息,例如节中的行数: 191 | 192 | ``` 193 | NSInteger sectionNumber = ... 194 | id sectionInfo = 195 | [self.fetchedResultsController.sections objectAtIndex:sectionNumber]; 196 | NSInteger numberOfRowsInSection = [sectionInfo numberOfObjects]; 197 | ``` 198 | 199 | >即使你不知道`sectionInfo`对象所属的类,`NSFetchedResultsSectionInfo`协议也使它可以响应`numberOfObjects`消息。 200 | 201 | ##总结(译者注) 202 | 本章讲解了协议的概念,其包含正式与非正式协议。我们可以通过在`@protocol`部分方法名来定义一组正式协议(在@optional后定义非正式协议)。在`@interface`声明中的类名之后列出用尖括号括起来的协议名称,对象即可采用这些协议。采用协议之后,对象便承诺了要实现协议中每一个必须实现的方法(正式协议的内容)。若没有实现所承诺的方法,编译器会对此发出警告。 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | -------------------------------------------------------------------------------- /7.值与集合类型Values-and-Collections.md: -------------------------------------------------------------------------------- 1 | # Values-and-Collections-值与集合类型 2 | ##简述 3 | Objective-C是一门面向对象的编程语言(C的一种超集),我们可以在Objective-C代码中使用标准C的纯量(非对象)类型,例如`int`,`float`和`char`。以及在Cocoa和Cocoa Touch应用程序中的其他纯量类型,例如:`NSInteger`,`NSUInteger`和`CGFloat`等,这些纯量类型在不同的目标体系结构(系统结构)中有不同的定义。 4 | 5 | 纯量类型用于您不需要使用对象来表示值(或相关的开销)的情况。虽然字符串通常表示为`NSString`类的实例,但数字值通常存储在纯量的局部变量或属性中。 6 | 7 | 我们可以在Objective-C中声明一个C风格的数组,但你会发现Cocoa和Cocoa Touch应用程序中的集合通常使用`NSArray`或`NSDictionary`类的实例来表示。这些类只能用于收集Objective-C对象,这意味着您需要创建类`NSValue`,`NSNumber`或`NSString`的实例,以便在您可以将它们添加到集合之前表示其值。 8 | 9 | 指南前面的章节经常使用NSString类及其初始化和类工厂方法,以及Objective-C` @“string”`文字,它提供了一个简洁的语法来创建一个`NSString`实例。本章将介绍如何使用方法调用或通过Objective-C值文字语法创建`NSValue`和`NSNumber`对象。 10 | 11 | ##在Objective-C中可使用基本C类型 12 | 在Objective-C中可用标准C纯量类型: 13 | 14 | ``` 15 | int someInteger = 42; 16 | float someFloatingPointNumber = 3.1415; 17 | double someDoublePrecisionFloatingPointNumber = 6.02214199e23; 18 | ``` 19 | 20 | 以及标准C运算符: 21 | 22 | ``` 23 | int someInteger = 42; 24 | someInteger++; // someInteger == 43 25 | int anotherInteger = 64; 26 | anotherInteger--; // anotherInteger == 63 27 | anotherInteger *= 2; // anotherInteger == 126 28 | ``` 29 | 30 | 如果你要使用一个纯量类型的Objective-C属性,如下所示: 31 | 32 | ``` 33 | @interface XYZCalculator : NSObject 34 | @property double currentValue; 35 | @end 36 | ``` 37 | 38 | 在通过点语法访问值时,也可以在其属性上使用标准C运算符,如下所示: 39 | 40 | ``` 41 | @implementation XYZCalculator 42 | - (void)increment { 43 | self.currentValue++; 44 | } 45 | - (void)decrement { 46 | self.currentValue--; 47 | } 48 | - (void)multiplyBy:(double)factor { 49 | self.currentValue *= factor; 50 | } 51 | @end 52 | ``` 53 | 54 | 点`·`语法是一个关于访问器方法调用的句法包装,因此本示例中的每个操作都等同于先使用`get accessor`方法获取值,然后执行操作,最后使用`set accessor`方法将所得的值设置为结果。 55 | 56 | ###Objective-C定义的附加基本类型 57 | 58 | 在Objective-C中定义`BOOL`纯量类型,用于保存布尔值,即`YES`或`NO`。`YES`在逻辑上等于`true`和`1`,而`NO`等于`false`和`0`。 59 | 60 | Cocoa和Cocoa Touch对象的许多参数也使用特殊的纯量数值类型,例如`NSInteger`或`CGFloat`。 61 | 62 | 例如: `NSTableViewDataSource`和`UITableViewDataSource`协议(在上一章中描述)都具有请求显示行数的方法: 63 | 64 | ``` 65 | @protocol NSTableViewDataSource 66 | - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView; 67 | ... 68 | @end 69 | ``` 70 | 71 | 这些类型,例如`NSInteger`和`NSUInteger`,在不同系统结构中有不同的定义。 当为32位环境(例如iOS)时,它们分别是32位有符号整数、无符号整数; 当构建用于64位环境(例如现代Mac OS)时,它们分别是64位有符号整数、无符号整数。 72 | >译者注:现阶段iOS环境也为64位。 73 | 74 | 如果您可能跨过API边界(包括内部和导出的API)传递值(比如应用程序代码和框架之间的方法或函数调用中的参数或返回值)请尽量使用这些平台特定的类型。 75 | 76 | 对于局部变量(例如循环中的计数器),如果您知道该值在标准限制范围内,则可以使用基本C类型。 77 | 78 | ###通过C结构保存原始值 79 | 一些Cocoa和Cocoa Touch API使用C结构来保存它们的值。 例如:可以向字符串对象请求子字符串的范围,如下所示: 80 | 81 | ``` 82 | NSString *mainString = @"This is a long string"; 83 | NSRange substringRange = [mainString rangeOfString:@"long"]; 84 | ``` 85 | 86 | `NSRange`结构保存位置和长度。 在这种情况下,`substringRange`将保留一个范围{10,4} 表示@“long”是`mainString`中基于0的索引在10处的字符,@“long”长度为4个字符 。 87 | 88 | 同样,如果你需要编写自定义绘图代码,你需要与Quartz进行交互,这需要基于CGFloat数据类型的结构,例如OS X上的`NSPoint`和`NSSize`,iOS上的`CGPoint`和`CGSize`。 同样,对于不同系统架构,`CGFloat`有不同的定义。 89 | 90 | 有关Quartz 2D绘图引擎的更多信息,请参阅[Quartz 2D编程指南](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html#//apple_ref/doc/uid/TP30001066)。 91 | 92 | ##对象可以表示原始值 93 | 94 | 如果需要将纯量值表示为对象(例如:在使用下一节中描述的集合类)时,可以使用Cocoa和Cocoa Touch提供的基本值类型之一。 95 | 96 | ###用NSString类的实例表示字符串 97 | 98 | 正如你在前面的章节中看到的,`NSString`可以用于表示一串字符,如"Hello World"。 有多种方法来创建`NSString`对象,包括标准分配和初始化、类工厂方法、赋值语法: 99 | 100 | ``` 101 | NSString *firstString = [[NSString alloc] initWithCString:"Hello World!" encoding:NSUTF8StringEncoding]; 102 | NSString *secondString = [NSString stringWithCString:"Hello World!" encoding:NSUTF8StringEncoding]; 103 | NSString *thirdString = @"Hello World!"; 104 | ``` 105 | 106 | 这些示例中的每一个都有效地完成相同的事情 -> 创建表示所提供的一串字符的字符串对象。 107 | 108 | 基本的`NSString`类是不可变的,这意味着其内容在创建时设定,以后不能更改。 如果需要表示不同的字符串,则必须创建一个新的字符串对象,如下所示: 109 | 110 | ``` 111 | NSString *name = @"John"; 112 | name = [name stringByAppendingString:@"ny"]; // returns a new string object 113 | ``` 114 | 115 | `NSMutableString`类是`NSString`的可变子类,允许您在运行时使用像`appendString:`或`appendFormat:`等方法更改其字符内容,如下所示: 116 | 117 | ``` 118 | NSMutableString *name = [NSMutableString stringWithString:@"John"]; 119 | [name appendString:@"ny"]; // same object, but now represents "Johnny" 120 | ``` 121 | 122 | ####格式字符串用于从其他对象或值创建字符串 123 | 124 | 使用一个格式字符串构建一个包含变量值的字符串。 这允许您使用格式说明符指示如何插入值: 125 | 126 | ``` 127 | int magicNumber = ... 128 | NSString *magicString = [NSString stringWithFormat:@"The magic number is %i", magicNumber]; 129 | ``` 130 | 131 | 可用的格式说明符在[字符串格式说明符](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html#//apple_ref/doc/uid/TP40004265)中已有描述。 有关字符串的更多信息,请参阅[字符串编程指南](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Strings/introStrings.html#//apple_ref/doc/uid/10000035i)。 132 | 133 | ###用NSNumber类的实例表示数字 134 | 135 | `NSNumber`类用于表示任何基本C纯量类型,包括`char`,`double`,`float`,`int`,`long`,`short`和每个纯量类型的无符号变体,以及Objective-C布尔类型`BOOL`。 136 | 137 | 与`NSString`一样,您有各种选项来创建`NSNumber`实例,包括分配和初始化或类工厂方法: 138 | 139 | ``` 140 | NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42]; 141 | NSNumber *unsignedNumber = [[NSNumber alloc] initWithUnsignedInt:42u]; 142 | NSNumber *longNumber = [[NSNumber alloc] initWithLong:42l]; 143 | NSNumber *boolNumber = [[NSNumber alloc] initWithBOOL:YES]; 144 | NSNumber *simpleFloat = [NSNumber numberWithFloat:3.14f]; 145 | NSNumber *betterDouble = [NSNumber numberWithDouble:3.1415926535]; 146 | NSNumber *someChar = [NSNumber numberWithChar:'T']; 147 | ``` 148 | 149 | 它也可以使用Objective-C赋值语法创建`NSNumber`实例: 150 | 151 | ``` 152 | NSNumber *magicNumber = @42; 153 | NSNumber *unsignedNumber = @42u; 154 | NSNumber *longNumber = @42l; 155 | NSNumber *boolNumber = @YES; 156 | NSNumber *simpleFloat = @3.14f; 157 | NSNumber *betterDouble = @3.1415926535; 158 | NSNumber *someChar = @'T'; 159 | ``` 160 | 161 | 这些例子等同于使用`NSNumber`类的工厂方法。 162 | 一旦创建了`NSNumber`实例,就可以使用一个访问器方法请求纯量值: 163 | 164 | ``` 165 | int scalarMagic = [magicNumber intValue]; 166 | unsigned int scalarUnsigned = [unsignedNumber unsignedIntValue]; 167 | long scalarLong = [longNumber longValue]; 168 | BOOL scalarBool = [boolNumber boolValue]; 169 | float scalarSimpleFloat = [simpleFloat floatValue]; 170 | double scalarBetterDouble = [betterDouble doubleValue]; 171 | char scalarChar = [someChar charValue]; 172 | ``` 173 | 174 | `NSNumber`类还提供了使用其他Objective-C基本类型的方法。 例如:如果需要创建纯量`NSInteger`和`NSUInteger`类型的对象表示,正确的方法如下: 175 | 176 | ``` 177 | SInteger anInteger = 64; 178 | NSUInteger anUnsignedInteger = 100; 179 | NSNumber *firstInteger = [[NSNumber alloc]initWithInteger:anInteger]; 180 | NSNumber *secondInteger = [NSNumbernumberWithUnsignedInteger:anUnsignedInteger]; 181 | NSInteger integerCheck = [firstInteger integerValue]; 182 | NSUInteger unsignedCheck = [secondInteger unsignedIntegerValue]; 183 | ``` 184 | 185 | 所有`NSNumber`实例都是不可变的,并且没有可变子类; 如果你需要一个不同的数值,只能创建并使用另一个`NSNumber`实例。 186 | 187 | >**注意:`NSNumber`实际上是一个类集群。 这意味着当您在运行时创建实例时,您将获得一个合适的具体子类来保存所提供的值。 只是将创建的对象作为`NSNumber`的实例。** 188 | 189 | ###使用NSValue类的实例表示其他值 190 | `NSNumber`类本身是基本`NSValue`类的子类,它提供了围绕单个值或数据项的对象包装器。 除了基本的C纯量类型,`NSValue`也可以用于表示指针和结构体。 191 | 192 | `NSValue`类提供了各种工厂方法来创建具有给定标准结构的值,这使得我们可以轻松地创建一个实例。例如:`NSRange`,就像本章前面的示例一样: 193 | 194 | ``` 195 | NSString *mainString = @"This is a long string"; 196 | NSRange substringRange = [mainString rangeOfString:@"long"]; 197 | NSValue *rangeValue = [NSValue valueWithRange:substringRange]; 198 | ``` 199 | 200 | 也可以创建`NSValue`对象来表示自定义结构。 如果您特别需要使用C结构体(而不是Objective-C对象)来存储信息,如下所示: 201 | 202 | ``` 203 | typedef struct { 204 | int i; 205 | float f; 206 | } MyIntegerFloatStruct; 207 | ``` 208 | 209 | 您可以通过提供结构体的指针以及编码的Objective-C类型来创建`NSValue`实例。( `@encode()`编译器指令用于创建正确的Objective-C类型)如下所示: 210 | 211 | ``` 212 | struct MyIntegerFloatStruct aStruct; 213 | aStruct.i = 42; 214 | aStruct.f = 3.14; 215 | 216 | NSValue *structValue = [NSValue value:&aStructwithObjCType:@encode(MyIntegerFloatStruct)]; 217 | ``` 218 | 标准C引用运算符(&)用于为`value`参数提供`aStruct`的地址。 219 | 220 | ##大多数集合都是对象 221 | 222 | 虽然我们可以使用C数组来保存纯量值的集合,甚至是对象指针,但Objective-C代码中的大多数集合都是Cocoa和Cocoa Touch集合类的实例,如`NSArray`,`NSSet`和`NSDictionary`。 223 | 224 | 这些类用于管理对象组,因此您添加到集合的任何项目必须是Objective-C类的实例。如果需要添加纯量值,则必须首先创建一个合适的`NSNumber`或`NSValue`实例来表示它。 225 | 226 | 集合类使用强引用来跟踪其内容,而不是以某种方式维护每个集合对象的单独的副本。这意味着,您添加到集合的所有对象均将保持活动状态,至少与集合保持活动状态的时间相当。例如:[通过所有权和责任管理对象图](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/EncapsulatingData/EncapsulatingData.html#//apple_ref/doc/uid/TP40011210-CH5-SW3)中的描述。 227 | 228 | 除了跟踪其内容之外,每个Cocoa和Cocoa Touch集合类都可以轻松执行某些任务,例如:枚举、访问特定项目或查明特定对象是否是集合的一部分。 229 | 230 | 基本的`NSArray`,`NSSet`和`NSDictionary`类是不可变的,这意味着它们的内容在创建时设置。但同时每个类还拥有一个可变子类,允许您随意添加或删除对象。 231 | 232 | 有关Cocoa和Cocoa Touch中可用的不同集合类的更多信息,请参阅[集合编程(Collections Programming Topics)](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Collections/Collections.html#//apple_ref/doc/uid/10000034i)。 233 | 234 | ###数组是有序集合类 235 | `NSArray`用于表示对象的有序集合类。 其唯一的要求是每个元素都是一个Objective-C对象,即不需要每个对象都是同一个类的实例。 236 | 237 | 为了保持数组中的顺序,每个元素存储在一个从0开始的索引中。如下图所示: 238 | ![数组](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/orderedarrayofobjects.png) 239 | 240 | ####创建数组 241 | 与本章前面描述的纯量值类一样,您可以通过分配和初始化、类工厂方法或赋值语法创建数组。 242 | 243 | 取决于对象的数量,有多种不同的初始化和工厂方法可用: 244 | 245 | ``` 246 | + (id)arrayWithObject:(id)anObject; 247 | + (id)arrayWithObjects:(id)firstObject, ...; 248 | - (id)initWithObjects:(id)firstObject, ...; 249 | ``` 250 | `arrayWithObjects:`和`initWithObjects:`方法都需要一个nil终止作为可变数量的参数,这意味着你必须包括nil作为最后一个值,用法如下所示: 251 | 252 | ``` 253 | NSArray *someArray =[NSArray arrayWithObjects:someObject, someString, someNumber, someValue, nil]; 254 | ``` 255 | 该示例创建一个类似于前面所示的数组(上图)。 第一个对象`someObject`的数组索引为0; 最后一个对象`someValue`的索引为3。 256 | 257 | 若提供的值之一为`nil`,可能会无意中截断对象列表,如下所示: 258 | 259 | ``` 260 | id firstObject = @"someString"; 261 | id secondObject = nil; 262 | id thirdObject = @"anotherString"; 263 | NSArray *someArray = [NSArray arrayWithObjects:firstObject, secondObject, thirdObject, nil]; 264 | ``` 265 | 在这种情况下,`someArray`将只包含`firstObject`,因为`nil secondObject`将被解释为项目列表的结尾。 266 | #####赋值语法创建数组 267 | 也可以使用Objective-C逐个对象创建一个数组,如下所示: 268 | 269 | ``` 270 | NSArray *someArray = @[firstObject, secondObject, thirdObject]; 271 | ``` 272 | 当使用此语法时,不应使用`nil`终止对象列表,实际上nil是无效值。 如果您尝试执行以下代码,将在运行时发生异常,例如: 273 | 274 | ``` 275 | id firstObject = @"someString"; 276 | id secondObject = nil; 277 | NSArray *someArray = @[firstObject, secondObject];// exception: "attempt to insert nil object" 278 | ``` 279 | 如果确实需要在某个集合类中表示一个`nil`值,那么应该使用`NSNull`单例类,详见[用NSNull表示nil](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/FoundationTypesandCollections/FoundationTypesandCollections.html#//apple_ref/doc/uid/TP40011210-CH7-SW34)中所述。 280 | 281 | #####查询数组对象 282 | 在创建数组后,您可以查询其对象的数量或是否包含给定对象等信息,如下所示: 283 | 284 | ``` 285 | NSUInteger numberOfItems = [someArray count]; 286 | if ([someArray containsObject:someString]) { 287 | ... 288 | } 289 | ``` 290 | 您还可以查询给数组定索引处的元素。若查询无效的索引,会在运行时遇到超出范围异常,因此您应该首先检查数组中对象数量,如下所示: 291 | 292 | ``` 293 | if ([someArray count] > 0) { 294 | NSLog(@"First item is: %@", [someArray objectAtIndex:0]); 295 | } 296 | ``` 297 | 此示例检查对象数量是否大于零。 如果是,则它记录第一项的描述(索引0)。 298 | 299 | **下标** 300 | 还有一个下标语法可以替代`objectAtIndex:`,与访问标准C数组中的值方法相似。 前面的例子可以这样重写: 301 | 302 | ``` 303 | if ([someArray count] > 0) { 304 | NSLog(@"First item is: %@", someArray[0]); 305 | } 306 | ``` 307 | #####排序数组对象 308 | `NSArray`类还提供了多种方法来对其收集的对象进行排序。 因为`NSArray`是不可变的,这些方法会返回一个包含排序顺序的项目的新数组。 309 | 310 | 例如,可以通过调用`compare:`对每个字符串排序的结果对字符串数组进行排序,如下所示: 311 | 312 | ``` 313 | NSArray *unsortedStrings = @[@"gammaString", @"alphaString", @"betaString"]; 314 | NSArray *sortedStrings = [unsortedStrings sortedArrayUsingSelector:@selector(compare:)]; 315 | ``` 316 | 317 | #####可变性 318 | 虽然`NSArray`类本身是不可变的,但这对其收集的对象没有影响。 如果你向一个不可变数组添加一个可变字符串,例如: 319 | 320 | ``` 321 | NSMutableString *mutableString = [NSMutableString stringWithString:@"Hello"]; 322 | NSArray *immutableArray = @[mutableString]; 323 | ``` 324 | 325 | 没有什么可以阻止你改变这个字符串: 326 | 327 | ``` 328 | if ([immutableArray count] > 0) { 329 | id string = immutableArray[0]; 330 | if ([string isKindOfClass:[NSMutableString class]]) { 331 | [string appendString:@" World!"]; 332 | } 333 | } 334 | ``` 335 | 336 | 如果需要在初始创建后能够从数组中添加或删除对象,则需要使用`NSMutableArray`,它添加了多种方法来实现添加,删除或替换一个或多个对象: 337 | 338 | ``` 339 | NSMutableArray *mutableArray = [NSMutableArray array]; 340 | [mutableArray addObject:@"gamma"]; 341 | [mutableArray addObject:@"alpha"]; 342 | [mutableArray addObject:@"beta"]; 343 | [mutableArray replaceObjectAtIndex:0 withObject:@"epsilon"]; 344 | ``` 345 | 346 | 此示例创建一个以对象`@“epsilon”、@“alpha”、@“beta”`结尾的数组。可以不创建辅助数组对可变数组进行排序: 347 | 348 | ``` 349 | [mutableArray sortUsingSelector:@selector(caseInsensitiveCompare:)]; 350 | ``` 351 | 352 | 在这种情况下,包含的对象将被排序为不区分大小写的升序,顺序为`@“alpha”、@“beta”、@“epsilon”`。 353 | 354 | ###集合是无序集合类 355 | `NSSet`类似于数组,但维护着一组无序的不同对象,如图所示: 356 | 357 | ![屏幕快照 2016-11-20 02.38.46-w354](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/unorderedsetofobjects.png) 358 | 359 | 因为集合无需保持顺序,所以在测试成员资格时,集合相对数组供了较大的性能改进。 360 | 361 | 基本的`NSSet`类是不可变的,所以它的内容必须在创建时使用分配和初始化或类工厂方法指定,如下所示: 362 | 363 | ``` 364 | NSSet *simpleSet =[NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil]; 365 | ``` 366 | 与`NSArray`一样,`initWithObjects:`和`setWithObjects:`方法都采用`nil`终止可变数量的参数。 可变的`NSSet`子类是`NSMutableSet`。 367 | 368 | 即使您尝试多次添加同一对象,集合仅存储对单个对象的一个引用: 369 | 370 | ``` 371 | NSNumber *number = @42; 372 | NSSet *numberSet =[NSSet setWithObjects:number, number, number, number, nil]; 373 | // numberSet only contains one object 374 | ``` 375 | 有关集合的更多信息,请参阅[集合:无序的对象集合](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Collections/Articles/Sets.html#//apple_ref/doc/uid/20000136)。 376 | 377 | ###字典收集键值对 378 | `NSDictionary`不是简单地维护有序或无序的对象集合,而是将对象存储在给定的键上,然后可以将键用于检索。 379 | 380 | 最好的做法是使用字符串对象作为字典键,如图所示: 381 | ![字典](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Art/dictionaryofobjects.png) 382 | >**注意**:可以使用其他对象作为键,但重要的是要注意每个键均需要被复制供字典使用,因此必须支持`NSCopying`协议。但是,如果您希望能够使用键值编码,则您必须对字典对象使用字符串键。 383 | 384 | ####创建字典 385 | 可以使用分配和初始化或类工厂方法来创建字典,如下所示: 386 | 387 | ``` 388 | NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: 389 | someObject, @"anObject", 390 | @"Hello, World!", @"helloString", 391 | @42, @"magicNumber", 392 | someValue, @"aValue", 393 | nil]; 394 | ``` 395 | 396 | >**注意**:对于`dictionaryWithObjectsAndKeys:`和`initWithObjectsAndKeys:`方法,每个对象应在它的键之前被指定,并且对象和键的列表必须是以nil为终止的。 397 | 398 | #####赋值语法创建字典 399 | Objective-C还提供了字典创建的赋值语法,如下所示: 400 | 401 | ``` 402 | NSDictionary *dictionary = @{ 403 | @"anObject" : someObject, 404 | @"helloString" : @"Hello, World!", 405 | @"magicNumber" : @42, 406 | @"aValue" : someValue 407 | }; 408 | ``` 409 | >**注意**:对于字典字的赋值法创建,键是在其对象之前指定的,并且不以`nil`为终止。 410 | >译者注:与数组的赋值法创建过程类似。 411 | 412 | ####查询字典 413 | 一旦你创建了一个字典,你可以要求它存储对象给定的键,像这样: 414 | 415 | ``` 416 | NSNumber *storedNumber = [dictionary objectForKey:@"magicNumber"]; 417 | ``` 418 | 419 | 如果没有找到对象,`objectForKey:`方法将返回`nil`。 420 | 421 | **下标** 422 | 423 | 还有一个下标语法替代`objectForKey:`,如下所示: 424 | 425 | ``` 426 | NSNumber *storedNumber = dictionary[@"magicNumber"]; 427 | ``` 428 | ####可变性 429 | 如果在创建后需要从字典中添加或删除对象,则需要使用`NSMutableDictionary`子类,如下所示: 430 | 431 | ``` 432 | [dictionary setObject:@"another string" forKey:@"secondString"]; 433 | [dictionary removeObjectForKey:@"anObject"]; 434 | ``` 435 | ###用NSNull表示nil 436 | 因为Objective-C中的`nil`意味着“无对象”,所以不能向本节中描述的集合类添加nil。如果需要在集合中表示“无对象”,可以使用`NSNull`类: 437 | 438 | ``` 439 | NSArray *array = @[ @"string", @42, [NSNull null] ]; 440 | ``` 441 | `NSNull`是一个单例类,这意味着`null`方法将总是返回相同的实例。 这意味着您可以检查数组中的对象是否等于设定的`NSNull`实例: 442 | 443 | ``` 444 | for (id object in array) { 445 | if (object == [NSNull null]) { 446 | NSLog(@"Found a null object"); 447 | } 448 | } 449 | ``` 450 | ##使用集合来保存对象图 451 | `NSArray`和`NSDictionary`类可以很容易地将其内容直接写入磁盘,如下所示: 452 | 453 | ``` 454 | NSURL *fileURL = ... 455 | NSArray *array = @[@"first", @"second", @"third"]; 456 | 457 | BOOL success = [array writeToURL:fileURL atomically:YES]; 458 | if (!success) { 459 | // an error occured... 460 | } 461 | ``` 462 | 如果每个包含的对象是属性列表类型之一(`NSArray`,`NSDictionary`,`NSString`,`NSData`,`NSDate`和`NSNumber`),可以从磁盘重建整个层次结构,像这样: 463 | 464 | ``` 465 | NSURL *fileURL = ... 466 | NSArray *array = [NSArray arrayWithContentsOfURL:fileURL]; 467 | if (!array) { 468 | // an error occurred... 469 | } 470 | ``` 471 | 有关属性列表的更多信息,请参阅[属性列表编程指南](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/PropertyLists/Introduction/Introduction.html#//apple_ref/doc/uid/10000048i)。 472 | 473 | 如果您需要持久保存其他类型的对象,而不仅仅是上面显示的标准属性列表类,您可以使用`archiver`对象(如`NSKeyedArchiver`)来创建收集对象的归档。 474 | 475 | 创建归档的唯一要求是每个对象必须支持`NSCoding`协议。这意味着每个对象必须知道如何将自己编码到归档(通过实现`encodeWithCoder:`方法),并在从现有归档(`initWithCoder:`方法)读取时解码自身。 476 | 477 | `NSArray`,`NSSet`和`NSDictionary`类及其可变子类都支持`NSCoding`,这意味着您可以使用归档器保留对象的复杂层次结构。例如,如果使用`Interface Builder`来布置窗口和视图,当你在可视化界面上创造了nib文件,nib文件就是这些对象层次关系的存档。在运行的时候,nib文件会被解压缩成为一套使用相关类的对象层次关系。 478 | 479 | 有关归档的详细信息,请参见[归档和序列化编程指南](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Archiving/Archiving.html#//apple_ref/doc/uid/10000047i)。 480 | 481 | ##使用最有效的集合枚举技术 482 | Objective-C和Cocoa或Cocoa Touch提供了各种方法来枚举集合的内容。 虽然可以使用传统的C中的`for`循环来遍历内容,如下所示: 483 | 484 | ``` 485 | int count = [array count]; 486 | for (int index = 0; index < count; index++) { 487 | id eachObject = [array objectAtIndex:index]; 488 | ... 489 | } 490 | ``` 491 | 但最好的做法是使用本节中描述的技术之一。 492 | 493 | ###快速枚举枚举集合 494 | 很多集合类符合`NSFastEnumeration`协议,包括`NSArray`,`NSSet`和`NSDictionary`。 这意味着您可以使用快速枚举 - 一个Objective-C语言级的功能。 495 | 496 | 列举数组或集合内容的快速枚举语法如下所示: 497 | 498 | ``` 499 | for ( in ) { 500 | ... 501 | } 502 | ``` 503 | 例如:可以使用快速枚举来记录数组中每个对象的描述,如下所示: 504 | 505 | ``` 506 | for (id eachObject in array) { 507 | NSLog(@"Object: %@", eachObject); 508 | } 509 | ``` 510 | `eachObject`变量在每次通过循环时自动设置为当前对象,因此每个对象都会显示一条日志语句。 511 | 512 | 如果要对字典使用快速枚举,则可以遍历字典键,如下所示: 513 | 514 | ``` 515 | for (NSString *eachKey in dictionary) { 516 | id object = dictionary[eachKey]; 517 | NSLog(@"Object: %@ for key: %@", object, eachKey); 518 | } 519 | ``` 520 | 快速枚举的行为非常类似于标准的C中的`for`循环,因此可以使用`break`关键字来中断迭代,或者`continue`前进到下一个元素。 521 | 522 | 如果要枚举有序集合,枚举应按顺序进行。 对于`NSArray`,这意味着第一次传递将用于索引0处的对象,第二次传递将用于索引1处的对象,以此类推。如果您需要跟踪当前索引,只需对迭代次数进行统计: 523 | 524 | ``` 525 | int index = 0; 526 | for (id eachObject in array) { 527 | NSLog(@"Object at index %i is: %@", index, eachObject); 528 | index++; 529 | } 530 | ``` 531 | ###大多数集合支持枚举器对象 532 | 我们可以通过使用`NSEnumerator`对象枚举许多的Cocoa和Cocoa Touch集合。 533 | 534 | 你可以查询一个`NSArray`,例如,一个`objectEnumerator`或一个`reverseObjectEnumerator`。 它可以对这些对象进行快速枚举,如下所示: 535 | 536 | ``` 537 | for (id eachObject in [array reverseObjectEnumerator]) { 538 | ... 539 | } 540 | ``` 541 | 在这个例子中,循环将以相反的顺序对所收集的对象进行迭代,所以最后一个对象将会第一个输出,依此类推。 542 | 543 | 也可以通过重复调用枚举器的`nextObject`方法来迭代内容,如下所示: 544 | 545 | ``` 546 | id eachObject; 547 | while ( (eachObject = [enumerator nextObject]) ) { 548 | NSLog(@"Current object is: %@", eachObject); 549 | } 550 | ``` 551 | 在本示例中,`while`循环用于将`eachObject`变量设置为每次通过循环的下一个对象。 当没有更多的对象时,`nextObject`方法将返回`nil`,它的值为`false`,循环终止。 552 | 553 | >注意:相等运算符`==`与C赋值运算符`=`混淆是一个常见的程序员错误,如果你在条件分支或循环中为变量赋值,编译器会警告你,如下所示: 554 | 555 | >``` 556 | if (someVariable = YES) { 557 | ... 558 | } 559 | ``` 560 | >如果你确定要重新分配一个变量(整个赋值的逻辑值是`=`左边的最终值),你可以通过把赋值放在括号中来表示,如下所示: 561 | 562 | >``` 563 | if ( (someVariable = YES) ) { 564 | ... 565 | } 566 | ``` 567 | 568 | 与快速枚举一样,枚举时不能改变集合中的对象。使用快速枚举比手动使用枚举器对象更快。 569 | 570 | ###基于块的集合枚举 571 | 我们也可以通过块枚举`NSArray`,`NSSet`和`NSDictionary`等。 块将在下一章中详细介绍。 572 | 573 | 574 | 575 | 576 | -------------------------------------------------------------------------------- /8.使用块Working-with-Blocks.md: -------------------------------------------------------------------------------- 1 | # 使用Blocks 2 | 一个Objective-C类描述一个包含数据和数据相关动作的对象。有时,也仅仅表示一个单一任务或者一些行为的集合,而不是一堆方法的集合。 3 | 4 | *Blocks* 是一种语言级别的特性,可以允许你创造一些特定的代码块,并像一个普通变量一样在让它方法或函数中传递。`Blocks`也是一种OC的对象,也可以添加进入`NSArray`或者`NSDictionary`。`Blocks`同时还有 在他的 *封闭作用域* 内 *捕获* 值的能力,就像其他语言的 *闭包* 或 *lambdas式* 。 5 | 6 | 这个章节描述了声明和引用`block`的语法,并且展示了如何去使用`block`来简化普通的任务,诸如遍历集合,更多信息请参阅[Blocks Programming Topics](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502)。 7 | 8 | ## Blocks基本语法 9 | 用一个*幂乘符号*`^`来定义一个*块*`Block`。 10 | 11 | ``` 12 | ^{ 13 | NSLog(@"This is a block"); 14 | } 15 | ``` 16 | 17 | 和定义一个函数或一个方法类似,用左花括号开始一段代码,并用右花括号结束这段代码。在上面的代码例子里,这个`Block`没有返回任何值,也不接受任何参数。 18 | 19 | 就如同你可以用一个指针去 *引用*(有时也叫做指向) 一个C语言函数,你可以声明一个变量来跟踪一个`Block`,就像这样: 20 | 21 | ``` 22 | void (^simpleBlock)(void); 23 | ``` 24 | 25 | 要是不是很熟悉如何使用C语言函数指针的话,这个语法你就会觉得有点别扭。这个例子声明了一个叫做`simpleBlock`的变量去引用一个不接受任何参数也不返回任何参数的`block`,也就是说,你可以像下面一样把一个符合要求的`block`赋值给它: 26 | 27 | ``` 28 | simpleBlock = ^{ 29 | NSLog(@"This is a block"); 30 | }; 31 | ``` 32 | 这只是一个赋值过程,就像其他变量被赋值一样,分号必须要跟在右花括号的后面。当然你也可以把声明变量和赋值两个步骤合二为一: 33 | 34 | ``` 35 | void (^simpleBlock)(void) = ^{ 36 | NSLog(@"This is a block"); 37 | }; 38 | ``` 39 | 40 | 当你已经声明了一个`block`变量并且把一个`block`赋值给它了,你就可以直接用这个变量来*调用(invoke)*这个block。 41 | 42 | ``` 43 | simpleBlock(); 44 | ``` 45 | 46 | >注意:当你打算调用一个没有赋值的Block变量(指向nil的变量),你的程序就崩溃啦。 47 | 48 | ### 使用接受参数并且能够返回值的Block 49 | 像函数和方法一样,`Blocks`也可以接受一些参数并且有返回值。 50 | 51 | 举个例子,我们先声明一个能够计算两个数乘积的`Block`,很显然,它应该接受两个`double`值和返回一个`double`值: 52 | 53 | ``` 54 | double (^multiplyTwoValues)(double, double); 55 | ``` 56 | 57 | 接下来把`Block`的实现代码赋给这个变量: 58 | 59 | ``` 60 | ^ (double firstValue, double secondValue) { 61 | return firstValue * secondValue; 62 | } 63 | ``` 64 | 65 | `firstValue`和`secondValue`代表你调用这个`block`的的时候传入的两个值,就像一个函数定义一样。在这个例子里,返回值的类型是根据`block`内的代码推测出来的。 66 | 67 | 如果你喜欢,你也可以在`^`和左括号中间精确指定返回值的类型: 68 | 69 | ``` 70 | ^ double (double firstValue, double secondValue) { 71 | return firstValue * secondValue; 72 | } 73 | ``` 74 | 75 | 一旦你已经声明并实现了一个`Block`,你可以像调用函数一样调用它: 76 | 77 | ``` 78 | double (^multiplyTwoValues)(double, double) = 79 | ^(double firstValue, double secondValue) { 80 | return firstValue * secondValue; 81 | }; 82 | 83 | double result = multiplyTwoValues(2,4); 84 | 85 | NSLog(@"The result is %f", result); //打印结果为8 86 | ``` 87 | 88 | ### Blocks可以捕获封闭作用域内的值 89 | 除了包含一些可以执行的代码,一个`block`还能捕获 *封闭作用域* 内的值。 90 | 91 | 如果你在一个方法里面声明了一个`block`,举个例子,这个`block`就可以捕获这个作用域(这里为这个方法内部)的任何量,像这样: 92 | 93 | ``` 94 | - (void)testMethod { 95 | int anInteger = 42; 96 | 97 | void (^testBlock)(void) = ^{ 98 | NSLog(@"Integer is: %i", anInteger); 99 | }; 100 | 101 | testBlock(); 102 | } 103 | ``` 104 | 105 | 在这个例子里,`anInterger`是在`block`外部声明的,但是在这个`block`定义的时候这个值已经被捕获了。 106 | 107 | 除非你特殊声明,只有变量的值被捕获了。这意味着如果你在定义`block`之后和调用它之前改变了这个值,像这样: 108 | 109 | ``` 110 | int anInteger = 42; 111 | 112 | void (^testBlock)(void) = ^{ 113 | NSLog(@"Integer is: %i", anInteger); 114 | }; 115 | 116 | anInteger = 84; 117 | 118 | testBlock(); 119 | ``` 120 | 121 | 已经被捕获的这个值是不会被影响的。这就意味着输出依然还会显示为: 122 | 123 | ``` 124 | Integer is: 42 125 | ``` 126 | 127 | >译者注:这里直接翻译会有些晦涩难懂,你可以把捕获的过程看做拍照片。当你(变量)被捕获的时候,只有你的样子(值)被捕获了,那么当你去整容(改变变量本身)的时候,你回头再看这个照片(调用`block`),照片是不会变的(输出结果不变)。 128 | 129 | 这也意味着,`block`不可能改变原来变量的值,甚至不能改变捕获到的值(因为被捕获的值是被储存为`const`类型的)。 130 | 131 | ####使用__block变量去共享储存 132 | 如果你需要在一个`block`里去改变捕获到的值,你可以在初始变量声明之前添加`__block`修饰。这表示,这个变量存在于一块被**这变量本身存在的作用域**和**任何存在于这个作用域的`block`**两者共享的内存中。 133 | 举个例子,你就可以像这样重写之前的例子: 134 | 135 | ``` 136 | __block int anInteger = 42; 137 | 138 | void (^testBlock)(void) = ^{ 139 | NSLog(@"Integer is: %i", anInteger); 140 | }; 141 | 142 | anInteger = 84; 143 | 144 | testBlock(); 145 | ``` 146 | 147 | 因为`anInterger`是在声明时被`__block`修饰的变量,他的储存空间将与`block`声明共享。这意味着打印的日志将为: 148 | 149 | ``` 150 | Integer is: 84 151 | ``` 152 | 这也就意味着`block`可以改变原来的值,像这样: 153 | 154 | ``` 155 | __block int anInteger = 42; 156 | 157 | void (^testBlock)(void) = ^{ 158 | NSLog(@"Integer is: %i", anInteger); 159 | anInteger = 100; 160 | }; 161 | 162 | testBlock(); 163 | NSLog(@"Value of original variable is now: %i", anInteger); 164 | ``` 165 | 166 | 这次打印的结果将是这样: 167 | 168 | ``` 169 | Integer is: 42 170 | Value of original variable is now: 100 171 | ``` 172 | 173 | ###将Blocks作为参数传递给方法或函数 174 | 在本章中,之前的所有例子都是在定义之后直接调用了一个`block`。在实际应用中,我们经常会把`block`传递给一些方法或函数去在其他地方被 *调用invocation* 。举个例子,你也许会利用 *Grand Central Dispatch* 在后台调用一个`block`。或者,不断地调用一个代表单个重复任务的`block`,比如遍历一个集合。*并发性concurrency* 和 *遍历 enumeration* 将会在本章的之后内容讲到。 175 | 176 | `blocks`有时候也会用于回调——定义一段当某一个任务完成时才会被调用的代码。举个例子,你的应用可能会通过创造一个执行复杂工作的对象来响应用户行为,比如从一个web服务器请求一些信息。因为这个工作可能要花费很长时间,所以你需要在工作进行的时候显示一个进度条,工作完成的时候就把进度条隐藏起来。 177 | 178 | 用 *委托* 是可以实现这个功能的:你需要(在工作中)创建一个合适的*委托协议 delegate protocal* ,并实现要求的方法(如显示和关闭进度条),让你的对象成为这个工作的委托人。 接下来就等待工作结束时你对象中实现的委托方法被调用。 179 | 180 | >注:这里指的是在执行复杂任务的对象A中定义一个委托,对象A在进行复杂任务时中会分别调用委托中的方法,当然这些方法是由你的对象B实现的,也就是说对象A仅决定这些方法调用的时机,而不决定做什么(因为委托给B对象做了),而你的对象B则决定做什么(比如打开或关闭进度条),详情请参考前文使用委托和代理Working with Protocols。 181 | 182 | 但是使用`block`会使这件事情更加简单,因为你可以在你初始化工作的时候才定义这个回调行为,像这样: 183 | 184 | ``` 185 | - (IBAction)fetchRemoteInformation:(id)sender { 186 | [self showProgressIndicator]; 187 | 188 | XYZWebTask *task = ... 189 | 190 | [task beginTaskWithCallbackBlock:^{ 191 | [self hideProgressIndicator]; 192 | }]; 193 | } 194 | ``` 195 | 196 | 这个例子调用了一个方法去显示一个进度条,接着创建了一个任务并且命令它开始执行。这个回调`block`规定了当任务完成的时候执行的代码;在这个情形下,它只调用了一个方法去关闭进度条。我们注意到,为了能够调用`hideProgressIndicator`方法,这个回调`block`捕获了`self`对象。当你捕获`self`的时候,一定要小心,因为你极容易创造了一个 *强引用循环strong reference cycle* ,关于如何避免强循环引用的内容会在之后详细描述。 197 | 198 | 使用`block`可以让你清晰地看出工作前后发生了什么,这提升了代码的可读性,因为你不需要在委托方法中不断跳转去搞清楚什么将要发生。 199 | 200 | 声明一个`beginTaskWithCallbackBlock:`方法这么写: 201 | 202 | ``` 203 | - (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock; 204 | ``` 205 | 206 | ` (void (^)(void))`规定了方法接受的参数是一个不接受任何参数同时也不返回任何参数的`block`. 在这个方法的实现中,你可以像平时调用一个`block`一样去调用这个传入的`block`参数: 207 | 208 | ``` 209 | - (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock { 210 | ... 211 | callbackBlock(); 212 | } 213 | ``` 214 | 215 | 一个接受带参数`block`的方法这么写。 216 | 217 | ``` 218 | - (void)doSomethingWithBlock:(void (^)(double, double))block { 219 | ... 220 | block(21.0, 2.0); 221 | } 222 | ``` 223 | 224 | ####Block应该作为一个方法的最后一个参数 225 | 最好一个方法只接受至多一个`block`参数。如果方法也接受其他不是`block`的参数,`block`应该成为最后一个参数。 226 | 227 | ``` 228 | - (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback; 229 | ``` 230 | 231 | 这样写,当你内嵌形式去定义一个`block`时,会让你的方法调用更加容易看懂: 232 | 233 | ``` 234 | [self beginTaskWithName:@"MyTask" completion:^{ 235 | NSLog(@"The task is complete"); 236 | }]; 237 | ``` 238 | 239 | ###使用类型定义(Type Definition)简化定义Block的语法 240 | 如果你需要定义多个拥有相同signature的`block`,那么就为了这个signature定义一个你自己的类型。 241 | 242 | 举个例子,你可以为一个既不接受数值也不返回数值的`block`定义一个类型,就像这样: 243 | 244 | ``` 245 | typedef void (^XYZSimpleBlock)(void); 246 | ``` 247 | 248 | 你可以使用在创建一个变量或者限定方法接受的参数类型时候使用这个自定义类型: 249 | 250 | ``` 251 | XYZSimpleBlock anotherBlock = ^{ 252 | ... 253 | }; 254 | ``` 255 | 256 | ``` 257 | - (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock { 258 | ... 259 | callbackBlock(); 260 | } 261 | ``` 262 | 263 | 当你使用接受一个`block`或者返回一个`block`的`block`的时候,自定义类型就非常有用了。看看下面这个例子: 264 | 265 | ``` 266 | void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) { 267 | ... 268 | return ^{ 269 | ... 270 | }; 271 | }; 272 | ``` 273 | 274 | 这个`complexBlock`变量引用了一个接受另一个`block`作为参数并且返回另一个`block`的`block`。 275 | 那么用自定义类型来重写这段代码就更容易读懂了: 276 | 277 | ``` 278 | XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) { 279 | ... 280 | return ^{ 281 | ... 282 | }; 283 | }; 284 | ``` 285 | 286 | ###给对象添加block属性 287 | 给对象添加一个`block`属性和定义一个`block`变量非常类似: 288 | 289 | ``` 290 | @interface XYZObject : NSObject 291 | @property (copy) void (^blockProperty)(void); 292 | @end 293 | ``` 294 | 295 | >注意: 你应该给这个属性添加`copy`特征,因为一个`block`需要被复制一份以便于追踪它在原来作用域外的捕获状态。这些都是自动完成的,所以你在使用自动引用计数的时候不需要考虑这些。最好为这种属性添加这个特征,因为从实际行为来看,这样是最好的。获取更多信息请查阅[Blocks Programming Topics](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html#//apple_ref/doc/uid/TP40007502)。 296 | 297 | 一个`block`属性会像其他`block`变量一样被设置或调用。 298 | 299 | ``` 300 | self.blockProperty = ^{ 301 | ... 302 | }; 303 | self.blockProperty(); 304 | ``` 305 | 306 | 也可以使用类型定义来声明一个`block`属性,像这样: 307 | 308 | ``` 309 | typedef void (^XYZSimpleBlock)(void); 310 | 311 | @interface XYZObject : NSObject 312 | @property (copy) XYZSimpleBlock blockProperty; 313 | @end 314 | ``` 315 | 316 | ###当捕获self时避免强引用循环 317 | 如果你需要在`block`中捕获`self`,比如说定义一个回调`block`,你一定要考虑内存管理中可能发生的细节。 318 | 319 | `block`会强引用任何它捕获的对象,包括`self`。这也就意味着它很容易导致强引用循环,举个例子,一个对象拥有一个`copy`特征的`block`属性,这个`block`捕获了`self`: 320 | 321 | ``` 322 | @interface XYZBlockKeeper : NSObject 323 | @property (copy) void (^block)(void); 324 | @end 325 | ``` 326 | 327 | ``` 328 | @implementation XYZBlockKeeper 329 | - (void)configureBlock { 330 | self.block = ^{ 331 | [self doSomething]; // capturing a strong reference to self 332 | // creates a strong reference cycle 333 | }; 334 | } 335 | ... 336 | @end 337 | ``` 338 | 339 | 对于这个简单的例子,编译器会发出警告。但是如果是更加复杂的例子,可能会包含多个对象间的强引用循环,这样编译器就很难诊断出问题。 340 | 341 | 为了避免这个问题,最好是弱引用`self`,像这样: 342 | 343 | ``` 344 | - (void)configureBlock { 345 | XYZBlockKeeper * __weak weakSelf = self; 346 | self.block = ^{ 347 | [weakSelf doSomething]; // 捕获了弱引用 348 | // 避免了引用循环 349 | } 350 | } 351 | ``` 352 | 353 | 因为捕获的是对于`self`的弱引用,这个`block`就不会再持有关于`XYZBlockKeeper`的强引用关系了。如果这个对象在`block`被调用前释放了,`weakSelf`指针会被设置为`nil`。 354 | 355 | ##Block可以简化遍历 356 | 「」,许多Cocoa 和 Cocoa Touch API使用`block`来简化普通任务,比如说遍历集合。举个例子,`NSArray`类提供三个基于`block`的方法,包括: 357 | 358 | ``` 359 | - (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block; 360 | ``` 361 | 362 | 这个方法接受一个`block`参数,这个`block`会为数组中每一个元素都调用一遍: 363 | 364 | ``` 365 | NSArray *array = ... 366 | [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) { 367 | NSLog(@"Object at index %lu is %@", idx, obj); 368 | }]; 369 | ``` 370 | 371 | 这个`block`本身接受三个参数,前两个指向当前元素本身及其在数组中的次序位置。第三个参数是指向一个布尔型变量,你可以使用这个变量来停止遍历,像这样: 372 | 373 | ``` 374 | [array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) { 375 | if (...) { 376 | *stop = YES; 377 | } 378 | }]; 379 | ``` 380 | 381 | 也可以使用`enumerateObjectsWithOptions:usingBlock:`方法自定义枚举。举个例子,通过使用`NSEnumerationReverse`选项可以逆序浏览集合。 382 | 383 | 如果在`block`中的代码是处理器密集型并且并发安全,你可以使用`NSEnumerationConcurrent`选项: 384 | 385 | ``` 386 | [array enumerateObjectsWithOptions:NSEnumerationConcurrent 387 | usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) { 388 | ... 389 | }]; 390 | ``` 391 | 392 | 这个标志表示遍历`block`的调用工作会分发给多个线程执行。如果`block`的代码是处理器密集型,这就提供了潜在的性能提升。要注意,使用这个选项后,遍历的顺序就不一定了。 393 | 394 | `NSDictionary`类也提供了基于`block`的方法,包括: 395 | 396 | ``` 397 | NSDictionary *dictionary = ... 398 | [dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) { 399 | NSLog(@"key: %@, value: %@", key, obj); 400 | }]; 401 | ``` 402 | 403 | 比使用传统循环来遍历每个键值对更加方便。 404 | 405 | ##Block可以简化并发工作 406 | `block`代表一个确定清晰地工作,既包含了可执行的代码,也可能捕获了所在作用域的状态。由于这些特性,`block`在使用OSX和iOS提供的并发选项完成一些异步的调用的场合中,就变得非常理想。你可以简单地 407 | 使用`block`定义你的任务然后让系统在处理器资源可用的时候执行这些任务,而不需要考虑如何使用低级的机制比如说线程。 408 | 409 | OSX和iOS提供多样的并发工作技术,包括两个任务计划机制:*Operation queues* 和 *Grand Central Dispatch*。这些机制都围绕着一个等待着调用的任务队列。为了让你的`block`被调用,把它们加入队列中,然后系统会在CPU时间和资源可用的时候把这些任务从队列中取出以便于调用。 410 | 411 | *串行队列(serial queue)* 同一时间仅允许一个任务执行——前一个任务已经完成之前,队列中下一个任务不会被出队执行。一个*并发队列(concurrent queue)*会执行尽可能多的任务,不会等待之前的任务结束。 412 | 413 | ###在操作队列中使用`block`操作 414 | Cocoa和Cocoa Touch使用`operation queue(操作队列)`来安排任务执行。你创建一个`NSOperation`实例来封装一系列的工作以及必要的数据,接着将这个操作(operation)加入`NSOperationQueue`实例。 415 | 416 | 你既可以创造自定义的`NSOperation`子类实现复杂的任务,也可以使用`NSBlockOperation`创建一个使用`block`的操作,像这样: 417 | 418 | ``` 419 | NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ 420 | ... 421 | }]; 422 | ``` 423 | 424 | 你可以手动执行一个操作,但一般都会将操作加入一个队列,这个队列可以是一个已经存在的队列,也可以是你自己创建的队列,准备好执行: 425 | 426 | ``` 427 | // schedule task on main queue: 428 | NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; 429 | [mainQueue addOperation:operation]; 430 | 431 | // schedule task on background queue: 432 | NSOperationQueue *queue = [[NSOperationQueue alloc] init]; 433 | [queue addOperation:operation]; 434 | ``` 435 | 436 | 如果你使用操作队列,你可以定义操作的优先级和依赖关系,比如说,你可以规定一个操作必须在一系列的其他操作结束之后才能执行。你也可以通过KVO(key-value observing键值观察)监测你操作的状态变化。更多关于操作及操作队列的内容,请看[Operation Queues](https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationObjects/OperationObjects.html#//apple_ref/doc/uid/TP40008091-CH101)。 437 | 438 | ###使用Grand Central Dispatch在调度队列(dispatch queue)上安排block 439 | 如果你需要安排任意的`block`执行,你可以直接使用调度队列,这个调度队列是在GCD控制下的。无论是同步还是异步操作,调度队列都给调用者带来很多便利,同时,这些操作会按照先入先出的顺序执行。 440 | 441 | 你既可以自己创建一个调度队列,也可以使用GCD自动提供的队列。举个例子,你想要安排一个并发执行的任务,你可以使用`dispatch_get_global_queue() `函数获得一个已经存在的调度队列的引用,同时定义队列的优先级,像这样: 442 | 443 | ``` 444 | dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 445 | ``` 446 | 447 | 谈及队列上调度`block`,你既可以使用`dispatch_async()`也可以使用`dispatch_sync()`。`dispatch_async()`函数会立刻返回,不会等待`block`被调用。 448 | 449 | ``` 450 | dispatch_async(queue, ^{ 451 | NSLog(@"Block for asynchronous execution"); 452 | }); 453 | ``` 454 | 455 | `dispatch_sync()`不会立刻返回,它会等到`block`结束执行后再返回。举个例子,在当一个并发执行的`block`需要等待另一个处于主线程的任务结束后才能继续执行的情况下,就可以用这个函数了。 456 | 457 | 更多关于GCD和调度队列的内容,请看[Dispatch Queues](https://developer.apple.com/library/content/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html#//apple_ref/doc/uid/TP40008091-CH102)。 458 | 459 | 460 | 461 | 462 | -------------------------------------------------------------------------------- /9.错误处理Dealing-with-Errors.md: -------------------------------------------------------------------------------- 1 | # 错误处理 2 | 3 | 几乎每一款应用都会遇到错误。其中一些错误会超出你的控制,比如用光了磁盘空间或丢失了网络连接。其中一些错误是可以恢复的,如无效的用户输入。并且,虽然所有的开发人员都在努力追求完美,但程序员偶尔也会出现错误。 4 | 5 | 如果你是来自其他平台和语言的开发者,你过去常常可能使用*异常(exceptions)*来处理大多数的错误。当你使用 *Objective-C* 编写代码时,*异常(exceptions)*仅针对于程序员的错误,如数组的访问越界或无效的方法参数。这些问题是在你应用程序启动之前,在进行测试的时候就应该找到并修复的。 6 | 7 | 所有其他错误都是`NSError`类实例化的表现。本章给出了使用`NSError`对象的简要介绍,包括如何使用框架的方法可能会失败并返回错误。需要更多信息,请参见[Error Handling Programming Guide]() 8 | 9 | ## 使用NSError处理大多数错误 10 | 11 | 错误是许多应用程序生命周期中不可避免的一部分。如果你需要从*远程Web服务器*请求数据,可能会出现各种各样的问题,比如: 12 | 13 | * 没有网络连接 14 | * 远程服务器可能无法访问 15 | * 远程服务器可能无法返回你请求的信息 16 | * 你收到的数据可能不符合你的期望 17 | 18 | 遗憾的是,不可能为每一个可能遇见的问题做应急预案和解决方案。取而代之的是,你必须为错误作出计划并且知道怎样处理它们,并带来最好的用户体验。 19 | 20 | ### 一些代理方法会提示你错误 21 | 如果你正在实现一个*框架类(framework class)*的*代理对象(delegate object)*用于执行某个特定的任务,如从远程Web服务器中下载信息,则通常会发现你至少需要实现一个与错误相关的方法。例如,`NSURLConnectionDelegate`协议,包括一个`connection:didFailWithError:`方法: 22 | 23 | ``` 24 | - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error; 25 | ``` 26 | 27 | 如果出现一个错误,这个代理方法会被调用并给你提供一个`NSError`对象来描述问题。 28 | 29 | 一个`NSError`对象包含一个*数字错误代码(numeric error code)*,*域(domain)*和*描述(description)*,以及被封装在一个*用户信息字典(user info dictionary)*中的其他相关信息。 30 | 31 | 相较于要求每一个可能的错误都有一个独特的数字代码,*Cocoa和Cocoa Touch*的错误被分为域(domins)。比如,如果发生一个`NSURLConnection`的错误,这个`connection:didFailWithError: `方法将提供来自`NSURLErrorDomain`的一个错误。 32 | 33 | 这个错误对象还包含一个*本地化描述(localized description)*,如“找不到指定的服务器”。 34 | 35 | ### 一些方法通过参数传递错误 36 | 一些*Cocoa*和*Cocoa Touch*的*接口(API)*通过参数传回错误。例如,你可能决定使用`NSData`的方法`writeToURL:options:error:`,将你从Web服务器接收到的数据通过写入磁盘保存起来。这个方法的最后一个参数是对`NSError`指针的引用: 37 | 38 | ``` 39 | - (BOOL)writeToURL:(NSURL *)aURL 40 | options:(NSDataWritingOptions)mask 41 | error:(NSError **)errorPtr; 42 | ``` 43 | 44 | 在你调用此方法之前,你需要创建一个合适的指针,以便你可以传入它的地址: 45 | 46 | ``` 47 | NSError *anyError; 48 | BOOL success = [receivedData writeToURL:someLocalFileURL 49 | options:0 50 | error:&anyError]; 51 | if (!success) { 52 | NSLog(@"Write failed with error: %@", anyError); 53 | // present error to user 54 | } 55 | ``` 56 | 57 | 如果出现了错误,这个` writeToURL:...`方法将会返回`NO`,并更新你的`anyError`指针指向错误的对象来描述问题。 58 | 59 | 当处理参数传递的错误时,通过测试方法的返回值来查看是否发生了错误非常重要,如上面所示。不要仅仅只是测试查看这个错误指针是否被设置为指向了一个错误。 60 | 61 | 提示:如果你对错误对象不敢兴趣,只需要给`error:`参数传`NULL`就行了。 62 | 63 | ### 如果可能的话尽量恢复或向用户显示错误 64 | 65 | 最好的用户体验就是让你的应用程序显而易见地从错误中恢复。如果你正在制作一个远程Web请求,比如,你可能会尝试使用不同的服务器重新发送请求。或者你可能需要在再次请求之前从用户获取更多信息,比如有效的用户名或密码凭据。 66 | 67 | 如果它不可能从错误中恢复,你应该提醒用户。如果你正在使用`Cocoa Touch`为`iOS`开发,你需要创建和配置一个`UIAlertView`来显示错误。如果你在使用`Cocoa`为`OS X(mac OS)`开发,你可以在任何`NSResponder`对象(如视图、窗口或应用程序对象本身)调用`presentError:`并且错误会随`响应链(responder chain)`传递以寻求更多配置或恢复。当它到达应用程序对象时,应用程序通过一个警告面板向用户呈现错误。 68 | 69 | 更多有关向用户展现错误的信息,请参见[ Displaying Information From Error Objects]()。 70 | 71 | ### 创建你自己的错误对象 72 | 73 | 为了创建自己的`NSError`对象,你需要定义你自己的错误域(domain),它应该是这样的形式: 74 | 75 | ``` 76 | com.companyName.appOrFrameworkName.ErrorDomain 77 | ``` 78 | 79 | 你还需要为每一个可能发生在你域(domain)中的错误选择一个唯一的`错误代码`,以及一个合适的描述,它被存储在`用户信息字典(user info dictionary)`中,像这样: 80 | 81 | ``` 82 | NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain"; 83 | NSString *desc = NSLocalizedString(@"Unable to…", @""); 84 | NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : desc }; 85 | 86 | NSError *error = [NSError errorWithDomain:domain 87 | code:-101 88 | userInfo:userInfo]; 89 | ``` 90 | 91 | 这个示例使用`NSLocalizedString`函数从一个`Localizable.strings`文件来查找错误描述的本地化版本,作为*本地化字符串资源(Localizing String Resources)*的描述。 92 | 93 | 如果你需要通过参数传回一个错误作为早期描述,你的*方法签名(method signature)*应该包含一个指向`NSError`对象的指针作为参数。你也应该使用返回值来表示成功或者失败,如: 94 | 95 | ``` 96 | - (BOOL)doSomethingThatMayGenerateAnError:(NSError **)errorPtr; 97 | ``` 98 | 99 | 如果出现错误,在你尝试引用它来设置错误之前,你首先应该检查是否给错误指针提供了一个非空指针,在返回`NO`来表示失败之前,像这样: 100 | 101 | ``` 102 | - (BOOL)doSomethingThatMayGenerateAnError:(NSError **)errorPtr { 103 | ... 104 | // error occurred 105 | if (errorPtr) { 106 | *errorPtr = [NSError errorWithDomain:... 107 | code:... 108 | userInfo:...]; 109 | } 110 | return NO; 111 | } 112 | ``` 113 | 114 | ## 异常用来处理程序员的错误 115 | `Objective-C`支持异常(exceptions),有和其他许多编程语言有相同的用法,和Java或C++有相似的语法。和`NSError`一样,在`Cocoa`和`Cocoa Touch`中的异常都是对象,通过`NSException`类的实例来表示。 116 | 117 | 如果你需要编写可能引发异常的代码,你可以将该代码封装在一个`try-catch`*代码块(block)*中: 118 | 119 | ``` 120 | @try { 121 | // do something that might throw an exception 122 | } 123 | @catch (NSException *exception) { 124 | // deal with the exception 125 | } 126 | @finally { 127 | // optional block of clean-up code 128 | // executed whether or not an exception occurred 129 | } 130 | ``` 131 | 132 | 如果一个异常被在`@try`代码块中的代码抛出,它将会被`@catch`代码块捕获,你便可以处理它。比如,如果你在使用用异常(exceptions)来处理错误的底层C++库,你可能会捕获到异常并创建合适的`NSError`对象来向用户显示。 133 | 134 | 如果一个异常被抛出但没有被捕获,默认的没有捕获异常处理会记录信息到控制台并终止程序。 135 | 136 | 你不应该在标准程序检查Objective-C方法的地方使用`try-catch`代码块。以一个`NSArray`为例,你应该在试图访问指定下标的对象之前,总是检查数组的`count`来确定元素的数量。如果你做了一个越界的访问请求,`objectAtIndex:`方法会抛出一个异常,在开发周期的早期你就可以找到在你代码中的*漏洞(bug)*,你在给用户的应用中应该避免抛出异常。 137 | 138 | 想了解更多关于在Objective-C程序中异常的信息,请参考[Exception Programming Topics]()。 139 | 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Programming-with-Objective-C-in-Chinese 2 | 3 | This is a Chinese version of Programming with Objective-C that is translated by Innovation Studio iOS Group in USETC. 苹果开发者平台Objective-C文档的中文翻译版本。 4 | ##贡献者: 5 | - Chibaibuki XiangfuGoh 6 | - Lilith FengChu 7 | - Caoxian HaoxianChan 8 | - Bomlsy SiyuLiao 9 | - tinoryj YanjingRen 10 | - yaoyai JianyunWu 11 | - toryznoco 12 | 13 | ##测试版本V0.1 14 | 本项目欢迎提出意见,如对任何章节有意见或疑问请发送邮件至chibaibuki@outlook.com,我们会在两个工日内对问题进行回复并可能修改相应章节。 15 | ##章节认领情况 16 | [原文地址](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html#//apple_ref/doc/uid/TP40011210-CH1-SW1) 17 | 18 | | 章节 | 认领者 | 19 | | --- | --- | 20 | | Introduction-介绍 | yaoyai| 21 | | Defining-Classes-定义类|Caoxian| 22 | |Working-with-Objects-对象的使用|Bomlsy| 23 | |Encapsulating-Data-数据的封装|Lilith| 24 | |Customizing-Existing-Classes-定制已有的类|Caoxian| 25 | |Working-with-Protocols-协议的使用|tinoryj| 26 | |Values-and-Collections-值与集合类型|tinoryj| 27 | |Working-with-Blocks-使用块|Chibaibuki| 28 | |Dealing-with-Errors-错误处理|toryznoco| 29 | |Conventions-命名规则|Bomlsy| 30 | 31 | ##流程 32 | 33 | 1. 章节认领,共10章 34 | - Introduction-介绍 35 | 2. Defining-Classes-定义类 36 | 3. Working-with-Objects-对象的使用 37 | 4. Encapsulating-Data-数据的封装 38 | 5. Customizing-Existing-Classes-定制已有的类 39 | 6. Working-with-Protocols-协议的使用 40 | 7. Values-and-Collections-值与集合类型 41 | 8. Working-with-Blocks-使用块 42 | 9. Dealing-with-Errors-错误处理 43 | 10. Conventions-命名规则 44 | 2. 各自`Fork`[Github-原工程](https://github.com/L1l1thLY/Programming-with-Objective-C-in-Chinese) 到自己的Github上并建立`dev`分支。将润色排版好的章节推送至原工程`dev`分支。 45 | 3. 其他翻译者校对审核原工程dev分支并进行修改,无误后推送至原工程`dev`分支。 46 | 4. 将`dev`分支合并进入`master`分支。 47 | 48 | ##排版约定 49 | 本文档使用`Markdown`书写,排版约定如下。 50 | 51 | - 普通字体:用于书写正文。如: 52 | 因为这是一个赋值操作而并非一个函数定义的过程,所以语句应由一个分号结尾。 53 | 54 | - *斜体*:用于指示第一次出现的术语、E-mail地址、文件名。文件扩展名、路径名、目录、以及Unix工具。对于较为晦涩的术语,**翻译者应加括号并简单解释**。如: 55 | *Objective-C*是你写*OS X(maxOS)程序*和*iOS*程序的首选语言。它作为C语言的一个*超集*(即C语言是它的一部分)提供了面对对象的特性和*动态运行时runtime*。 56 | 57 | 58 | - `等宽字体`:用于指示命令、命令选项、命令开关、变量、属性、键值、函数、类型、类、名字空间、方法、模块、成员属性、参数、值、对象、事件、事件处理器、XML标签、HTML标签、宏、文件内容或命令的输出结果。如: 59 | 60 | `Blocks`是一种语言级别的特性,可以允许你创造一些特定的代码块,并像一个普通变量一样在让它方法或函数中传递。Blocks也是一种OC的对象,也可以添加进入`NSArray`或者`NSDictionary`。 61 | 62 | ``` 63 | myBlocksName1 = ^{ 64 | NSlog(@"I don't take any arguments and don't return any value."); 65 | } 66 | ``` 67 | - **粗体**:用于强调;用于显示命令或其他应该由用户逐字输入的文本。 68 | 69 | - 标题尺寸:对于文章中大标题使用`#`一级标题,其他则根据情况使用二级标题`##`、三级标题`###`或更小。 70 | 71 | --------------------------------------------------------------------------------