编写高质量iOS与OSX代码的52个有效方法-第一章:熟悉OC

1、了解OC

OC使用消息结构(messaging structure)而不是函数调用(function calling)。OC由Smalltalk演化而来。

使用消息结构的语言,其运行时所执行的代码由运行环境来决定;而使用函数调用的语言,由编译器决定。

采用消息结构的语言,不论是否多态,总是在运行时才会去查找所要执行的方法。编译器甚至不关心接收消息的对象是何种类型。接收消息的对象问题也要在运行时处理,其过程叫做“动态绑定(dynamic binding)”

OC的重要工作由运行期组件(runtime component)而非编译器来完成。使用OC的面向对象特性所需的全部数据结构及函数都在运行期组件里面。

运行期组件,本质上就是一种与开发者所编写代码相连接的动态库(dynamic library),其代码能把所有程序粘合起来。只要更新运行期组件,即可提升应用程序性能。

同时掌握C与OC两门语言的核心概念,才能写出高效的OC代码来。尤为重要的是C语言的内存模型(memory model),有助于理解OC的内存模型及其“引用计数”机制的工作原理。

OC语言中的指针是用来指示对象的。对象所在内存总是分配在堆空间(heap space)中,不会分配在栈(stack)上。

OC是C的超集(superset)。使用动态绑定的消息结构,在运行时才会检查对象类型。接收消息之后,究竟应执行何种代码,由运行环境而非编译器来决定。

2、在类的头文件中尽量少引入其他头文件

头文件 header file

实现文件 implementation file

向前声明 forward declaring

在头文件中,通过@class 类名的方式,告诉编译器,有一个类名。

但是在实现文件中,如果需要知道所有的接口细节,则需要#import "xxx.h"

优点:

  • 将头文件引入的时机尽量延后,只在确有需要时才引入,减少类的使用者所需引入的头文件数量。减少编译时间。

  • 解决两个类互相引用的问题。在各自的头文件里引入对方头文件,会导致循环引用(chicken-and-egg situation)。

不能使用向前声明,只能引入头文件的情况

  • 1、如果写的类继承自某个超类,则必须引入定义那个超类的头文件。
  • 2、声明你写的类遵从某个协议(protocol),那么该协议必须有完整定义,不能使用向前声明。向前声明只能告诉编译器有某个协议,而此时编译器却要知道该协议中定义的方法。

第二种情况,解决方式:

  • 1、把协议单独放在啊一个头文件中,这样,会产生互相依赖问题,增加编译时间。
  • 2、委托协议(delegate protocol)不用单独写一个头文件,只有与接收协议委托的类放在一起才有意义。此时最好能在实现文件中声明此类实现了该委托协议,并把实现代码放在class-continuation 分类里

每次在头文件中引入其他头文件之前,都要先问问自己这样做是否确有必要。如果可以使用向前声明取代引入,那么就不要引入。如果因为要实现属性、实例变量或者要遵循协议而必须引入头文件,则尽量将其移至“class-continuation分类”中。不仅可以缩减编译时间,还能降低彼此依赖程度。

3、多用字面量语法(语法糖),少用与之等价的方法

使用字面量语法(literal syntax)可以缩减源代码长度,使其更为易读。

字面数值

将整型、浮点数、布尔值封入OC对象。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.4f;
NSNumber *doubleNumber = @3.242592;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
int x = 2;
int y = 2.2;
NSNumber *number = @(x * y);

字面量数组

字面量语法床架数组时要注意,数组元素对象有nil,会抛出异常。

NSArray *array = @[@"Dog",@"Tiger",@"Cat",@"Panda"];
NSString *item = array[0];//取下标
  • arrayWithObjects:与字面量数组的区别
    arrayWithObjects:方法会在遇到第一个为nil的对象时,结束添加。
    但是字面量创建数组不同,遇到nil 就会报异常。

字面量字典

NSDictionary *dict = @{@"1":@"one",
                           @"2":@"two",
                           @"3":@"three",
                           };
NSString *one = dict[@"1"];

字典中的键和对象必须是OC对象。字面量字典中有nil就会抛出异常。

可变数组与字典

通过下标可以访问数组中的某个下标或者字典中某个键对应的元素。如果数组和字典是可变的,也可以通过下标修改其中的值。

mutableArray[0] = @100;

mutableDict[@"2"] = @"222";
array[0] = @100;

局限性

字面量语法,除了字符串意外,所创建出来的对象必须属于Foundation框架才行。如果自定义这些类的子类,无法用字面量语法创建其对象。

使用字面量语法创建出来的字符串、数组、字典对象都是不可变的。

