• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

从C++到Objective-C

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

Objective-C 可以算作 Apple 平台上“唯一的”开发语言。很多 Objective-C 的教程往往直接从 Objective-C 开始讲起。不过,在我看来,这样做有时候是不合适的。很多程序员往往已经掌握了另外一种开发语言,如果对一门新语言的理解建立在他们已有的知识之上,更能 起到事半功倍的效果。既然名为 Objective-C,它与 C 语言的联系更加密切,然而它又是 Objective 的。与 C 语言联系密切,并且是 Objective 的,我们能够想到的另外一门语言就是 C++。C++ 的开发人员也更普遍,受众也会更多。于是就有了本系列,从 C++ 的角度来讲述 Objective-C 的相关知识。不过,相比 C++,C# 似乎更近一些。不过,我们还是还用 C++ 作为对比。这个系列不会作为一个完整的手册,仅仅是入门。本系列文章不会告诉你 Objective-C 里面的循环怎么写,而是通过与 C++ 的对比来学习 Objective-C 一些更为高级的内容,例如类的实现等等。如果要更好的使用 Objective-C,你需要阅读更多资料。但是,相信在本系列基础之上,你在阅读其他资料时应该会理解的更加透彻一些。

说明:本系列大致翻译来自《From C++ to Objective-C》,你可以在这里找到它的英文 pdf 版本。

下面来简单介绍一下 Objective-C。

要说 Objective-C,首先要从 Smalltalk 说起。Smalltalk 是第一个真正意义上的面向对象语言。Smalltalk 出现之后,很多人都希望能在 C 语言的基础之上增加面向对象的特性。于是就出现了两种新语言:C++ 和 Objective-C。C++ 不必多说,很多人都比较熟悉。Objective-C 则比较冷门。它完全借鉴了 Smalltalk 的思想,有着类似的语法和动态机制;相比来说,C++ 则更加静态一些,目的在于提供能好的性能。Objective-C 最新版本是 2.0.我们的这个系列就是以 Objective-C 2.0 为基础讲解。

Objective-C 是一门语言,而 Cocoa 是这门语言用于 MacOS X 开发的一个类库。它们的关系类似于 C++ 和 Qt,Java 和 Spring 一样。所以,我们完全可以不使用 Cocoa,只去用 Objective-C。例如 gcc 就是一个不使用 Cocoa 的编译器。不过在 MacOS X 平台,几乎所有的功能都要依赖 Cocoa 完成。我们这里只是做一个区别,应该分清 Objective-C 和 Cocoa 的关系。

从 C++到 Objective-C(2):语法概述

关键字

Objective-C 是 C 语言的超集。类似于 C++,良好的 C 源代码能够直接被 Objective-C 编译器编译。不同于 C++ 直接改变 C 语言的设计思路,Objective-C 仅仅是在 C 语言的基础上增加了一些概念。例如,对于类的概念,C++ 是增加了一个全新的关键字 class,把它作为语言内置的特性,而 Objective-C 则是将类转换成一个 struct 去处理。所以,为了避免冲突,Objective-C的关键字都是以 @ 开头。一个简单的关键字列表是:@class, @interface, @implementation, @public,@private, @protected, @try, @catch, @throw, @finally, @end, @protocol,@selector, @synchronized, @encode, @defs。Objective-C 2.0 又增加了 @optional, @required, @property, @dynamic, @synthesize 这几个。

另外的一些值同样也类似于关键字,有 nil 和 Nil, 类型 id, SEL 和 BOOL, 布尔变量 YES 和 NO。最后,特定上下文中会有一些关键字,分别是:in, out, inout, bycopy, byref, oneway 和 getter, setter, readwrite, readonly, assign,retain, copy, nonatomic 等。

很多继承自 NSObject 的函数很容易与关键字混淆。比如 alloc, release 和 autorelease 等。这些实际都是 NSObject 的函数。另外一个需要注意的是self 和 super。self 实际上是每一个函数的隐藏参数,而 super 是告知编译器使用 self 的另外语义。

注释

Objective-C 使用 // 和 /*…*/ 两种注释风格。

变量声明的位置

Objective-C 允许在代码块的中部声明变量,而不仅仅在块的最开始处。

新增的值和变量

BOOL, YES, NO

C++ 中使用 bool 表示布尔类型。Objective-C 中则是使用 BOOL,其值为 YES 和 NO。

nil, Nil 和 id

简单来说:

·        每一个对象都是 id 类型的。该类型可以作为一种弱类型使用。id 是一个指针,所以在使用时应注意是否需要再加 *。例如 id*foo = nil,实际是定义一个指针的指针;

·        nil 等价于指向对象的 NULL 指针。nil 和NULL 不应该被混用。实际上,nil 并不简单是 NULL 指针;

·        Nil 等价于指针 nil 的类。在 Objective-C 中,一个类也是一个对象(作为元类 Meta-Class 的实例)。nil 代表 NULL 指针,但它也是一个类的对象,nil 就是 Nil类的实例。C++ 没有对应的概念,不过,如果你熟悉 Java 的话,应该知道每一个类对象都对应一个 Class 实例,类似这个。

SEL

SEL 用于存储选择器 selector 的值。所谓选择器,就是不属于任何类实例对象的函数标识符。这些值可以由 @selector 获取。选择器可以当做函数指针,但实际上它并不是一个真正的指向函数的指针。

@encode

为了更好的互操作性,Objective-C 的数据类型,甚至自定义类型、函数或方法的元类型,都可以使用 ASCII 编码。@encode(aType) 可以返回该类型的 C 字符串(char *)的表示。

源文件

与 C++ 类似,Objective-C同样建议将声明和实现区分开。Objective-C 的头文件后缀名是 .h,源代码后缀名是 .m。Objective-C 使用 #import 引入其它头文件。与 #include 不同的是,#import 保证头文件只被引入一次。另外,#import 不仅仅针对 Objective-C 的头文件,即便是标准 C 的头文件,比如 stdlib.h,同样可以使用 #import 引入。

C++

头文件

源文件

//In file Foo.h
#ifndef __FOO_H__ //compilation guard
#define __FOO_H__ //
class Foo
{
...
};
#endif
//In file Foo.cpp
#include "Foo.h"
...

 

Objective-C

头文件

