Objective C 是 C 语言的超集,继承了 C 语言的语法、基本数据类型、控制流语句;同时加入了对象和动态的运行时:类和方法、动态类型和绑定、直到运行时才做责任推导。

类与对象

类的定义分为接口和实现,对象方法和类方法用 -+ 标识,使用 [] 进行方法调用,方法调用被视为发送消息:

//声明
@interface Person : NSObject
-(void)sayHello;
-(void)Greeting;
@end

//实现
@implementation Person
-(void)sayHello {
    NSLog(@"Hello!");
}
-(void)greeting{
    [self sayHello];
}
@end

类本身也是对象,其类型为 Class

方法不允许重载(编译错),子类中方法将覆盖父类中同名方法。因为 objc 的类型是动态的,所以调用时的指针类型并不重要,总会调用到正确的(子类)方法:

@interface BadPerson: Person
-(void) sayHello;
@end

@implementation BadPerson
-(void) sayHello{
    NSLog(@"Great!");
}
@end
...
Person p = [[BadPerson alloc] init];
Person q = [Person new];
[p sayHello];       // Great!
[q sayHello];       // Hello!

init 可能返回与 alloc 不同的对象,所以应该总是将返回值赋给某个指针。

变量的判等使用 ==,对象判等则使用 isEqual 方法;变量声明时最好初始化,而对象指针则会自动初始化为 nil

数据封装

类声明中可以包含属性声明,使外部能够访问对象的数据。属性可以添加限定符来限定访问权限,objc 会自动为属性生成 gettersetter,该方法名也可用户指定,实现同名方法即可自定义访问器。objc 将为每个属性生成一个实例,与属性同名并拥有前导下划线,当然用户可以指定:

@interface XYZPerson : NSObject
@property (readwrite) NSString *firstName;
@property (readonly, getter=isFinished) NSString *lastName;
@end

@implementation XYZPerson
@synthesis firstname = ivar_firstName;
...
_lastName = @"Jobs";        // 可以这样访问属性
ivar_firstName = @"Steve";  // 使用访问器才是推荐的做法
...
@end

NSString *firstName = [somePerson firstName];
[somePerson setFirstName:@"Johnny"];

// 访问器方法可用点来调用
NSString *firstName = somePerson.firstName;
somePerson.firstName = @"Johnny";

objective c 属性与方法并无 publicprivate 的区别。类中声明的变量(无论是对象属性还是类属性)都是对外不可见的,外部可见的只有方法。 objective c 通过引入 .h 文件来使方法可见(即 public)。因为 .m 文件不会引入,其中定义的方法只有当前类可见(即 private)。 所以定义在 .m 文件中的类扩展(Extension,@interface(){})相当于私有方法和属性。

@propperty 的本质是生成访问器方法。属性访问是原子操作,即 objc 会原子的调用访问器方法。在构造函数中总是应该访问实例变量而不是属性访问器,否则可能产生副作用(MVC 通知、子类不知情的重写)。

@interface{} 中声明的变量将被作为对象变量,@interface{} 外禁止声明变量; @implementation{} 中声明的变量也将被视为对象变量,@implementation 外的变量将被作为类变量。

@interface SomeClass : NSObject {
    // 实例变量
    NSString *_myNonPropertyInstanceVariable;
}
// Error! @interface 中禁止声明变量
NSString *_someVariable;
...
@end
 
@implementation SomeClass {
    // 实例变量
    NSString *_anotherCustomInstanceVariable;
}
// 类变量
NSString *_classVariable;
...
@end

属性可以声明为 strongweak,用它们来组织对象图。要避免形成 strong 引用环,否则当它们的拥有者析构后,它们将同时保持 alive。有一些 Cocoa 和 Cocoa Touch 类不支持 weak,此时应使用 unsafe unretained,对象离开作用域后不再 alive,但指针不会设为 nil(unsafe)。

// 对象
@property (unsafe_unretained) NSObject *unsafeProperty;
// 变量
NSObject * __unsafe_unretained unsafeReference;

类的扩展

Category 用来给既有的类添加功能(方法),适用于任何已有的类,即使没有该类的实现源码。添加 Category 方法后,添加的方法与既有方法没有任何区别,适用于原始类以及原始类的子类。

// file: XYZPerson+XYZPersonNameDisplayAdditions.h
#import "XYZPerson.h"

@interface XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString;
@end

// file: XYZPerson+XYZPersonNameDisplayAdditions.m
#import "XYZPerson+XYZPersonNameDisplayAdditions.h"
 
@implementation XYZPerson (XYZPersonNameDisplayAdditions)
- (NSString *)lastNameFirstNameString {
    return [NSString stringWithFormat:@"%@, %@", self.lastName, self.firstName];
}
@end

