对于有C/C++背景的程序员,Objective-C(简称ObjC)是可以速成的,因为说到底,ObjC只是在传统的C语言上加上一层面向对象的语义。ObjC是C语言的超集,一个C语言程序就是一个ObjC程序。

Hello in ObjC

先来看一个例子:

#import <Foundation/Foundation.h>

int main(int argc, char** argv)
{
  NSLog(@"Hello!");
}

解释一下上面的程序:

  • #import 等同于C语言中的#include,只不过前者只会保护头文件一次,不管头文件中是否有保护
  • Foundation是苹果为ObjC提供的基础库,比如NSLog就是在Foundation.h中定义的。
  • @"Hello!"是一个字符串,前面的@表示这个字符串不是传统的C字符串,而是NSString类型

把上面的程序保存成hello.m,然后使用下面的clang命令编译这个文件

clang -framework Foundation hello.m

sindresorhus/compile-objc.sh这个gist里面提供了更多clang编译选项的参考:

# -fobjc-arc: enables ARC
# -fmodules: enables modules so you can import with `@import AppKit;`
# -mmacosx-version-min=10.6: support older OS X versions, this might increase the binary size

clang hello.m -fobjc-arc -fmodules -mmacosx-version-min=10.6 -o hello

ObjC的面向对象机制

和C++一样,ObjC也是通过扩展C语言来增加面向对象机制。可是两者走的路不一样,C++学习了Simula,所以变成了现在的C++;而ObjC学习了SmallTalk,导致它的机制跟C++/Java之类的有些许不同,语法看起来也不太一样。

声明一个类


@interface Person: NSObject {
  // instance variables
  int age;
}

// class method
+ (bool) canFly;

// instance method
- (void) walk: (int)distance andDirection:(int)dir;

@end
  • 上面的例子中声明了一个类Person,这个类从NSObject继承(NSObject属于Foundation)
  • Person有一个成员变量age,用来表是Person的年龄
  • Person有一个类方法canFly,这个方法返回布尔类型。注意,类变量用+开头
  • Person有一个实例方法Walk,这个方法不返回任何值,但是接受两个参数,第一个参数是(int)distance,表明要走多远;第二个参数是andDirection:(int)dir,其中的andDirection其实是函数名的一部分。注意,实例方法用-开头

和C语言一样,类声明一般房子.h的头文件中。

实现一个类

#import "person.h"

@implementation Person
+ (bool) canFly
{
  return false;
}

- (void) walk: (int)distance andDirection:(int)dir
{
  ...
  // turn to direction
  // move a distance
  ...
}
@end

类的实现一般放在.m文件中,类似.c文件。(不得不说,.m文件容易和matlab的文件格式混淆)。

和C++的一些比较

消息传递

C++的面向对象机制是基于虚函数,也就是说多态是在编译时就确定了的;而ObjC是基于消息发送的,比如:

Person *person = [[Person alloc]init];
[person walk:20 andDirection:0];

首先[Person alloc]发送一条消息allocPersonPerson必须具有名字叫做alloc的类方法,否则就会出错。因为Person继承了NSObject,所以从NSObject那里获得了alloc

alloc生成一个Person的实例,然后调用实例的init方法,来初始化这个实例。init的作用比较像C++中的初始化函数,大楷是这么个样子的的:

- (id)init {
    self = [super init];
    if (self) {
        // perform initialization of object here
    }
    return self;
}

init返回id类型。id是一个万能指针,可以接受任何类型的对象。另外[super init]的意思是调用父类的init方法。

有人可能注意到我们的Person类并没有提供init方法,所以我们可以用new直接实例化Person:

[Person new]

消息转发

因为ObjC的消息是动态处理的,所以当一个对象收到一个消息时,可以把这个消息转给第三者来处理:

以下例子来自于Wikipedia,当Forwarder收到一条消息不能直接处理的时候,它通过forward方法把消息转给成员变量recipient,试图让recipient去处理这条消息:

@implementation Forwarder

- (retval_t)forward:(SEL)sel args:(arglist_t) args {
 /*
 * Check whether the recipient actually responds to the message.
 * This may or may not be desirable, for example, if a recipient
 * in turn does not respond to the message, it might do forwarding
 * itself.
 */
 if([recipient respondsToSelector:sel]) {
  return [recipient performv:sel args:args];
 } else {
  return [self error:"Recipient does not respond"];
 }
}

- (id)setRecipient:(id)_recipient {
 [recipient autorelease];
 recipient = [_recipient retain];
 return self;
}

- (id) recipient {
 return recipient;
}
@end

