Objective-C基本语法

简介

OC历史

Objective-C是一种简单的计算机语言,设计为可以支持真正的面向对象编程。
Objective-C通过提供类定义,方法以及属性的语法,还有其他可以提高类的动态扩展能力的结构等,扩展了标准的 ANSI C 语言。类的语法和设计主要是基于Smalltalk,是最早的面向对象编程语言之一。许多传统的面向对象概念,例如封装、继承以及多态,在Objective-C中都有所体现。

Objective-C语言的定位于C语言的严格超集。这意味着任何C语言程序不经修改就可以直接通过Objective-C编译器,在Objective-C中使用C语言代码也是完全合法的。Objective-C被描述为盖在C语言上的薄薄一层,因为Objective-C的原意就是在C语言主体上加入面向对象的特性。

文件结构

Objective-C的源代码文件格式如下:
| 扩展名 | 内容类型 |
| ——— | ————————————————————— |
| .h | 头文件,包含类,类型,函数和常数的声明 |
| .m | 源代码文件,可以包含 Objective-C 和 C 代码 |
| .mm | 源代码文件,支持与C++代码的混编 |

当你需要在源代码中包含头文件的时候,你可以使用标准的#include编译选项,但在Objective-C中提供了更好的方法。#import选项和#include功能完全相同,并且它可以确保相同的文件只会被包含一次。Objective-C的例子和文档都倾向于使用 #import

消息传递机制

Objective-C最大的特色是承自Smalltalk的消息传递模型(message passing),这样的机制与今日C++式之主流风格差异甚大。Objective-C里,与其说对象互相调用方法,不如说对象之间互相传递消息更为精确。

这两种风格的主要差异在于调用方法/消息传递这个动作。C++里类别与方法的关系严格清楚,一个方法必定属于一个类别,而且在编译时(compile time)就已经紧密绑定,不可能调用一个不存在类别里的方法。但在Objective-C,类别与消息的关系比较松散,调用方法视为对对象发送消息,所有方法都被视为对消息的回应。所有消息处理直到运行时(runtime)才会动态决定,并交由类别自行决定如何处理收到的消息。也就是说,一个类别不保证一定会回应收到的消息,如果类别收到了一个无法处理的消息,程序只会抛出异常,不会出错或崩溃。

C++里,送一个消息给对象(或者说调用一个方法)的语法如下:

1
obj.method(argument);

Objective-C中则写成:

1
[obj method: argument];

这两者并不仅仅是语法上的差异,还有基本行为上的不同。
这里以一个汽车类(car class)的简单例子来解释Objective-C的消息传递特性:[car fly];

典型的C++意义解读是调用car类别的fly方法,若car类别里头没有定义fly方法,那编译就不会通过。而在Objective-C里,我们应当解读为发送一个fly的消息给car对象,fly是消息,而car是消息的接收者。car收到消息后会决定如何回应这个消息,若car类别内定义有fly方法就运行方法内之代码,若car内不存在fly方法,则程序依旧可以通过编译,运行期则抛出异常。

这两种风格各有优劣。C++强制要求所有的方法都必须有对应的动作,且编译期绑定使得函数调用非常快速。缺点是仅能借由virtual关键字提供有限的动态绑定能力。Objective-C天生即具备态绑定能力,因为其在运行期才处理消息,允许发送未知消息给对象甚至是整个对象集合,而不需要一一检查每个对象的类型,也具备消息转送机制。同时空对象nil接受消息后默认为不做事,所以送消息给nil也不用担心程序崩溃。

基本语法

基本数据类型

Objective-C支持以下基本数据类型:

  • int:声明整型变量
  • double:声明双精度变量
  • float:声明浮点型变量
  • char:声明字符型变量
  • id:通用的指针类型
  • enum:声明枚举类型
  • long:声明长整型变量或函数
  • short:声明短整型变量或函数
  • signed:声明有符号类型变量
  • struct:声明结构体变量
  • union:声明共用体(联合)数据类型
  • unsigned:声明无符号类型变量
  • void:声明函数无返回值或无参