源文件

//In file Foo.h
//class declaration, different from
//the "interface" Java keyword
@interface Foo : NSObject
{
...
}
@end
//In file Foo.m
#import "Foo.h"
@implementation Foo
...
@end

NS 前缀

我们前面看到的类 NSObject,NSString 都有一个前缀 NS。这是 Cocoa 框架的前缀(Cocoa 开发公司是 NeXTStep)。

函数和方法的区别

Objective-C 并不是“使用方括号表示函数调用”的语言。一开始很容易把

[object doSomething];

理解成

object.doSomething();

但实际上并不是这么简单。Objective-C 是 C 语言的超集,因此,函数和 C 语言的声明、定义、调用是一致的。C 语言并没有方法这一概念,因此方法是使用特殊语法,也就是方括号。不仅仅是语法上的,语义上也是不同的:这并不是方法调用,而是发送一条消息。看上去并没有什么区别,实际上,这是 Objective-C 的强大之处。例如,这种语法允许你在运行时动态添加方法。

从 C++到 Objective-C(3):类和对象

既然是面向对象语言,类和对象显然是应该优先考虑的内容。鉴于本系列已经假定你已经熟悉 C++ 语言,自然就不会去解释类和对象的含义。我们直接从 Objecti-C 和 C++ 的区别开始说起。

Objetive-C 使用的是严格的对象模型,相比之下,C++ 的对象模型则更为松散。例如,在 Objective-C 中,所有的类都是对象,并且可以被动态管理:也就是说,你可以在运行时增加新的类,根据类的名字实例化一个类,以及调用类的方法。这比 C++ 的 RTTI 更加强大,而后者只不过是为一个“static”的语言增加的一点点功能而已。C++ 的 RTTI 在很多情况下是不被推荐使用的,因为它过于依赖编译器的实现,牺牲了跨平台的能力。

根类,id 类型,nil 和 Nil 的值

任何一个面向对象的语言都要管理很多类。同 Java 类似,Objective-C 有一个根类,所有的类都应该继承自这个根类(值得注意的是,在 Java 中,你声明一个类而不去显式指定它继承的父类,那么这个类就是 Object 类的直接子类;然而,在 Objective-C 中,单根类的子类必须被显式地说明);而 C++ 并没有这么一个类。Cocoa 中,这个根类就是 NSObject,它提供了很多运行时所必须的能力,例如内存分配等等。另外需要说明一点,单根类并不是 Objective-C 语言规范要求的,它只不过是根据面向对象理论实现的。因此,所有 Java 虚拟机的实现,这个单根类都是 Object,但是在Objective-C 中,这就是与类库相关的了:在Cocoa 中,这个单根类是 NSObject,而在 gcc 的实现里则是 Object。

严格说来,每一个类都应该是 NSObject 的子类(相比之下,Java 应该说,每一个类都必须是 Object 的子类),因此使用 NSObject * 类型应该可以指到所有类对象的指针。但是,实际上我们使用的是 id 类型。这个类型更加简短,更重要的是,id 类型是动态类型检查的,相比来说,NSObject * 则是静态类型检查。Objective-C 里面没有泛型,那么,我们就可以使用 id 很方便的实现类似泛型的机制了。在 Objective-C 里面,指向空的指针应该声明为 nil,不能是 NULL。这两者虽然很相似但并不可以互换。一个普通的 C 指针可以指向 NULL,但是Objective-C 的类指针必须指向 nil。正如前文所说,Objective-C 里面,类也是对象(元类 Meta-Class 的对象)。nil 所对应的类就是 Nil。

类声明

属性和方法

在 Objective-C 里面,属性 attributes 被称为实例数据 instance data,成员函数 member functions 被称为方法 methods。如果没有特殊说明,在后续文章中,这两组术语都会被混用,大家见谅。

C++

Objective-C

class Foo
{
    double x;
    public:
    int f(int x);
    float g(int x, int y);
};
 
int Foo::f(int x) {...}
 
float Foo::g(int x, int y) {...}
@interface Foo : NSObject
{
    double x;
}
 
-(int)   f:(int)x;
-(float) g:(int)x :(int)y;
 
@end
 
@implementation Foo
 
-(int)   f:(int)x {...}
-(float) g:(int)x :(int)y {...}
 
@end

在 C++ 中,属性和成员函数都在类的花括号块中被声明。方法的实现类似于 C 语言,只不过需要有作用于指示符(Foo::)来说明这个函数属于哪个类。

Objective-C 中,属性和方法必须分开声明。属性在花括号中声明,方法要跟在下面。它们的实现要在 @implementation 块中。

这是与 C++ 的主要不同。在Objective-C 中,有些方法可以不被暴露在接口中,例如 private 的。而 C++ 中,即便是 private 函数,也能够在头文件中被看到。简单来说,这种分开式的声明可以避免 private 函数污染头文件。

实例方法以减号 – 开头,而 static 方法以 + 开头。注意,这并不是 UML 中的 private 和 public 的区别!参数的类型要在小括号中,参数之间使用冒号 : 分隔。

Objective-C 中,类声明的末尾不需要使用分号 ;。同时注意,Objective-C 的类声明关键字是 @interface,而不是 @class。@class 关键字只用于前向声明。最后,如果类里面没有任何数据,那么花括号可以被省略。

前向声明

为避免循环引用,C 语言有一个前向声明的机制,即仅仅告诉存在性,而不理会具体实现。C++ 使用 class 关键字实现前向声明。在 Objective-C 中则是使用 @class 关键字;另外,还可以使用 @protocol 关键字来声明一个协议(我们会在后面说到这个概念,类似于 Java 里面的 interface)。

C++

//In file Foo.h
 
#ifndef __FOO_H__
#define __FOO_H__
 
class Bar; //forward declaration
 
class Foo
{
    Bar* bar;
public:
    void useBar(void);
};
 
#endif
//In file Foo.cpp
 
#include "Foo.h"
#include "Bar.h"
 
void Foo::useBar(void)
{
    ...
}

 

Objective-C

//In file Foo.h
 
@class Bar; //forward declaration
 
@interface Foo : NSObject
{
    Bar* bar;
}
 
-(void) useBar;
 
@end
//In file Foo.m
 
#import "Foo.h"
#import "Bar.h"
 
