第一条 Object-C语言的起源
- oc是面向对象语言
- 虽然oc是面向对象的,但是对比java c++这类面向对象的语言还是略有差别:
1>java c++的面向对象语言使用的是“函数调用”
2>而oc的面向对象语言使用的是“消息结构” - oc的消息型语言是从Smalltalk演化而来的,Smalltalk是消息型语言的鼻祖
- 消息结构和函数调用在代码上的区别:
//这是oc的消息结构
NSObject *obj = [[NSObject alloc] init];
[obj perform:parm1 parm:parm2];
//这是c++的函数调用
Object *obg = new Object;
obj->perform(parm1, parm2);
上述的就是写的语法上的区别,一个是通过:,:来的,另一个是直接最后括号里作为参数传过去的 - 消息结构的语言:其运行时执行的代码由运行环境来决定(正是这个特性才有我们平时说的runtime)
- 函数调用的语言:其运行时执行的代码由编译器决定(在编译时期就决定了执行的代码。)
- 总结5,6,oc在编译时期并不决定时期执行的代码,比如歌函数只有声明,没有具体实现在oc中就可以变易通过,只有在运行时如果运行环境发现没有具体实现的时候才会报错。而函数调用语言如果只声明没有具体实现,在编译时期就会报错。
- 多态:疑问,如果4中的c++代码,是个多态的话怎么办,不是说c++这类语言是在编译的时候就要决定调用那个实现吗,这个多态怎么办,答案是c++这类代码,如果是多态的话,比较特殊,也是在运行时起决定的,在运行时期通过“虚方法表”来查出到底该执行那个函数的实现。然而对于oc这类消息型语言,不管你是不是多态我都是在运行时期才去查找要执行的方法,这就是为什么咱们7中所说的如果只声明不实现在编译时期不会报错的原因
- Object-C是为c语言添加了面向对象特性的c语言的超集的语言
第二条 在类的头文件中尽量少引入其他头文件
-
什么时候使用#import什么时候@class(向前声明)
如上图截图所示,我在person类中的头文件需要加入一个employer的类的属性:这时候在编译的时候,我们只需要知道有一个类名叫做YTEmployer就行了,不需要知道YTEmployer里面的具体实现细节,所以上面的#import "YTEmployer.h"就显得又点引入的太多了,所以优雅的做法应该是只声明一下该类就行了,即使用@class "YTEmployer.h"就行了,这种使用@class的行为叫做“向前声明”。
ps:所以为了提高编译时的效率,尽量能不在.h中使用全部引入的#import就少使用,对于具体细节的实现放在.m中在导入
ps:也就是将引入头文件的时机尽量延后,只在确有需要的时候才引入 -
1中的介绍我们知道,@class增加了编译的效率,但是@class相比于#import还有一个好处,请看下面如果使用#import的话:
在YTPerson中需要知道雇员的属性,导入了YTEmployer.h
在YTEmployer.h中需要有一个增加雇员的方法,导入了YTPerson.h
如上述这样怎么办,这个时候你去编译的时候,如果编译YTPerson需要引入YTEmployer.h,如果编译YTEmployer.h需要引入YTPerson,所以假如我们使用了上述的#import分别进行导入,这样在解析其中一个头文件的时候编译器发现他引入了另一个头文件,然后去编译另一个发现另一个也引入这个头文件,这样虽不像使用#include能造成死循环,但是这样的使用会使这两个文件永远都有一个无法正确编译,通过上面的截图你也能看出来 ,总有一个会报错。
so,此时我们就使用@class就会避免这种循环引用的问题,如下:
这样就没问题。你看到截图中两个我都用了@class。其实如果仅仅是为了编译能通过,其中一个使用@class就行了,就不会循环引用了,但是学习了1,我们要尽量延后引入,所以两个都应该使用@class
-
学习了1,2我们知道在.h中尽量使用@class,而不是@import,但是有没有在.h中必须要要引入头文件的呢?答案是:有,例如:
1》情况1:自定义的这个类继承自父类,.h中必须引入,这个毋庸置疑(YTSonPerson 继承自YTPerson)
2》情况2:如果我们这个类要遵从某个协议,也必须使用#import,使用@class只能知道说有这个名字的协议存在,但是却不知道这个协议里面有什么方法,但是我们的编译器在此时是需要知道这个协议有什么方法的:
YTEmployer中定义了协议:
YTPerson中要遵从这个协议:
所以此时我们就也必须引入头文件使用#import。
但是对于情况2,我们也就是只是必须知道YTEmployer协议中的方法,对于YTEmployer其他全局具体内容也是不必知道的,我们就使用#import显的也不是十分优雅,所以“我们应该把协议单独放到一个文件中来写”:
总结:
1》除非有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合
2》有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入
第三条 多用字面量语法,少用与之等价的方法
1. NSString 字符串字面量语法
2. 字面量数值
3. 字面量数组
id obj1, obj2, obj3;
NSArray *arrayA = [NSArray arrayWithObjects:obj1,obj2,obj3, nil];
NSArray *arrayB = @[obj1, obj2, obj3];
问题:假如obj2是nil,来比较两种方式的区别
1. 首先两种方式都会报错崩溃
2. arrayA这中方式,数组arrayA已经被创建出来了,并且obj1已经被添加进去了,到了obj2发现obj2是nil,就会报错。
3. arrayB这种方式,他的效果相当于先创建数组,然后再把中括号里面的所有对象都加到这个数组中,他这种方式在创建数组的时候就会检查是否有nil,那么检查到obj2为nil,直接这个数组就不创建了
4. 综上,还是字面量数组方式比较好一点,因为数组中有nil本身就是一个bug,一个错误,字面量可以在未创建出数组之前就判断有nil。报错,比arrayA的方式要好。比正常方式更加安全
4.字面量字典
同样也会有值为nil的问题,这个道理和字面量数组是一模一样的
字面量语法的局限性
1.上面我们说的这些类都可以使用字面量语法,但是如果我们自定义这些类的“子类”,这些“子类”不能使用字面量语法,尽管使用了也会正常运行编译,但是回报警告
“Incompatible pointer types initializing 'SonNSString *' with an expression of type 'NSString *'”
“Incompatible pointer types initializing 'SonArray *' with an expression of type 'NSArray *'”
所以不建议我们对这些类的子类使用字面量语法
2.使用字面量语法创建出来的都是不可变的--"Incompatible pointer types initializing 'NSMutableArray *' with an expression of type 'NSArray *'"
如果想用字面量语法创建出可变的
总结
- 应该使用“字面量”语法来创建字符串,数值,数组,字典。与创建此类对象的常规方法相比,这么做更加简明扼要
- 应该通过取下标操作来访问数组下标或字典中的键对应的元素
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此无比确保值里不含nil。(即使是常规方法有nil也会抛出异常)
因此使用字面量语法1,简明扼要 2,对于有nil值,数组和字典来说字面量语法更安全。所以建议“多用字面量语法,少用与之等价的方法”
第四条 多用类型常量,少用#define预处理指令
Object-C定义常量的三种方式
- 第一种
#define Animation_Const 0.6
- 第二种
static const NSInteger kAnimation_Const = 0.3;
- 第三种
在.h 中
extern NSString *const Animation_Notification;
在.m中
NSString *const Animation_Notification = @"value";
1这种方式,“不建议使用”,这样定义出来的常量不包含类型信息,可读性不强,编译器针对这种方式做的就是在编译之前把引用到的“ Animation_Const”这个东西替换为未0.6,其他的不会做人和操作。这样的话如果中途有人改了这个常量值,编译器也不会警告,将导致程序中不同的地方常量的值不一样
2这种方式,比较好,但是仅仅使用与定义在.m中,这种方式如果仅仅是在自己的编译单元(一般都是.m是自己的一个独立编译单元)中使用的话用这个比较好
包含类型信息,清楚的描述了常量的含义。并且我们使用了static和const常量,因此中途如果有人试图修改该常量编译器就会报错,因此相比较于1更加安全
3这种方式,刚才我们说2的好处,但是2用在编译单元中也就是.m中的时候比较好,但是有些时候我就是需要外部能够访问到我的这个常量怎么办,就是用3种方式,在.h中声明,在.m中定义,这样一举两得。
---使用extern关键字来声明的全局常量,会出现在全局符号表中
----编译器再看到extern关键字的时候,编译器就能明白如何在引入此头文件的代码中处理该常量了,这个extern关键字就是告诉编译器,在全局符号表中将会有一个名叫Animation_Notification的符号,也就是说编译器无需查看其定于你,就允许代码使用此常量,因为他知道,当连接成二进制文件之后,肯定能找到这个常量
第五条 用枚举表示状态、选项、状态码
1.第一种定义枚举:
这种方式下面定义枚举变量的时候需要:
enum SocketConnectState state = SocketConnectStateDisConnected;
这样每次都需要使用那个enum,不太简洁,怎么才能不每次都适用enum呢,看第二种方式。
2.第二种定义枚举
这种方式下面定义枚举变量的时候就只需要:
ConnectState state = SocketConnectStateDisConnected;
| 上面这两种方式都没有定义枚举的“类型”,编译器会为枚举分配一个独有的编号,从0开始,每个枚举递增1,但是这个枚举具体类型取决于编译器,不过其“二进制位”的个数必须能完全表示下枚举编号才行,前例中因为只有三个枚举,所以最大也就是2,即0x10就能够表示,所以此枚举类型位char就可以了,如果枚举种类更多了,那么就可能不是char了,可能就是int或者什么的了
3-1. 第三种-1定义枚举
前面1,2两种,都没有声明枚举类型,第三种直接声明了枚举的数据类型。并且还可以设置枚举的起始值。
3-2. 第三种-2定义枚举
如果定义的这些选项可以彼此“组合”,就更应该使用这种方式了。各选项之间可以通过“按位或操作符”来组合,请看:
因此
EnumNumber num = EnumTwo | EnumThree;
就相当于0 0 0 0 1 1 因为进行了或操作嘛,
因此在下面判断的时候,就是只有这个枚举值得位一样就可以进入,所以:
这种3-2这种枚举定义方式系统库在非常频繁的使用
4.第四种 定义枚举
使用的这种“NS_ENUM”,“NS_OPTIONS”系统宏,这些宏具备向后兼容能力,如果目标平台的编译器支持新标准就是用新式语法,否则改用旧式语法
NS_ENUM宏如果是新式语法,上面的定义就相当于:
NS_OPTIONS宏针对看是否支持c++编译
1》如果不按c++编译,上面的枚举展开其实和NS_ENUM的展开是一样的,即:
为什么呢?--原因在于,用“按位或运算”来操作两个枚举值时,c++编译模式的处理办法与非c++模式不一样的。用“或”运算操作两个枚举值时,c++认为运算结果的数据类型应该是枚举的底层数据类型,也就是NSUInteger,并且c++还不允许将这个底层类型“隐式转换”为枚举类型本身,针对上面的展开,加入我们这样来使用:
EnumNumber num = EnumFour | EnumSenven;
若编译器是按c++模式编译的(也可能是按object-c++模式编译),那么上面的这句代码就会报错"不可能初始化这个变量类型"
所以我们如果想变易上面这句代码,就要将“按位或操作”的结果进行“显示转换”(因为c++编译不能隐式转换)为枚举类型(EnumNumber类型)。
总结,因为我们不想每次都进行一行显示类型转换,所以我们应该用NS_OPTIONS宏。使用NS_OPTIONS就是担心我们如果按照c++模式编译了,如果还没有显示转换就会报错,如果显示转换每次还比较麻烦,所以直接用了NS_OPTIONS宏
1》凡是需要按位操作的都应该使用NS_OPTIONS
2》若是枚举不需要组合也就是不需要按位操作什么的使用NS_ENUM就可以了
5.在switch中使用枚举
在switct如果食用了枚举我们最好不要使用default分之,为什么呢,加入我们不适用default分之,我们switch中如果少了那个枚举,编译就会警告,但是如果我们加了default编译器就不会警告,所以使用枚举在switch表示状态机的时候最好不要加default
枚举本节全文总结
1.应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值起个易懂的名字
2.如果传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合起来
3.用NS_ENUM 与NS_OPTIONS宏定义来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型
4.在处理枚举类型的switch语句中不要实现default分之。这样的话,加入新枚举之后编译器就会提示开发者:switch语句并未处理所有枚举。