字符串支持

作为C语言的超集,Objective-C支持C语言字符串方面的约定,即单个字符被单引号包括,字符串被双引号包括。然而,大多数Objective-C通常不使用C语言风格的字符串。反之,大多数框架把字符串传递给NSString对象。NSString类提供了字符串的类包装,包含了所有你期望的优点,包括对保存任意长度字符串的内建内存管理机制,支持Unicode,printf风格的格式化工具,等等。因为这种字符串使用的非常频繁,Objective-C提供了一个助记符可以方便地从常量值创建NSString对象,只需要在普通的双引号字符串前放置一个@符号:

1
2
3
4
5
6
NSString* myString = @"My String\n";
NSString* anotherString = [NSString stringWithFormat:@"%d %s", 1, @"String"];
// 从一个C语言字符串创建Objective-C字符串
NSString* fromCString = [NSString stringWithCString:"A C string"
encoding:NSASCIIStringEncoding];

如同所有其他的面向对象语言,类是Objective-C用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。

Objective-C的类规格说明包含了两个部分:定义(interface)与实现(implementation)。定义(interface)部分包含了类声明和实例变量的定义,以及类相关的方法。实现(implementation)部分包含了类方法的实际代码。

类声明总是由@interface编译选项开始,由@end编译选项结束。类名之后用冒号分隔的是其所继承的父类。类的实例(或成员)变量声明在被大括号包含的代码块中。实例变量块后面就是类声明的方法的列表。每个实例变量和方法声明都以分号结尾。

Interface

定义部分,清楚定义了类的名称、数据成员和方法。 以关键字@interface作为开始,@end作为结束。

1
2
3
4
5
6
7
8
9
10
11
@interface MyObject : NSObject {
int memberVar1; // 实体变量
id memberVar2;
}
+(return_type) class_method; // 类方法
-(return_type) instance_method1; // 实例方法
-(return_type) instance_method2: (int) p1;
-(return_type) instance_method3: (int) p1 andPar: (int) p2;
@end

方法前面的加减号表示函数的类型:加号代表类方法的声明,不需要实例就可以调用,类似C++的静态函数。减号表示一般的实例方法的声明。

不同于C语言函数的括号来传递参数的方式,Objective-C定义一个新的方法时,名称内的冒号代表参数传递。Objective-C方法使得参数可以夹杂于名称中间,不必全部附缀于方法名称的尾端,可以提高程序可读性。以设定颜色RGB值的方法为例:

1
2
3
4
// declaration
- (void) setColorToRed: (float)red Green: (float)green Blue:(float)blue;
// call method
[myColor setColorToRed: 1.0 Green: 0.8 Blue: 0.2];

这个方法的签名是setColorToRed:Green:Blue:,每个冒号后面都带着一个float类别的参数,分别代表红,绿,蓝三色。

Implementation

实现区块则包含了公开方法的实现,以及定义私有(private)变量及方法。 以关键字@implementation作为区块起头,@end结尾。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation MyObject {
int memberVar3; // private instance property
}
+(return_type) class_method {
.... // method implementation
}
-(return_type) instance_method1 {
....
}
-(return_type) instance_method2: (int) p1 {
....
}
-(return_type) instance_method3: (int) p1 andPar: (int) p2 {
....
}
@end

不只Interface区块可定义实体变量,Implementation区块也可以定义实体变量,两者的差别在于访问权限的不同。Interface区块内的实体变量默认权限为protected,定义在implementation区块的实体变量则默认为private,因此在Implementation区块定义私有成员更匹配面向对象的封装原则。

创建对象

Objective-C创建对象需通过alloc以及init两个消息(参数)。alloc方法的作用是分配内存,init方法则是初始化对象。 initalloc都是定义在NSObject里的方法,父对象收到这两个信息并做出正确回应后,新对象才创建完毕。

如以下实例:

1
MyObject * my = [[MyObject alloc] init];

在Objective-C 2.0里,若创建对象不需要参数,则可直接使用new方法:

1
MyObject * my = [MyObject new];