@implementation Foo
 
-(void) useBar
{
    ...
}
 
@end

private,protected 和 public

访问可见性是面向对象语言的一个很重要的概念。它规定了在源代码级别哪些是可见的。可见性保证了类的封装性。

C++

Objective-C

class Foo
{
public:
    int x;
    int apple();
protected:
    int y;
    int pear();
private:
    int z;
    int banana();
};
@interface Foo : NSObject
{
@public:
    int x;
@protected:
    int y;
@private:
    int z;
}
 
-(int) apple;
-(int) pear;
-(int) banana;
 
@end

在 C++ 中,属性和方法可以是 private,protected 和 public 的。默认是 private。

在 Objective-C 中,只有成员数据可以是 private,protected 和 public 的,默认是 protected 。方法只能是 public 的。然而,我们可以在 @implementation 块中实现一些方法,而不在 @interface 中声明;或者是使用分类机制(class categories)。这样做虽然不能阻止方法被调用,但是减少了暴露。不经过声明实现一些方法是 Objective-C 的一种特殊属性,有着特殊的目的。我们会在后面进行说明。

Objective-C 中的继承只能是 public 的,不可以是 private 和 protected 继承。这一点,Objective-C 更像 Java 而不是 C++。

static 属性

Objective-C 中不允许声明 static 属性。但是,我们有一些变通的方法:在实现文件中使用全局变量(也可以添加 static 关键字来控制可见性,类似 C 语言)。这样,类就可以通过方法访问到,而这样的全局变量的初始化可以在类的 initialize 方法中完成。

从 C++到 Objective-C(4):类和对象(续)

方法

Objective-C 中的方法与 C++ 的函数在语法方面风格迥异。下面,我们就来讲述 Objective-C 的方法。

原型、调用、实例方法和类方法

·        以 – 开头的是实例方法(多数情况下都应该是实例方法);以 + 开头的是类方法(相当于 C++ 里面的static 函数)。Objective-C的方法都是 public 的;

·        返回值和参数的类型都需要用小括号括起来;

·        参数之间使用冒号:分隔;

·        参数可以与一个标签 label 关联起来,所谓标签,就是在 : 之前的一个名字。标签被认为是方法名字的一部分。这使得方法比函数更易读。事实上,我们应该始终使用标签。注意,第一个参数没有标签,通常它的标签就是指的方法名;

·        方法名可以与属性名相同,这使 getter 方法变得很简单。

C++

// 原型
void Array::insertObject(void *anObject, unsigned int atIndex);
 
// shelf 是 Array 类的一个实例,book 是一个对象
shelf.insertObject(book, 2);

Objective-C(不带 label,即直接从 C++ 翻译来)

// 方法原型
// 方法名字是“insertObject::”
// 这里的冒号:用来分隔参数,成为方法名的一部分(注意,这不同于 C++ 的域指示符::)
-(void) insertObject:(id)anObject:(unsigned int)index
 
// shelf 是 Array 类的一个实例,book 是一个对象
[shelf insertObject:book:2];

Objective-C(带有 label)

// 方法原型。“index” 有一个标签“atIndex”
// 方法名为“insertObject:atIndex:”
// 这样的话,调用语句就很容易阅读了
-(void) insertObject:(id)anObject atIndex:(unsigned int)index
 
// shelf 是 Array 类的一个实例,book 是一个对象
[shelf insertObject:book:2];         // 错误!
[shelf insertObject:book atIndex:2]; // 正确

注意,方括号语法不应该读作“调用 shelf 对象的 insertObject 方法”,而应该是“向 shelf 对象发送一个 insertObject 消息”。这是Objective-C 的实现方式。你可以向任何对象发送任何消息。如果目标对象不能处理这个消息,它就会将消息忽略(这会引发一个异常,但不会终止程序)。如果接收到一个消息,目标对象能够处理,那么,目标对象就会调用相应的方法。如果编译器能够知道目标对象没有匹配的方法,那么编译器就会发出一个警告。鉴于 Objective-C 的前向机制,这并不会作为一个错误。如果目标对象是 id 类型,那么在编译期就不会有警告,但是运行期可能会有潜在的错误。

this,self 和 super

一个消息有两个特殊的目标对象:self 和 super。self 指当前对象(类似 C++ 的 this),super 指父对象。Objective-C 里面没有 this 指针,取而代之的是 self。

注意,self 不是一个关键字。实际上,它是每个消息接收时的隐藏参数,其值就是当前对象。它的值可以被改变,这一点不同于 C++ 的 this 指针。然而,这一点仅仅在构造函数中有用。

在方法中访问实例变量

同 C++ 一样,Objective-C在方法中也可以访问当前对象的实例变量。不同之处在于,C++ 需要使用 this->,而Objective-C 使用的是 self->。

C++

Objective-C

class Foo
{
    int x;
    int y;
 
    void  f(void);
};
 
void Foo::f(void)
{
    x = 1;
    int y; // 隐藏 this->y
    y = 2; // 使用局部变量 y
    this->y = 3; // 显式使用成员变量
}
@interface Foo : NSObject
{
    int x;
    int y;
}
 
-(void) f;
@end
 
@implementation Foo
 
-(void) f
{
    x = 1;
    int y; // 隐藏 super->y
    y = 2; // 使用局部变量 y
    self->y = 3; // 显式使用成员变量
}
@end

原型的 id、签名和重载

函数就是一段能够被引用的代码,例如使用函数指针。一般的,方法名会作为引用方法的唯一 id,但是,这就需要小心有重载的情况。C++ 和 Objective-C 使用截然不同的两种方式去区分:前者使用参数类型,后者使用参数标签。

在 C++ 中,只要函数具有不同的参数类型,它们就可以具有相同的名字。const 也可以作为一种重载依据。

C++

int f(int);
int f(float); // 允许,float 和 int 是不同类型
 
class Foo
{
public:
    int g(int);
    int g(float); // 允许,float 和 int 是不同类型
    int g(float) const; // 允许,const 可以作为重载依据
};
 
class Bar
{
public:
    int g(int); // 允许,我们使用的是 Bar::,而不是 Foo::
}

在 Objective-C 中,所有的函数都是普通的 C 函数,不能被重载(除非指定使用 C99标准)。方法则具有不同的语法,重载的依据是 label。

