Objective-C通过一套全新语法,在C语言基础上添加了面向对象特性。Objective-C的语法中频繁使用方括号,而且不吝于写出极长的方法名,这通常令许多人觉得此语言较为冗长。其实这样写出来的代码十分易读,只是C++或Java程序员不太能适应。
Objective-C语言学起来很快,但有很多微妙细节需注意,而且还有许多容易为人所忽视的特性。另一方面,有些开发者并未完全理解或是容易滥用某些特性,导致写出来的代码难于维护且不易调试。本章讲解基础知识,后续各章谈论语言及其相关框架中的各个特定话题。
和C++,Java一样,Objective-C也是面向对象语言,但是它们在许多方面都有差别。差别在于Objective-C使用的是消息结构而非函数调用,Objective-C语言由Smalltalk演化而来的,Smalltalk是消息型语言的鼻祖
//Messaging
Object* obj = [Object new];
[obj performWith: parameter1 and: parameter2];
//Function
Object* obj = new Object;
obj->preform (parameter1, parameter2);
关键区别在于:使用消息结构的语言,其运行时所应执行的代码由环境来决定;使用函数调用的语言,则由编译器决定。对于函数来说,如果范例代码的调用函数是多态的,那么就在运行时按照虚方法表来查出来到底执行哪个函数,而采用消息结构的语言,不管是否为多态总是在运行时才回去查找所要执行的方法。
NSString *someString = @"The string";
上述代码声明了一个someString的变量,类型为Nsstring*,也就是说,此变量为指向Nsstring的指针,所有oc的对象都必须这样声明,对象所占内存总是分配在堆空间上,而绝不能分配在栈空间上。
如果再次创建一个对象Same,那么这两个对象队徽分配在堆中,它们同时指向了堆中的NSString实例:
NSString *someString = @"The string";
NSString *anotherString = someString;
如下图所示:
分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈桢弹出时自动清理。
Objective-C运行期环境把堆内存管理工作抽象为一套内存管理结构,名叫“引用计数”。
Objective-C采用的是头文件和实例文件区分代码,头文件.h ,实例文件.m,这里以EOCPerson为例格式如下:
// EOCPerson.h
#import <Foundation/Foundation.h>
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@end
//EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
// Implementation of methods
@end
如果又创建一个名为EOCEmployer的新类,然后为EOCPerson类添加这个属性。就会是这个样子。
// EOCPerson.h
#import <Foundation/Foundation.h>
#import "EOCEmployer.h"
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
如果在EOCPerson类的头文件中,我们不需要知道这个新类的全部信息,就可以使用向前声明的方式。现在的头文件就变成这样了:
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer;
@interface EOCPerson : NSObject
@property (nonattomic, copy) NSString *firstName;
@property (nonattomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
EOCPerson类的实现文件则需引入EOCEmployer类的头文件,因为若要使用后者,则必须知道其所有接口细节。于是,实现文件就是:
//EOCPerson.m
#import "EOCPerson.h"
#import "EOCEmployer.h"
@implementation EOCPerson
// Implementation of methods
@end
尽量将引入头文件的时机延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入头文件数量,减少编译时间。
向前上名声明的好处:
不使用alloc及init方法来分配并初始化NSString对象,让语法更简洁。
NSString *someString = @"Effective Objective-C 2.0";
这种语法也可以来声明NSNumber、NSArray、NSDictionary类的实例。
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5f;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
字面量语法创建数组。
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
字面量语法操作数组
NSString *dog = animals[1];
“字典”是一种映射型数据结构,可向其中添加键值对。
字面量字典创建
NSDictionary *personData = @{@"firstName" : @"Matt", @"lastName" : @"Galloway", @"age" : @28};
字面量语法访问
NSString *lastName = personData[@"lastName"];
这样写省去了沉赘的语法,令此行代码简单易读。
字面量语法除了字符串以外,所创建出来的对象必须属于Foundation框架才行。然而一般来说,标准的实现已经很好了,使用这些已经足够了。
此外,使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,,则需复制一份:
NSMutableArray *mutable = [@[@1, @2, @3, @4] mutableCopy];
这样做会多调用一个方法,而且还要再创建一个对象,不过使用字面量语法所带来的好处还是多与上述缺点的。
编写代码时经常要定义常量。如果我们使用预处理指令,如下。
#define ANIMATION_DURATTON 0.3;
那么源代码中的ANIMATION_DURATTON字符串都会被替换为0.3,不过这样定义出的常量没有类型信息。此外,假设此指令声明在某个头文件中,那么所有引入这个头文件的代码,其ANIMATION_DURATTON都会被替换。
所以我们最好使用类型常量,如下:
static const NSTimeInterval KAnimationDuration = 0.3;
用此方法定义的常量包含类型信息,其好处是清楚地描述了常量的含义。由此可知该常量类型为NSTimeInterval,这有助于为其编写开发文档。
若常量局限于某“编译单元”(也就是“实现文件”)之内,则在前面加字面k;若常量在类之外可见,则通常以类名为前缀。
位置
因为Objective-C没有“名称空间”(namespace)这一概念,所以在头文件使用static const定义常量,其实等于声明了一个名叫KAnimationDuration的全局变量。此名称应该加上前缀,以表明其所属的类,例如可改为EOCViewClassAnimationDuration。
若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。
// EOCAnimatedView.h
#import <UIKit/UIKit.h>
@interface EOCAnimatedView : UIView
- (void)animate;
@end
// EOCAnimatedView.m
#import "EOCAnimatedView.h"
static const NSTimeInterval KAnimationDuration = 0.3;
@implementation EOCAnimatedView
- (void)animate {
[UIViewanimateWithDuration:KAnimationDuration animations:^(){
// .......
}];
}
@end
该修饰符意味着变量仅在定义此变量的编译单元可见。假如声明此变量时不加static,则编译器会为它创建一个“外部符号”。此时若是另一个编译单元中也声明了同名变量,那么编译器就会抛出一条错误消息:
duplicate symbol _KAnimationDuration in:
EOCAnimatedView.o
EOCOtherView.o
该变量意味着变量不可修改,如果试图修改由const修饰符所声明的变量,那么编译器就会报错。
实际上,如果一个变量既声明为static,又声明为const,那么编译器就会像#define预处理指令一样,把所有遇到的变量都替换为常值。不过,用这种方式定义的常量带又类型信息。
有时候需要对外公开某个常量。此时,我们需要声明一个外界可见的常值变量(constant variable)。此类常量需放在“全局符号表”(global symbol table)中,以便可以在编译单元之外使用。定义方法为:
// In the header file
extern NSString *const EOCStringConstant;
// In the implementation file
NSString *const EOCtringCostant = @"VALUE";
这个常量在头文件中“声明”,且在实现文件中“定义”。在本例中,EOCtringCostant就是一个常量,这个常量是指针,指向NSString对象。
此类常量必须要定义,而且只能定义一次。通常将其定义在与声明该常量的头文件相关的实现文件里。
枚举只是一种常量命名方式,某个对象所经历的各个状态、定义选项或者把逻辑含义相似的一组状态码都可以放入一个枚举集里。
编译器会为枚举分配一个独有的编号,从0开始,每个枚举递增1,也可以手动设置某个枚举成员对应的值,后面的枚举值一次加1。
可以指明枚举用的何种底层数据类型,这样编译器清楚底层数据类型的大小,可以向前声明枚举类型。
UIButton的状态:
typedef NS_ENUM(NSInteger, UIButtonRole) {
UIButtonRoleNormal,
UIButtonRolePrimary,
UIButtonRoleCancel,
UIButtonRoleDestructive
} API_AVAILABLE(ios(14.0));