两种写法的区别在于,使用[[MyObject alloc] init]方法初始化,可以自定义初始化的过程,只需要重写init方法就可以在类初始化时添加额外的工作(类似C++ 的构造函数),而[MyObject new]方法只能调用默认的初始化方法。

类的成员变量与属性

首先要明确,成员变量是类的固有属性,只在类中使用;属性是供外部调用的,相当于类中成员变量的外部接口。

一个类的成员变量定义如下:

1
2
3
4
5
6
7
@interface People : NSObject
{
NSString *_peopleName;
int _peopleAge;
int _peopleSex;
}
@end

成员变量不能在外部被调用,如果一定要在外部调用需要使用@public修饰符,并且需要使用->语法调用。

类的属性定义如下:

1
2
3
@interface People : NSObject
@property(nonatomic, strong) NSString *peopleName;
@end

可以使用.语法进行访问:

1
2
People *people = [[People alloc] init];
people.peopleName = @"Hello";

这里的@property语法相当于创建了一对get、set方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface People : NSObject
- (void)setName: (NSString *)name;
- (NSString *)getName;
@end
@implementation People
- (void)setName: (NSString *)name
{
_peopleName = name;
}
- (NSString *)getName
{
return _peopleName;
}
@end
// 方法调用
People *people = [[People alloc] init];
[people setName:@"Hello"];
NSLog(@"%@", [people getName]);

一般成员变量的命名规则使用_开头,属性名不需要_。在新版的SDK中,不声明成员变量,直接声明属性,也会自动生成以_开头的对应的成员变量。

方法

方法的声明

Objective-C中的类可以声明两种类型的方法:实例方法和类方法。实例方法就是一个方法,它在类的一个具体实例的范围内执行。也就是说,在你调用一个实例方法前,你必须首先创建类的一个实例。而类方法不需要你创建一个实例。

对象方法与成员方法的相互调用:

1
2
3
4
5
6
7
8
9
10
@implementation People
- (void)instanceMethod
{
[People staticMethod];
}
+ (void)staticMethod
{
[[People alloc] instanceMethod];
}
@end

Objective-C中的方法声明包括方法类型标识符、返回值类型、一个或多个方法标识关键字,以及参数类型和名称等信息。如以下这个实例方法的声明:

1
- insertObject:(id)anObject atIndex:(NSUInteger)index

声明由一个减号-开始,这表明这是一个实例方法。方法实际的名字insertObject: atIndex:(包含冒号)是所有方法标识关键的级联。冒号标记了方法对应的参数。如果方法没有参数,可以省略第一个(也是唯一的)方法标识关键字后面的冒号。

方法的调用

当调用一个方法时,其实就是传递消息到对应的对象。这里消息就是方法标识符,以及传递给方法的参数信息。发送给对象的所有消息都会动态分发,这样有利于实现Objective-C类的多态行为。也就是说,如果子类定义了跟父类的具有相同标识符的方法,那么子类首先收到消息,然后可以有选择的把消息转发(也可以不转发)给他的父类。

消息包含在中括号[ ]之间,接收消息的对象在左边,消息(包括消息需要的任何参数)在右边。例如,给myArray变量传递消息insertObject:atIndex:消息:

1
[myArray insertObject:anObj atIndex:0];

为了避免声明过多的本地变量保存临时结果,Objective-C允许使用嵌套消息。每个嵌套消息的返回值可以作为其他消息的参数或者目标。例如,你可以用任何获取这种值的消息来代替前面例子里面的任何变量。所以,如果你有另外一个对象叫做myAppObject拥有方法,可以访问数组对象,以及插入对象到一个数组,你可以把前面的例子写成如下的样子:

1
[[myAppObject getArray] insertObject:[myAppObject getObjectToInsert] atIndex:0];

虽然前面的例子都是传递消息给某个类的实例,但是你也可以传递消息给类本身。当给类发消息,你指定的方法必须被定义为类方法,而不是实例方法。这和C++中的类静态成员方法有点像(但并不完全相同)。