4、多用类型常量,少用#define预处理指令

static const NSTimeInterval kAnimationDuration = 0.4;

这种方式定义的常量,包含类型信息,清楚地描述了常量的含义。

常量的名称:若常量局限于某编译单元(translation unit,也就是实现文件)之内,则在前面加字母k;若常量在类之外可见,则通常以类名为前缀。

定义常量的位置很重要,同时也要注意常量的命名规则。

私有常量

只在编译单元内可见的常量(translation-unit-specific constant)。

若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。变量一定要同时使用static和const来声明。如果试图修改const修饰符所声明的变量,那么编译器就会报错。static修饰意味着该变量仅在定义此变量的编译单元中可见。

编译器每收到一个编译单元,就会输出一份目标文件(object file)。如果声明常量不加static,编译器会为它创建一个外部符号(external symbol),此时若另外一个编译单元也生命了同名变量,就会出现错误信息:

duplicate symbol _kAnimationDuration in:
    xxxx/x86_64/ViewController.o
    xxxx/x86_64/ZYDErrors.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

如果一个变量既声明为static,又声明为const,编译器根本不会创建符号,而是像#define预处理指令一样,把所有的变量都替换为常值。不过:用这种方式定义的常量带有类型信息。

对外可见的常值变量(constant variable)

此类常量需放在全局符号表(global symbol table)中,以便可以在定义该常量的编译单元之外使用。

需要在头文件中声明:
extern NSString *const DogObjectNotificationString;
extern关键字,告诉编译器,在全局符号表中会有一个指定的符号。

常量定义从右至左解读,所以“DogObjectNotificationString是一个常量,这个常量是一个指针,指向NSString对象”

在实现文件中定义:
NSString *const DogObjectNotificationString = @"DogObjectNotificationString";

此类定义必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件中。

由实现文件生成目标文件时,编译器会在数据段为字符串分配存储空间。链接器会把此目标文件与其它目标文件相链接,以生成最终的二进制文件。凡是用到该全局符号的地方,链接器都能将其解析。

因为符号要放在全局符号表里,所以命名一定要注意。最好是根据命名规范加前缀同时加类名前缀。

5、用枚举表示状态、选项、状态码

枚举只是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单的枚举集(enumeration set)

每个状态都用一个便于理解的值来表示,所以这样写出来的代码更容易读懂。

简单枚举 一

编译器会为枚举分配一个独有的编号,从0还是,每个枚举递增1。

enum DogObjectTest {
    DogObjectTestFirst,
    DogObjectTestSecond,
};

此类型枚举,声明一个枚举变量:

enum DogObjectTest one = DogObjectTestFirst;

声明变量不够简洁。

简单枚举 二

用typedef关键字重新定义枚举类型。

enum DogObjectTest {
    DogObjectTestFirst,
    DogObjectTestSecond,
};
typedef enum DogObjectTest DogObjectTest;

声明枚举标量,可以不用enum直接使用枚举类型名。

DogObjectTest two = DogObjectTestSecond;

指定底层数据类型枚举

可以向前声明枚举变量。

若不指底层数据类型,无法向前声明枚举类型,因为编译器不知道底层数据类型的大小,所以在用到次枚举类型的时候,也就不知道究竟该给变量分配多少空间。

enum DogObjectStatus:NSUInteger {
    DogObjectStatusOne,
    DogObjectStatusTwo,
};

指定底层数据类型是NSInteger。

  • 向前声明式指定底层数据类型
    enum DogObjectStatus: NSUInteger;

指定类型并且简化声明

typedef enum : NSUInteger {
    DogObjectPeriodLow,
    DogObjectPeriodMiddle,
    DogObjectPeriodHeight,
} DogObjectPeriod;

声明变量

DogObjectPeriod period = DogObjectPeriodLow;

定义选项

若选项可以彼此组合,更应如此。只要枚举定义的对,各选项之间可以通过按位或操作符(bitwise OR operator)来组合。如iOS UI框架有如下枚举类型,用来表示某个视图应该在水平或垂直方向上调整大小。

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

每个选项均可启用或禁用。每个枚举值所对应的二进制表中,只有一个二进制位是1。用按位或操作符可组合多个选项如UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth

可以用按位与操作符(bitwise AND operator)即可判断出是否已启用某个选项。

状态码

如果用switch作状态码判断,不应使用default分支,那么如果后面添加新的状态值,就会提示Enumeration value 'DogObjectPeriodOther' not handled in switch不会直接跳到default分支,导致分支情况处理不完整的情况。

推荐阅读更多精彩内容