Objective-C

int f(int);
int f(float); // 错误!C 函数不允许重载
 
@interface Foo : NSObject
{
}
 
-(int) g:(int) x;
-(int) g:(float) x; // 错误!类型不同不作为重载依据,同上一个没有区别
 
-(int) g:(int) x :(int) y;   // 正确:两个匿名 label
-(int) g:(int) x :(float) y; // 错误:同上一个没有区别
 
-(int) g:(int) x andY:(int) y;   // 正确:第二个 label 是 “andY”
-(int) g:(int) x andY:(float) y; // 错误:同上一个没有区别
-(int) g:(int) x andAlsoY:(int) y; // 正确:第二个 label 是 “andAlsoY”
 
@end

基于 label 的重载可以很明白地解释方法的名字,例如:

@interface Foo : NSObject {}
 
// 方法名是“g”
-(int) g;
 
// 方法名是“g:”
-(int) g:(float) x;
 
// 方法名是“g::”
-(int) g:(float) x :(float) y;
 
// 方法名是“g:andY:”
-(int) g:(float) x andY:(float) y;
 
// 方法名是“g:andZ:”
-(int) g:(float) x andZ:(float) z;
@end

显然,Objective-C 的方法使用 label 区分,而不是类型。利用这种机制,我们就可以使用选择器 selector 来指定一个方法,而不是“成员函数指针”。

从 C++到 Objective-C(5):类和对象(续二)

成员函数的指针:选择器

在 Objective-C 中,方法具有包含了括号和标签的特殊语法。普通的函数不能使用这种语法。在 Objective-C 和 C 语言中,函数指针具有相同的概念,但是对于成员函数指针则有所不同。

在 C++ 中,尽管语法很怪异,但确实兼容 C 语言的:成员函数指针也是基于类型的。

C++

class Foo

{

public:

    int f(float x) {...}

};

 

Foo bar

int (Foo::*p_f)(float) = &Foo::f; // Foo::f 函数指针

(bar.*p_f)(1.2345); // 等价于 bar.f(1.2345);

在 Objective-C 中,引入了一个新的类型:指向成员函数的指针被称为选择器 selector。它的类型是 SEL,值通过 @selector 获得。@selector 接受方法名(包括 label)。使用类 NSInvocation 则可以通过选择器调用方法。大多时候,工具方法族 performSelector: (继承自 NSObject)更方便,约束也更大一些。其中最简单的三个是:

-(id) performSelector:(SEL)aSelector;

-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter;

-(id) performSelector:(SEL)aSelector withObject:(id)anObjectAsParameter

                                     withObject:(id)anotherObjectAsParameter;

这些方法的返回值同被调用的函数的返回值是一样的。对于那些参数不是对象的方法,应该使用该类型的包装类,如 NSNumber 等。NSInvocation 也有类似的功能,并且更为强大。

按照前面的说法,我们没有任何办法阻止在一个对象上面调用方法,即便该对象并没有实现这个方法。事实上,当消息被接收到之后,方法会被立即触发。但是,如果对象并不知道这个方法,一个可被捕获的异常将被抛除,应用程序并不会被终止。我们可以使用 respondsToSelector: 方法来检查对象是否可被触发方法。

最后,@selector 的值是在编译器决定的,因此它并不会减慢程序的运行效率。

Objective-C

@interface Slave : NSObject {}

 

-(void) readDocumentation:(Document*)document;

@end

 

// 假设 array[] 是包含 10 个 Slave 对象的数组,

// document 是一个 Document 指针

// 正常的方法调用是

for(i=0 ; i<10 ; ++i)

    [array[i] readDocumentation:document];

 

// 下面使用 performSelector: 示例:

for(i=0 ; i<10 ; ++i)

    [array[i] performSelector:@selector(readDocumentation:)

                   withObject:document];

 

// 选择器的类型是 SEL

// 下面代码并不比前面的高效,因为 @selector() 是在编译器计算的

SEL methodSelector = @selector(readDocumentation:);

for(i=0 ; i<10 ; ++i)

    [slaves[i] performSelector:methodSelectorwithObject:document];

 

// 对于一个对象“foo”,它的类型是未知的(id)

// 这种测试并不是强制的,但是可以避免没有 readDocumentation: 方法时出现异常

if ([foo respondsToSelector:@selector(readDocumentation:)])

    [foo performSelector:@selector(readDocumentation:) withObject:document];

因此,选择器可被用作函数参数。通用算法,例如排序,就可以使用这种技术实现。

严格说来,选择器并不是一个函数指针。它的底层实现是一个 C 字符串,在运行时被注册为方法的标识符。当类被加载之后,它的方法会被自动注册到一个表中,所以 @selector 可以很好的工作。根据这种实现,我们就可以使用 == 来判断内存地址是否相同,从而得出选择器是否相同,而无需使用字符串函数。

方法的真实地址,也就是看做 C 字符串的地址,其实可以看作是 IMP 类型(我们以后会有更详细的说明)。这种类型很少使用,除了在做优化的时候。例如虚调用实际使用选择器处理,而不是 IMP。等价于 C++ 函数指针的 Objective-C 的概念是选择器,也不是 IMP。

最后,你应该记得我们曾经说过 Objective-C 里面的 self 指针,类似于 C++ 的 this 指针,是作为每一个方法的隐藏参数传递的。其实这里还有第二个隐藏参数,就是 _cmd。_cmd 指的是当前方法。

@implementation Foo

 

-(void) f:(id)parameter // 等价于 C 函数 void f(id self, SEL _cmd,id parameter)

{

    id currentObject = self;

    SEL currentMethod = _cmd;

    [currentObjectperformSelector:currentMethod

                        withObject:parameter]; // 递归调用

    [self performSelector:_cmd withObject:parameter]; // 也是递归调用

}

@end

参数的默认值

Objective-C 不允许参数带有默认值。所以,如果某些参数是可选的,那么就应当创建多个方法的副本。在构造函数中,这一现象成为指定构造函数(designated initializer)。

可变参数

Objective-C 允许可变参数,语法同 C 语言一样,使用 … 作为最后一个参数。这实际很少用到,即是 Cocoa 里面很多方法都这么使用。

匿名参数