ObjC通过消息转发实现Dynamic Typping(动态类型指定),可以用来做多态。

Protocol

ObjC支持Protocol,所谓Protocol,其实像C++的抽象类,就是一堆方法的集合,如下所示:

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end

支持某个Protocol的类必须实现Protocol中所列举的方法:

@interface NSLock : NSObject <NSLocking>
//...
@end

说实话,这跟Go语言中的interface机制很像。

静态类型指定

静态类型指定是在编译期间发生的,看下面的例子:

  1. - (void)setMyValue:(id)foo;
  2. - (void)setMyValue:(id<NSCopying>)foo;
  3. - (void)setMyValue:(NSNumber *)foo;
  4. - (void)setMyValue:(NSNumber<NSCopying> *)foo;

上面的例子中,1可以匹配自己以及2、3、4,因为id是万能指针。2只匹配自己以及其他实现了NsCopying协议的同名方法,比如4。3可以匹配自己和4。4只能匹配自己。

Category

既然ObjC的对象的方法不是在编译时绑定的,那么可不可以在运行的时候为一个对象增加或者删除一个方法呢?答案是肯定的。ObjC可以在运行时添加若干个方法到一个对象的类型上。但是添加的粒度不是按照方法来算的,而是按照Category来算的。一个Category和一个Protocol一样,是若干个方法的集合。不过Protocol是在编译的时候添加若干方法到Interface,而Category是在运行的时候生效的。

但是和Protocol不同,Category不仅可以添加方法,还可以添加变量。同时,添加的Category的时候并不需要知道类的源代码,而且添加的Category中的方法可以访问类的私有成员。如果添加的Category中的方法和类中原有的方法重名,则类中原有的方法会被Category中的方法覆盖。这是给代码打补丁的一个很好的方式。

来看一下Wikipedia上的例子:

Integer.h中包含Integer的原始代码:

# import <objc/Object.h>

@interface Integer : Object {
 int integer;
}

- (int) integer;
- (id) integer: (int) _integer;
@end

Integer+Arithmetic.h为Integer增加了一个Category:

# import "Integer.h"

@interface Integer (Arithmetic)
- (id) add: (Integer *) addend;
- (id) sub: (Integer *) subtrahend;
@end

ObjC的这个特性依赖于它对Reflection的支持。也就是ObjC可以在运行时有办法便利类中的每个属性以及方法。这一点比C++要厉害许多。C++对Reflection只有简单的typeinfo的支持。

其他方面

ObjC的小历史

ObjC是一个很老的语言,在1984年由Tom Love和Brad Cox创建。后来NeXT选择ObjC作为主力开放语言。苹果收购了NeXT,乔帮主回到了苹果,同时ObjC也成为了苹果的主力开发语言。

苹果在2006的WWDC上推出了ObjC的2.0版本,给ObjC带来了垃圾收集,语法改进、以及64位支持等等。同时又在2012年的WWDC上推出了Modern Objective-C,带来了ARC,同时又增强了语法。

发展至今,ObjC可以说是长盛未衰,甚至微软都在Windows上推出了ObjC的编译器,叫做WinObjC,可以帮你方便得把mac/iOS上的程序移植到Windows上。

ObjC和Swift

ObjC曾经是iOS和macOS上的编写APP的唯一选择,直到WWDC 2014上苹果推出了Swift。到目前为止,两种语言还是并驾齐驱。苹果越来越倾向于Swift,所以在未来Swift会有更好的发展,比如新的语言特性,以及新的开发工具支持。但是个人觉得ObjC应该不会消失,毕竟ObjC只是C语言上的一个抽象层,只要C语言存在,让ObjC存在不难。

Swift降低了编写iOS/macOS APP的难度,毕竟ObjC有一部分是C,而C语言对于新人是不太友好的。

Objective C++

Clang和GCC支持Objective-C++,允许ObjC和C++可以有一些互操作性。你可以用Objective-C++编译C++代码,然后和ObjC链接。但是其中存在一些限制:

  • C++类和ObjC类不能互相继承
  • ObjC的类型声明不能与C++的namespace交互操作。
  • ObjC的成员变量的类型不能是带虚函数的C++类,或者没有默认构造函数的C++类
  • C++只能通过指针引用ObjC的对象
  • C++的模板不能用在ObjC的类型声明上,但是ObjC的类型可以作为模板参数
  • ObjC和C++的异常机制有所不同,ObjC的异常发送时可能不会调用C++的析构函数。在苹果系统中如果使用ObjC++,ObjC的异常会被替换成C++异常。

参考资源

主要参考

Medium 文章

Github资源

苹果官方文档

其他