除了用于扩展既有类的功能,还可以用于将复杂类分为多个文件,这样使用时也可以方便地添加功能。

在 Category 的接口中添加属性是合法的,然而在实现中添加实例变量是被禁止的(所以 objc 不会合成该属性,也不会生成访问器方法)。唯一给现有类添加传统属性的方法是 Extension。

Category 中的方法名产生冲突时,调用结果是未定义的,即依赖于编译器实现。这一问题在使用 NS 类库时更为突出,所以小心的命名很重要,推荐的做法是:在方法前加前缀,就像在自定义类前加前缀一样。

Extension 与 Category 类似,区别在于 Extension 需要在原始实现代码中添加实现,于是需要有原始类的实现源码。同时,Extension 允许在接口定义的括号中添加属性与实例变量。

类接口用于定义类的公共接口,而 Extension 一般用于定义实现相关的方法和属性,常常在实现文件中加入 Extension 的接口定义,用来添加私有方法和属性。

协议的使用

Protocol 定义了在某种情形下会用到的一系列方法,本质上是一种通信合同。例如饼状图的数据源:

@protocol XYZPieChartViewDataSource
- (NSUInteger)numberOfSegments;
- (CGFloat)sizeOfSegmentAtIndex:(NSUInteger)segmentIndex;
@optional
- (NSString *)titleForSegmentAtIndex:(NSUInteger)segmentIndex;
@end

@optional 后的都是可选的,也可用 @required 来终结。

提供这样三个方法(第三个是可选的)的类才满足该协议,其语义就是该类可以作为饼状图的数据源。使用数据源时也会指定这样的协议:

@interface XYZPieChartView : UIView
@property (weak) id <XYZPieChartViewDataSource> dataSource;
@end

可以声明一个类满足某个协议(如果没有实现会有编译警告),协议间也可以继承:

// 协议继承
@protocol MyProtocol <NSObject>
@end
// 声明满足某个协议
@interface MyClass : NSObject <MyProtocol,AnotherProtocol>
@end

如果在运行时调用的协议方法没有实现则会出错,为此可以来检测方法是否被实现:

NSString *thisSegmentTitle;
if ([self.dataSource respondsToSelector:@selector(titleForSegmentAtIndex:)]) {
    thisSegmentTitle = [self.dataSource titleForSegmentAtIndex:index];
}

协议用来隐藏对象所属的类,或者用于所属的类未知的情形。

数据类型与块语法

objc 兼容 C 的基本类型和运算符,并提供了一系列基本数据类型,如 BOOL, NSInteger。当用于平台相关的 API 调用时,NSInteger 会有优势,它是平台无关的。同时 objc 常采用 C 的结构体来组织数据。

objc 对象可以用来储存基本数据类型。例如 NSString 可以用来存储字符串类型,NSNumber 可以用来储存 C 的基本标量类型,其他的值可以用 NSValue 来储存。

objc 提供了很多集合类型:数组用来表示有序集合,集合用来表示无序集合,字典用来收集键值对:

NSArray *someArray = @[firstObject, secondObject, thirdObject];
NSSet *simpleSet =
      [NSSet setWithObjects:@"Hello, World!", @42, aValue, anObject, nil];
NSDictionary *dictionary = @{
                  @"anObject" : someObject,
               @"helloString" : @"Hello, World!",
               @"magicNumber" : @42,
                    @"aValue" : someValue
    };

在 objc 中 nil 表示没有对象,如果要将在集合中加入空对象应该使用 [NSNull null]

支持 NSFastEnumeration 协议的集合类型可以这样遍历:

for (<Type> <variable> in <collection>) {
    ...
}

块是一种只完成单一任务的对象,可以存储和传递。也可以访问局部(当前作用域)变量,这使得它有点像闭包和 lambda 演算。其声明和定义如下:

// 声明
void (^simpleBlock)(void);
// 定义
simpleBlock = ^{
    NSLog(@"This is a block");
}
// 初始化(声明+定义)
void (^simpleBlock)(void) = ^{
    NSLog(@"This is a block");
};
// 调用
simpleBlock();

错误处理

可以创建一个 NSError 对象来引发错误:

NSString *domain = @"com.MyCompany.MyApplication.ErrorDomain";
NSString *desc = NSLocalizedString(@"Unable to…", @"");
NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : desc };

NSError *error = [NSError errorWithDomain:domain
                                     code:-101
                                 userInfo:userInfo];

异常处理:

@try {
    // do something that might throw an exception
}
@catch (NSException *exception) {
    // deal with the exception
}
@finally {
    // optional block of clean-up code
    // executed whether or not an exception occurred
}

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2014/08/21/objc.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。