C++ 允许匿名参数,它可以将不使用的参数类型作为一种占位符。Objective-C 不允许匿名参数。

原型修饰符(const,static,virtual,”= 0″,friend,throw)

在 C++ 中,还有一些可以作为函数原型的修饰符,但在 Objective-C 中,这都是不允许的。以下是这个的列表:

·        const:方法不能使用 const 修饰。既然没有了 const,也就不存在 mutable 了;

·        static:用于区别实例方法和类方法的是原型前面的 – 和 +;

·        virtual:Objective-C 中所有方法都是 virtual 的,因此没有必要使用这个修饰符。纯虚方法则是声明为一个典型的协议 protocol;

·        friend:Objective-C 里面没有 friend 这个概念;

·        throw:在 C++ 中,可以指定函数会抛除哪些异常,但是 Objective-C 不能这么做。

从 C++到 Objective-C(6):类和对象(续三)

消息和消息传输

给 nil 发送消息

默认情况下,给 nil 发送消息也是合法的,只不过这个消息被忽略掉了。这种机制可以避免很多检查指针是否为空的情况。不过,有些编译器,比如 GCC,也允许你通过编译参数的设置关闭这一特性。

将消息代理给未知对象

代理 delegation 是 Cocoa 框架中 UI 元素的一个很常见的部分。代理可以将消息转发给一个未知的对象。通过代理,一个对象可以将一些任务交给另外的对象。

// 设置一个辅助对象 assistant
-(void) setAssistant:(id)slave
{
    [assistant autorelease];
    assistant = [slave retain];
}
// 方法 performHardWork 使用代理
-(void) performHardWork:(id)task
{
// assistant 在编译期是未知的
// 我们首先要检查它是否能够响应消息
if ([assistant respondsToSelector:@selector(performHardWork:)])
    [assistant performHardWork:task];
else
    [self findAnotherAssistant];
}

转发:处理未知消息

在 C++ 中,如果对象函数没有实现,是不能通过编译的。Objective-C 则不同,你可以向对象发送任何消息。如果在运行时无法处理,这个消息就被忽略了(同时会抛出一个异常)。除了忽略它,另外的处理办法是将消息转发给另外的对象。

当编译器被告知对象类型时,它可以知道对象可以处理哪些消息,因此就可以知道消息发出后是否会失败,也就可以抛出异常。这也就是为什么消息在运行时被执行,但是编译时就可以发出警告。这并不会引发错误,同时还有另外的选择:调用 forwardInvocation: 方法。这个方法可以将消息进行转发。这个方法是 NSObject 的,默认不做任何操作。下面代码就是一种实现:

-(void) forwardInvocation:(NSInvocation*)anInvocation
{
    // 如果该方法被调用,意味着我们无法处理这个消息
    // 错误的选择器(也就是调用失败的那个方法名)可以通过
    // 向 anInvocation 对象发送“selector” 获得
    if ([anotherObject respondsToSelector:[anInvocation selector]])
        [anInvocation invokeWithTarget:anotherObject];
    else  // 不要忘记调用父类的实现
        [super forwardInvocation:anInvocation];
}

即是在最后,这个消息在 forwardInvocation: 中被处理,respondsToSelector: 还是会返回 NO。事实上,respondsToSelector:并不是用来检查 forwardInvocation: 是否被调用的。

使用这种转发机制有时候被认为是一种不好的习惯,因为它会隐藏掉本应引发错误的代码。事实上,一些很好的设计同样可以使用这种机制实现,例如 Cocoa 的 NSUndoManager。它允许一种对异常友好的语法:undo manager 可以记录方法调用历史,虽然它并不是那些调用的接收者。

向下转型

C++ 中,父类指针调用子类的函数时,需要有一个向下转型的操作(downcasting),使用dynamic_cast 关键字。在Objective-C 中,这是不必要的。因为你可以将任何消息发送给任何对象。但是,为了避免编译器的警告,我们也可以使用简单的转型操作。Objective-C 中没有类似 C++ 的专门的向下转型的操作符,使用 C 风格的转型语法就可以了。