类方法的典型用途是创建新的类实例的工厂方法,或者是访问类相关的共享信息的途径。类方法声明的语法跟实例方法的几乎完全一样,只是方法前使用加号修饰。

下面的例子演示了一个类方法如何作为类的工厂方法。在这里,arrayWithCapacity是NSMutableArray类的类方法,为类的新实例分配内容并完成初始化,然后返回。

1
2
3
4
NSMutableArray* myArray = nil;
// 创建一个新的数组,并把它赋值给 myArray 变量
myArray = [NSMutableArray arrayWithCapacity:0];

@property属性

属性是用来代替声明存取方法的便捷方式。属性不会在你的类声明中创建一个新的实例变量,而仅仅是定义方法访问已有的实例变量的速记方式而已。对于需要暴露实例变量的类,可以使用属性记号代替getter和setter语法。

类还可以使用属性暴露一些“虚拟”的实例变量,它们是部分数据动态计算的结果,而不是确实保存在实例变量内的。

使用属性标记避免了为类暴露的每个实例变量提供不同的getter和setter的需求,节约了大量多余的代码。使用属性声明来指定希望的行为,程序就会在编译期间合成基于声明的实际的getter和setter方法。

属性声明应该放在类接口的方法声明那里。基本的定义使用@property编译选项,紧跟着类型信息和属性的名字。你还可以用定制选项对属性进行配置,这决定了存取方法的行为。下面的例子展示了一些简单的属性声明:

1
2
3
4
5
6
7
8
9
10
11
12
@interface Person : NSObject {
@public
NSString *name;
@private
int age;
}
@property(copy) NSString *name;
@property(readonly) int age;
- (id)initWithAge:(int)age;
@end

属性的访问方法由@synthesize关键字来实现,它可以根据属性的声明自动生成成员变量的一对访问方法。另外,也可以选择使用@dynamic关键字表明访问方法会由程序员手动实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation Person
@synthesize name;
@dynamic age;
- (id)initWithAge:(int)initAge
{
age = initAge; // 注意:直接赋给成员变量,而非属性
return self;
}
- (int)age
{
return 29; // 注意:并非返回真正的年龄
}
@end

属性可以利用消息表达式、点表达式或valueForKey:/setValue:forKey:方法对来访问。

1
2
3
4
Person *aPerson = [[Person alloc] initWithAge: 53];
aPerson.name = @"Steve"; // 注意:点表达式,等于[aPerson setName: @"Steve"];
NSLog(@"Access by message (%@), dot notation(%@), property name(%@) and direct instance variable access (%@)",
[aPerson name], aPerson.name, [aPerson valueForKey:@"name"], aPerson->name);

为了利用点表达式来访问实例的属性,需要使用self关键字:

1
2
3
4
- (void) introduceMyselfWithProperties:(BOOL)useGetter
{
NSLog(@"Hi, my name is %@.", (useGetter ? self.name : name)); // NOTE: getter vs. ivar access
}

类或协议的属性还可以被动态的读取:

1
2
3
4
5
6
7
8
9
int i;
int propertyCount = 0;
objc_property_t *propertyList = class_copyPropertyList([aPerson class], &propertyCount);
for(i = 0; i < propertyCount; i++) {
objc_property_t *thisProperty = propertyList + i;
const char* propertyName = property_getName(*thisProperty);
NSLog(@"Person has a property: '%s'", propertyName);
}

初始化方法

在Objective-C中,我们可以Override init方法来自定义类的初始化:

1
2
3
4
5
6
7
8
- (instancetype)init
{
self = [super init];
if(self) {
// statements ...
}
return self;
}

这样的方式覆盖了类的默认初始化方法,每一个实例出来的对象都将调用这个方法进行初始化。为了实现每个对象的自定义初始化方法,我们可以定义自己的初始化函数(类似含参构造器):

1
2
3
4
5
6
7
8
- (instancetype)initWitName: (NSString *)name andAge: (int)age
{
_peopleName = name;
_peopleAge = age;
}
// 调用初始化方法
People *people = [[People alloc] initWitName:"Hello" andAge:20];