// NSMutableString 是 NSString 的子类
// 允许字符串修改的操作
// "appendString:" 仅在 NSMutableString 中实现
NSMutableString* mutableString = ... 初始化可变字符串 ...
NSString* string = mutableString;// 传给 NSString 指针
// 这些调用都是合法的
[string appendString:@"foo"]; // 有编译器警告
[(NSMutableString*)string appendString:@"foo"]; // 无警告
[(id)string appendString:@"; // 无警告

从 C++到 Objective-C(7):继承

简单继承

Objective-C 也有继承的概念,但是不能多重继承。不过,它也有别的途径实现类似多重继承的机制,这个我们后面会讲到。

C++

Objective-C

class Foo : public Bar,
            protected Wiz
{
}
@interface Foo : Bar // 单继承
// 如果要同时“继承” Wiz,需要使用另外的技术
{
}
@end

在 C++ 中,一个类可以继承自一个或多个类,使用 public、protected 以及 private 修饰符。子类的函数如果要调用父类的版本,需要使用 :: 运算符,例如 Bar::,Wiz:: 等。

在 Objective-C中,一个类只能继承一个父类,并且只能是 public 的(这和 Java 是一致的)。同样类似 Java,如果你要在子类中调用父类的函数,需要使用 super。

多重继承

Java 同样不允许多重继承。但是它提供了 interface 来模拟多重继承。类似的,Objective-C 也有同样的机制,这就是协议 protocol 和分类 categories。我们将在后面的内容详细讲述这两种技术。

虚拟性

虚方法

在 Objective-C 中,所有方法都是虚的,因此,没有 virtual 关键字或其等价物。

虚方法重定义

在 Objective-C 中,你可以定义一个没有在 @interface 块里面声明的方法。但这并不是一种替代 private 的机制,因为这种方法实际是能够被调用的(回想下,Objective-C 中方法的调用是在运行期决定的)。不过,这确实能够把接口定义变得稍微干净了一些。

这并不是一种坏习惯,因为有时你不得不重定义父类的函数。由于所有方法都是虚的,你无需像 C++ 一样在声明中显式写明哪些函数是 virtual 的,这种做法就成为一种隐式的重定义。很多继承西 NSObject 的方法都是是用这种方法重定义的。例如构造方法 init,析构方法 dealloc,view 类的 drawRect: 等等。这样的话,接口就变得更简洁,更易于阅读。不好之处就是,你不能知道究竟哪些方法被重定义了。

纯虚方法则是使用正式协议 formal protocols 来实现。

虚继承

Objective-C 中不允许多重继承,因此也就没有虚继承的问题。

协议

Java 和 C# 使用接口 interface 的概念来弥补多重继承的不足。Objective-C 也使用了类似的机制,成为协议 protocol。在 C++ 中,这种概念是使用抽象类。协议并不是真正的类:它只能声明方法,不能添加数据。有两种类型的协议:正式的 formal 和非正式的 informal。

正式协议

正式协议的方法,所有实现这个协议的类都必须实现。这就是一种验证,也就是说,只要这个类说实现这个协议,那么它肯定可以处理协议中规定的方法。一个类可以实现任意多个协议。

C++

class MouseListener
{
public:
    virtual bool mousePressed(void) = 0; // 纯虚方法
    virtual bool mouseClicked(void) = 0; // 纯虚方法
};
 
class KeyboardListener
{
public:
    virtual bool keyPressed(void) = 0; // 纯虚方法
};
 
class Foo : public MouseListener,  public KeyboardListener {...}
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
// 然后 Foo 就可以作为鼠标和键盘的事件监听器

Objective-C

@protocol MouseListener
 
-(BOOL) mousePressed;
-(BOOL) mouseClicked;
 
@end
 
@protocol KeyboardListener
 
-(BOOL) keyPressed;
 
@end
 
@interface Foo : NSObject <MouseListener, KeyboardListener>
{
...
}
@end
// Foo 必须实现 mousePressed, mouseClicked 和 keyPressed
// 然后 Foo 就可以作为鼠标和键盘的事件监听器

C++ 中,协议可以由抽象类和纯虚函数实现。C++ 的抽象类要比 Objective-C 的协议强大的多,因为抽象类可以带有数据。

Objective-C 中,协议是一个特殊的概念,使用尖括号 <…> 表明。注意,尖括号在 Objective-C 中不是模板的意思,Objective-C 中没有类似 C++ 模板的概念。

一个类也可以不经过协议声明,直接实现协议规定的方法。此时,conformsToProtocol: 方法依然返回 NO。出于性能考虑,conformsToProtocol: 方法只检查类接口的声明,不会一个方法一个方法的对比着检查。conformsToProtocol: 的返回值并不会作为是否调用方法的依据。下面是这个方法的原型:

-(BOOL) conformsToProtocol:(Protocol*)protocol
// Protocol 对象可以由 @protocol(协议名) 返回

实现了正式协议的对象的类型同协议本身是兼容的。这一机制可以作为协议的筛选操作。例如:

// 下面方法是 Cocoa 提供的标准方法
// 方法参数可以是任意类型 id,但是必须兼容 NSDraggingInfo 协议
-(NSDragOperation) draggingEntered:(id )sender;

可选方法

有时我们需要这么一种机制:我们的类需要实现一部分协议中规定的方法,而不是整个协议。例如在 Cocoa 中,代理的概念被广泛使用:一个类可以给定一个辅助类,由这个辅助类去完成部分任务。

一种实现是将一个协议分割成很多小的协议,然后这个类去实现一个协议的集合。不过这并不具有可操作性。更好的解决方案是使用非正式协议。在 Objective-C 1.0 中就有非正式协议了,Objective-C 2.0 则提出了新的关键字 @optional 和 @required,用以区分可选方法和必须方法。

@protocol Slave
 
@required // 必须部分
-(void) makeCoffee;
-(void) duplicateDocument:(Document*)document count:(int)count;
 
@optional // 可选部分
-(void) sweep;
 
@required // 又是一个必须部分
-(void) bringCoffee;
@end

非正式协议

非正式协议并不是真正的协议,它对代码没有约束力。非正式协议允许开发者将一些方法进行归类,从而可以更好的组织代码。所以,非正式协议并不是协议的宽松版本。另外一个相似的概念就是分类。

让我们想象一个文档管理的服务。假设有绿色、蓝色和红色三种文档,一个类只能处理蓝色文档,而 Slave 类使用三个协议 manageBlueDocuments, manageGreenDocuments 和 manageRedDocuments。Slave 可以加入一个分类 DocumentsManaging,用来声明它能够完成的任务。分类名在小括号中被指定:

@interface Slave (DocumentsManaging)
 
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) trashBlueDocuments:(BlueDocument*)document;
 
@end

任何类都可以加入 DocumentsManaging 分类,加入相关的处理方法:

@interface PremiumSlave (DocumentsManaging)
 
-(void) manageBlueDocuments:(BlueDocument*)document;
-(void) manageRedDocuments:(RedDocument*)document;
 
@end

另一个开发者就可以浏览源代码,找到了 DocumentsManaging 分类。如果他觉得这个分类中有些方法可能对自己,就会检查究竟哪些能够使用。即便他不查看源代码,也可以在运行时指定:

if([mySlave respondsToSelector:@selector(manageBlueDocuments:)])
    [mySlave  manageBlueDocuments:document];

严格说来,除了原型部分,非正式协议对编译器没有什么意义,因为它并不能约束代码。不过,非正式协议可以形成很好的自解释性代码,让 API 更具可读性。

从 C++到 Objective-C(8):继承(续)

Protocol 对象

运行时,协议就像是类对象,其类型是 Protocol*。例如,conformsToProtocol: 方法就需要接受一个 Protocol* 类型的参数。@protocol 关键字不仅用于声明协议,还可以用于根据协议名返回 Protocol* 对象。

Protocol* myProtocol = @protocol(协议名)

远程对象的消息传递

由于 Objective-C 的动态机制,远程对象之间的消息传递变得很简单。所谓远程对象,是指两个或多个处于不同程序,甚至不同机器,但是可以通过代理完成同一任务,或者交换信息的对象。正式协议就是一种可以确保对象提供了这种服务的有效手段。正式协议还提供了很多额外的关键字,可以更好的说明各种参数。这些关键字分别是 in, out, inout, bycopy, byref 和 oneway。这些关键字仅对远程对象有效,并且仅可以在协议中使用。出了协议,它们就不被认为是关键字。这些关键字被插入到在协议中声明的方法原型之中,提供它们所修饰的参数的额外信息。它们可以告知,哪些是输入参数,哪些是输出参数,哪些使用复制传值,哪些使用引用传值,方法是否是同步的等等。以下是详细说明:

·        in:参数是输入参数;

·        out:参数是输出参数;

·        inout:参数即是输入参数,又是输出参数;

·        bycopy:复制传值;

·        byref:引用传值;

·        oneway:方法是异步的,也就是不会立即返回,因此它的返回值必须是 void。

例如,下面就是一个返回对象的异步方法:

-(oneway void) giveMeAnObjectWhenAvailable:(bycopy out id *)anObject;

默认情况下,参数都被认为是 inout 的。如果参数由 const 修饰,则被当做 in 参数。为参数选定是 in 还是 out,可以作为一种优化手段。参数默认都是传引用的,方法都是同步的(也就是不加 oneway)。对于传值的参数,也就是非指针类型的,out 和 inout 都是没有意义的,只有 in 是正确的选择。

分类

创建类的分类 categories,可以将一个很大的类分割成若干小部分。每个分类都是类的一部分,一个类可以使用任意多个分类,但都不可以添加实例数据。分类的好处是:

·        对于精益求精的开发者,分类提供了一种划分方法的机制。对于一个很大的类,它可以将其划分成不同的角色;

·        分类允许分开编译,也就是说,同一个类也可以进行多人的分工合作;

·        如果把分类的声明放在实现文件(.m)中,那么这个分类就只在文件作用域中可见(虽然这并没有调用上的限制,如果你知道方法原型,依然可以调用)。这样的分类可以取一个合适的名字,比如 FooPrivateAPI;

·        一个类可以在不同程序中有不同的扩展,而不需要丢弃通用代码。所有的类都可以被扩展,甚至是 Cocoa 中的类。

最后一点尤其重要。很多开发人员都希望标准类能够提供一些对他们而言很有用的方法。这并不是一个很困难的问题,使用继承即可实现。但是,在单继承的环境下,这会造成出现很多的子类。仅仅为了一个方法就去继承显得有些得不偿失。分类就可以很好的解决这个问题:

C++

Objective-C

class MyString : public string
{
public:
    // 统计元音的数目
    int vowelCount(void);
};
 
int MyString::vowelCount(void)
{
...
}
@interface NSString (VowelsCounting)
// 注意并没有使用 {}
-(int) vowelCount; // 统计元音的数目
@end
 
@implementation NSString (VowelsCounting)
-(int) vowelCount
{
...
}
@end

在 C++ 中,这是一个全新的类,可以自由使用。

在 Objective-C 中,NSString 是 Cocoa 框架的一个标准类。它是使用分类机制进行的扩展,只能在当前程序中使用。注意此时并没有新增加类。每一个 NSString 对象都可以从这个扩展获得统计元音数目的能力,甚至常量字符串也可以。同时注意,分类不能增加实例数据,因此没有花括号块。

分类也可以使匿名的,更适合于 private 的实现:

@interface NSString ()
// 注意并没有使用 {}
-(int) myPrivateMethod;
@end
 
@implementation NSString ()
-(int) myPrivateMethod
{
...
}
@end

混合使用协议、分类和子类

混合使用协议、分类和子类的唯一限制在于,你不能同时声明子类和分类。不过,你可以使用两步来绕过这一限制:

@interface Foo1 : SuperClass  //ok
@end
 
@interface Foo2 (Category)   //ok
@end
 
// 下面代码会有编译错误
@interface Foo3 (Category)  : SuperClass
@end
 
// 一种解决方案
@interface Foo3 : SuperClass  // 第一步
@end
 
@interface Foo3 (Category) // 第二步
@end

从 C++到 Objective-C(9):实例化

类的实例化位导致两个问题:构造函数、析构函数和赋值运算符如何实现,以及如何分配内存。

在 C++ 中,变量默认是“自动的”:除非被声明为 static,否则变量仅在自己的定义块中有意义。动态分配的内存可以一直使用,直到调用了 free() 或者 delete。C++ 中,所有对象都遵循这一规则。

然而在Objective-C 中,所有对象都是动态分配的。其实这也是符合逻辑的,因为 C++ 更加 static,而Objective-C 则更加动态。除非能够在运行时动态分配内存,否则 Objective-C 实现不了这么多动态的特性。

构造函数和初始化函数

分配 allocation 和初始化 initialization 的区别

在 C++ 中,内存分配和对象初始化都是在构造函数中完成的。在 Objective-C 中,这是两个不同的函数。

内存分配由类方法 alloc 完成,此时将初始化所有的实例数据。实例数据将被初始化为 0,除了一个名为 isa 的 NSObject 的指针。这个指针将在运行时指向对象的实际类型。实例数据根据传入的参数初始化为某一特定的值,这一过程将在一个实例方法 instance method 中完成。这个方法通常命名为 init。因此,构造过程被明确地分为两步:内存分配和初始化。alloc 消息被发送给类,而 init 消息则被发送给由 alloc 创建出来的新的对象。初始化过程不是可选的,alloc 之后应该跟着 init,之后,父类的 init 也会被调用,直到 NSObject 的 init 方法。这一方法完成了很多重要的工作。

在 C++ 中,构造函数的名字是规定好的,必须与类名一致。在 Objective-C 中,初始化方法与普通方法没有什么区别。你可以用任何名字,只不过通常都是选用 init 这个名字。然而,我们还是强烈建议,初始化方法名字一定要用 init 或者 init 开头的字符串

使用 alloc 和 init

调用 alloc 之后将返回一个新的对象,并且应该给这个对象发送一个 init 消息。init 调用之后也会返回一个对象。通常,这就是初始化完成的对象。有时候,如果使用单例模式,init 可能会返回另外的对象(单例模式要求始终返回同一对象)。因此,init 的返回值不应该被忽略。通常,alloc 和 init 都会在一行上。

C++

Foo* foo = new Foo;

Objective-C

Foo* foo1 = [Foo alloc];
[foo1 init]; // 这是不好的行为:应该使用 init 的返回值
Foo* foo2 = [Foo alloc];
foo2 = [foo2 init]; // 正确,不过看上去很啰嗦
Foo* foo3 = [[Foo alloc] init]; // 正确,这才是通常的做法

为检查内存分配是否成功,C++ 可以判断 new 返回的指针是否是 0(如果使用的是  new(nothrow) 运算符)。在 Objective-C 中,检查返回值是否是 nil 就已经足够了。

初始化方法的正确示例代码

一个正确的初始化方法应该有如下特点:

·        名字以init 开始;

·        返回能够使用的对象;

·        调用父类的 init 方法,直到 NSObject 的init 方法被调用;

·        保存[super init...] 的返回值;

·        处理构造期间出现的任何错误,无论是自己的还是父类的。

下面是一些代码:

C++

class Point2D
{
public:
    Point2D(int x, int y);
private:
    int x;
    int y;
};
Point2D::Point2D(int anX, int anY) {x = anX; y = anY;}
...
 
Point2D  p1(3,4);
Point2D* p2 = new Point2D(5, 6);

Objective-C

@interface Point2D : NSObject
{
    int x;
    int y;
}
 
// 注意,在 Objective-C 中,id 类似于 void*
// (id) 就是对象的“一般”类型
-(id) initWithX:(int)anX andY:(int)anY;
@end
 
@implementation Point2D
 
-(id) initWithX:(int)anX andY:(int)anY
{
    // 调用父类的初始化方法
    if (!(self = [super init])) // 如果父类是 NSObject,必须进行 init 操作
        return nil; // 如果父类 init 失败,返回 nil
    // 父类调用成功,进行自己的初始化操作
   self->x = anX;
    self->y = anY;
    return self; // 返回指向自己的指针
}
@end
 
...
Point2D* p1 = [[Point2D alloc] initWithX:3 andY:4];

从 C++到 Objective-C(10):实例化(续)

self = [super init...]

在上一篇提到的代码中,最不可思议的可能就是这句 self = [super init...]。回想一下,self 是每个方法的一个隐藏参数,指向当前对象。因此,这是一个局部变量。那么,为什么我们要改变一个局部变量的值呢?事实上,self 必须要改变。我们将在下面解释为什么要这样做。

[super init] 实际上返回不同于当前对象的另外一个对象。单例模式就是这样一种情况。然而, 有一个 API 可以用一个对象替换新分配的对象。Core Data(Apple 提供的 Cocoa 里面的一个 API)就是用了这种 API,对实例数据做一些特殊的操作,从而让这些数据能够和数据库的字段关联起来。当继承 NSManagedObject 类的时候,就需要仔细对待这种替换。在这种情形下,self 就要指向两个对象:一个是 alloc 返回的对象,一个是 [super init] 返回的对象。修改 self 的值对代码有一定的影响:每次访问实例数据的时候都是隐式的。正如下面的代码所示:

@interface B : A

{

int i;

}

 

@end

 

@implementation B

 

-(id) init

{

    // 此时,self 指向 alloc 返回的值

    // 假设 A 进行了替换操作,返回一个不同的 self

    id newSelf = [super init];

    NSLog(@"%d", i); // 输出 self->i 的值

    self = newSelf; // 有人会认为 i 没有变化

    NSLog(@"%d", i); // 事实上,此时的 self->i, 实际是 newSelf->i,

                     // 和之前的值可能不一样了

    return self;

}

 

@end

...

B* b = [[B alloc] init];

self = [super init] 简洁明了,也不必担心以后会引入 bug。然而,我们应该注意旧的 self 指向的对象的命运:它必须被释放。第一规则很简单:谁替换 self 指针,谁就要负责处理旧的 self 指针。在这里,也就是 [super init] 负责完成这一操作。例如,如果你创建 NSManagedObject 子类(这个类会执行替换操作),你就不必担心旧的 self 指针。事实上,NSManagedObject 的开发者必须考虑这种处理。因此,如果你要创建一个执行替换操作的类,你必须知道如何在初始化过程中释放旧有对象。这种操作同错误处理很类似:如果因为非法参数、不可访问的资源造成构造失败,我们要如何处理?

初始化错误

初始化出错可能发生在三个地方:

1.    调用 [super init...] 之前:如果构造函数参数非法,那么初始化应该立即停止;

2.    调用 [super init...] 期间:如果父类调用失败,那么当前的初始化操作也应该停止;

3.    调用 [super init...] 之后:例如资源分配失败等。

在上面每一种情形中,只要失败,就应该返回 nil;相应的处理应该由发生错误的对象去完成。这里,我们主要关心的是1, 3情况。要释放当前对象,我们调用 [self release] 即可。

在调用 dealloc 之后,对象的析构才算完成。因此,dealloc 的实现必须同初始化方法兼容。事实上,alloc 将所有的实例数据初始化成 0 是相当有用的。

@interface A : NSObject {

    unsigned int n;

}

 

-(id) initWithN:(unsigned int)value;

@end

 

@implementation A

 

-(id) initWithN:(unsigned int)value

{

    // 第一种情况:参数合法吗?

    if (value == 0) // 我们需要一个正值

    {

        [self release];

        return nil;

    }

    // 第二种情况:父类调用成功吗?

    if (!(self = [super init])) // 即是 self 被替换,它也是父类

        return nil; // 错误发生时,谁负责释放 self?

    // 第三种情况:初始化能够完成吗?

    n = (int)log(value);

    void* p = malloc(n); // 尝试分配资源

    if (!p) // 如果分配失败,我们希望发生错误

    {

        [self release];

        return nil;

    }

}

@end

将构造过程合并为 alloc+init

有时候,alloc 和 init 被分割成两个部分显得很罗嗦。幸运的是,我们也可以将其合并在一起。这主要牵扯到 Objective-C 的内存管理机制。简单来说,作为一个构造函数,它的名字必须以类名开头,其行为类似 init,但要自己实现 alloc。然而,这个对象需要注册到 autorelease 池中,除非发送 retain 消息,否则其生命周期是有限制的。以下即是示例代码:

// 啰嗦的写法

NSNumber* tmp1 = [[NSNumber alloc] initWithFloat:0


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
上一篇:
c#访问gooleAPI(原创)发布时间:2022-07-13
下一篇:
C++11 std::ref使用场景发布时间:2022-07-13
热门推荐
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap