从0开始学习Object- C开发(基础)

1,246 阅读34分钟

本人:zackzhang

正在入门OC,如有错误欢迎前指正

若有错误可通过飞书联系,欢迎相互探讨

准备工作

一台Mac和一台iOS设备

注册Apple ID,在Apple Store中下载

了解一下面相对象的鼻祖语言SmalltalkSmalltalk 语言(面向对象的鼻祖语言)

Objective-c入门

语言概述

objective-c是在C语言的基础上发展的一门语言,本质上就是在C语言的主体傻姑娘加入面向对象的特性

代码练习

选择macOS中的Commend,建立完成即可运行代码。

头文件

  • .h:头文件。头文件包含类,类型,函数和常数的声明(和C语言无区别
  • .m:源代码文件。源代码文件扩展名,可以包含objective-c和C代码
  • .mm:源代码文件。在.m的基础上可以包含C++代码。(最好是确实需要C++类或者特效的时候才用)

OC新增了一个#import,本质上无差别,但倾向于使用#import

OC与C的区别

后缀不同一个是.c一个是.m,m代表message

#import是对include的增强,同一个文件无论#import多少次,都只会包含一次(原理: 有个判重)

数据结构有所不同(字符串

OC编译、连接、运行

  1). 在.m文件中写规范的源代码

  2). 使用编译器编译

    cc -c xx.m 预编译、检查语法、编译

  3). cc xx.o

  cc xx. o -framework (有用到框架的写,无则忽略)

  4). 链接成功后,会生成 x.out 执行即可

源文件目标文件执行文件
C.c.o.out
OC.m.o.out

OC 中的数据类型

支持C中的所有数据类型

基础类型

Int double float char

构造类型

数组 结构体 枚举

指针类型

空类型

typedef自定义类型

BOOL类型

  可以存储YES或者NO

  本质上typedef signed char BOOL. 是一个有符号的char变量

  YES和NO本质上 #define YES((BOOL)1)、#define NO ((BOOL)0)

Boolean类型

  可以存储true或者false

  本质上typedef unsigned char Boolean. 是一个无符号的char变量

  YES和NO本质上 #define true 1、#define false 0

class类型

id类型。万能指针

Nil 与NULL相同(两者意思一样,但是建议C指针用NULL,OC用NIL)

SEL 方法选择器

Block 代码段

语法

OC只有面向对象的语法源于Smalltalk消息传递风格,其它非面向对象的语法都与C语言一致(两者有些细微的地方不一致即表达的意思不一定相同)

Objective-C的面向对象语法源于Smalltalk消息传递风格。所有其他非面向对象的语法,包括变量类型,预处理器(preprocessing),流程控制,函数声明与调用皆与C语言完全一致。但有些C语言语法合法代码在objective-c中表达的意思不一定相同,比如某些布尔表达式,在C语言中返回值为true,但在Objective-C若与yes直接相比较,函数将会出错,因为在Objective-C中yes的值只表示为1。

你的第一个oc语言程序:

#import <Foundation/Foundation.h>
Int main(int argc, char *argv[])  {
        @autoreleasepool {
                NSLog(@“Hello World”);
                }
        return 0;
}

消息传递

Objective-C最大的特色是承自Smalltalk的消息传递模型(message passing), 在objective-c里,与其说对象互相调用方法,不如说时互相发送消息精准。在oc环境中,类别与消息的关系比较松散,调用方法视为对对象发送消息,所有方法都被视为对消息的回应。所有消息处理直到运行时(runtime)才会动态决定,并交由类别自行决定如何处理收到的消息。也就是说,一个类别不保证一定会回应收到的消息,如果类别收到了一个无法处理的消息,程序只会抛出异常,不会出错或崩溃。下面我们用c++和oc举两个例子:

在c++里,调用一个方法

obj.method(argument);

在oc中,发送一个消息给对象

[obj  method:argument];

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

类 class

Objective-C 中所有的对象都是指针类 型。换句话说,你永远不要单独使用 String,而是应该使用 String *。所有的 Objective-C 对象都是在堆中分配内存的,而不是在栈中进行分配(也并非全部,这里也有你稍后会遇到 的例外)。如果你声明一个静态分配的对象,而非一个指针,编译器将会为你提供有用的提示。

如同所有其他的面向对象语言,类是 Objective-C 用来封装数据,以及操作数据的行为的基础结构。对象就是类的运行期间实例,它包含了类声明的实例变量自己的内存拷贝,以及类成员的指针。Objective-C 的类规格说明包含了两个部分:声明interface与实现implementation 。声明(interface)部分包含了类声明和实例变量的定义,以及类相关的方法。实现(implementation)部分包含了类方法的实际代码。

下图展现了声明一个叫做 MyClass 的类的语法,这个类继承自 NSObject 基础类(或称根类)。类声明总是由 @interface 编译选项开始,由 @end 编译选项结束。类名之后的(用冒号分隔的)是父类的名字。类的实例(或者成员)变量声明在被大括号包含的代码块中。实例变量块后面就是类声明的方法的列表。每个实例变量和方法声明都以分号结尾。

类的定义文件遵循C语言之惯例以.h为后缀,实现文件以.m为后缀。

方法前面的 +/- 号代表函数的类型:加号(+)代表类方法(class method),不需要实例就可以调用,与C++ 的静态函数(static member function)相似。减号(-)即是一般的实例方法(instance method)。

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

-(void)setColorToRed: (float)red Green:(float)green Blue:(float)blue;                //声明发法
 [myColor serColorToRed: 1.0 Green:0.8 Blue: 0.2];                        //呼叫方法(调用方法) 

这个方法的签名是setColorToRed:Green:Blue:。每个冒号后面都带着一个float类别的参数

声明 interface 与实现 implementation都可定义变量

不只Interface区块可定义实体变量,Implementation区块也可以定义实体变量,两者的差别在于访问权限的不同,Interface区块内的实体变量默认权限为protected,宣告于implementation区块的实体变量则默认为private,故在Implementation区块定义私有成员更匹配面向对象之封装原则,因为如此类别之私有信息就不需曝露于公开interface(.h文件)中。

内存分配和初始化

在 Mac 和 iOS 设备上运行的 Objective-C 中,所有对象 1 都是从 NSObject 类 2 向下继 承。这个根类实现基本的分配和初始化方法,会被其他所有的 Objective-C 对象继承。这些 方法中包含+alloc 和-init 方法,合并使用这两个方法可以创建给定类的新实例。

Objective-C 提供了一种单独的数据类型:id(与“did”押韵)。它指与它所属的类无关 的任何对象类型,并且可以指代实例和类自身。它实际上作为指向基础 C 类型的指针实现, 这种类型是所有 Objective-C 对象的基础。

Objective-C 中对象方法的默认返回类型(如果没有明确指定类型,那么会由编译器设定) 总是 id,而对于普通的 C 语言结构而言默认返回 int 类型。另外,Objective-C 对象有明确 的“零”值,定义为 nil。任何返回对象实例的方法都能返回 nil 作为零值或失败值。

-init 方法是最基本的初始化方法。当你创建自己的对象时包含它通常会很有用处,并 且大多数(如果不是全部的话)系统对象会在调用时适当地初始化自身。但是多数类使用传 参的方式实现更多特定的初始化方法。这些方法都以“init”开头以把它们标记成初始化方 法,但也可以在“init”后面采取除此之外的其他形式,因此-initWithString:、- initWithOrigin: andSize:等都是有效的初始化方法。 +alloc 和-init 都返回 id 类型,并且通常连在一起调用。+alloc 返回后立即传递-init 消 息,之后把调用的结果作为变量保存起来,否则不要使用+alloc 的结果。如果它不能执行 这个函数,那么函数可能返回 nil。但对-init 而言,该方法也会释放+alloc 分配的内存。

MyObject * obj1 = [[MyObject alloc] init]; 
NamedObject * obj2 = [[NamedObject alloc] initWithName: "aName"]; 
if(obj2 == nil ) { 
    // error allocating or initializing NamedObject instance 
} 

不需要显式声明这个对象支持+alloc 或-init 方法,这些方法是从 NSObject 超类 继承而来的。MyObject 可以重定义这些方法,但实际上 NSObject 已经声明了它们,所以不需要再重复这项工作。

内存管理(实践见进阶OC基础补充及进阶

三种 Objective-C 使用的不同类型的内存管理技术。

  ● 手动引用计数:这是指由程序员调用-retain、-release 和-autorelease 方法来管理单 个对象的引用计数。这已经成为 iOS 5.0 之前以及历代 OS X 中使用的标准模式。

  ● 垃圾回收(Garbage Collection,GC):伴随着 OS X 10.4 的到来,Objective-C 获得了 一种与其他许多当代编程语言相似的、自动管理内存的垃圾回收器。这使引用计 数方式变得不那么必要,它也能应用到系统中使用 C 语言代码做出的任何内存申 请上。它也提供了一个非常有用的“弱引用归零”系统。通过它,指向对象的弱 引用能一直保留,直至引用计数归零才失效。一旦对象被删除,所有指向它的弱 引用也都会被置为 0。但是,垃圾回收机制需要大量的资源,它太消耗资源,从而 不能在 iPhone 或 iPad 平台上部署。也因如此,在 OS X 上对垃圾回收机制的使用 也被正式弃用,取而代之的是列表中的下一项。

  ● 自动引用计数(Automatic Reference Counting,ARC):在 OS X 10.7 Lion 和 iOS 5.0 中介绍过(也能在认可的 OS X 10.6 和 iOS 4 中执行)的 ARC 技术把引用计数机制植 入语言运行时,并且加强了 LLVM 编译器对它的支持(想了解 LLVM 和其他 OS X 上的编译器的更多信息,请参阅附文“编译”)。这个机制使编译器可以准确地判 定对象在哪里保留、释放,以及自动释放,这意味着这项工作是由编译器完成的, 而非程序员。ARC 也包含了通过 OS X 的垃圾回收器生效的弱引用归零系统。ARC 和 GC 之间的区别在于:GC 会让对象积聚起来并且在一个特定的间隔之后释放, ARC 只是简单地替你插入相应的 retain/release 调用。因此没有内存积聚,也没有 高系统开销的收集阶段。事实上,编译器会严谨地优化整个 retain/release 循环。

使用 ARC 守则,对那些习惯于自动内存管理语言的人而言, 这种方式更加平易近人,而且他们更习惯使用这样的代码。另外,使用 ARC 可以允许我 们使用弱引用归零机制,这使得大量的代码更稳定且无差错。

但也不要以为你可以完全摆脱困难。你可能不再需要实际动手输入 retain 和 release, 但它还在起作用,也仍然可能遇到问题,例如可怕的 retain 循环。

面向对象语言中的实例变量通常保持着对除了对象自己定义的方法以外的其他任何 调用的密封性,Objective-C 也不例外。Objective-C 提供了一些作用域来指定访问单个变量 的级别。这些作用域都使用单独的关键字进行指定,任何关键字后面的变量都有相同的作 用域。这些关键字与其他流行的面向对象编程语言中的关键字很相似。

@public:后面的实例变量可以被所有对象访问。

@protected:后面的变量只能由该类和该类的子类访问。如果不进行指定,这将是 默认的作用域。

@private:后面的变量只能由该类进行访问,子类不能继承访问。

@package:特殊情况下,实例变量在相同的可执行映像(例如框架或插件)中是可以 访问的,就像@public 作用域一样,但在这些映像之外会被认为是@private 作用域。 这主要由苹果公司用于紧耦合框架类,这些框架的实例变量访问是共享的,但不 应该被导出到框架外。

@interface MyObject : NSObject { 
    @public
    int var1; 
    } 
    ... 
@end

创建对象

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

MyObject * my = [[MyObject alloc] init];
在Objective-C 2.0里,若创建对象不需要参数,则可直接使用new
MyObject * my = [MyObject new];

但是new仅仅是语法上的精简,但其实并不方便,为了其他操作,建议自己定义初始化的过程。

方法

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

方法声明包括方法类型标识符,返回值类型,一个或多个方法标识关键字,参数类型和名信息。下图展示 insertObject:atIndex: 实例方法的声明。声明由一个减号(-)开始,这表明这是一个实例方法。方法实际的名字(insertObject:atIndex:)是所有方法标识关键的级联,包含了冒号。冒号表明了参数的出现。如果方法没有参数,你可以省略第一个(也是唯一的)方法标识关键字后面的冒号

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

消息被中括号( [ 和 ] )包括。中括号中间,接收消息的对象在左边,消息(包括消息需要的任何参数)在右边。例如,给myArray变量传递消息insertObject:atIndex:消息,你需要使用如下的语法:

[myArray insertObject:anObj atIndex:0];

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

[[myAppObject getArray] insertObject:[myAppObject getObectToInsert] antIndex:0];

虽然前面的例子都是传递消息给某个类的实例,但是你也可以传递消息给类本身。当给类发消息,你指定的方法必须被定义为类方法,而不是实例方法。你可以认为类方法跟C++类里面的静态成员有点像(但是不是完全相同的)。类方法的典型用途是用做创建新的类实例的工厂方法,或者是访问类相关的共享信息的途径。类方法声明的语法跟实例方法的几乎完全一样,只有一点小差别。与实例方法使用减号作为方法类型标识符不同,类方法使用加号( + )。

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

SMutableArray*   myArray = nil;          // nil 基本上等同于 NULL
// 创建一个新的数组,并把它赋值给 myArray 变量
myArray = [NSMutableArray arrayWithCapacity:0];

方法和函数的区别:方法只能放在类里,函数无论类外还是类里都能直接使用

方法可以不被实现,编译器不会报错,只会警告

属性

属性是用来代替声明存取方法的便捷方式。属性不会在你的类声明中创建一个新的实例变量。他们仅仅是定义方法访问已有的实例变量的速记方式而已。暴露实例变量的类,可以使用属性记号代替getter和setter语法。类还可以使用属性暴露一些“虚拟”的实例变量,他们是部分数据动态计算的结果,而不是确实保存在实例变量内的。

实际上可以说,属性节约了你必须要写的大量多余的代码。因为大多数存取方法都是用类似的方式实现的,属性避免了为类暴露的每个实例变量提供不同的getter和setter的需求。取而代之的是,你用属性声明指定你希望的行为,然后在编译期间合成基于声明的实际的getter和setter方法。

可以对 Objective-C 中的属性使用一些限定符,用以说明如何使用这些属性。它们被 放置在@property 关键字后面的括号里,包含如下这些限定符:

  ● 访问(readonly、readwrite):表示该属性是可读写的还是只读的。默认是 readwrite。 对于单个属性只能指定为其中的一项。

  ● 线程安全(atomic、nonatomic):通过指定 atomic 关键字(默认项),这个属性的所有 合成访问器将会为保证线程安全而被锁定和合成。nonatomic 关键字不能这样做而 且也更常用在 iOS 设备上。大量的锁定操作会降低在 iOS 设备上运行时的性能。

  ● 存储(assign、retain、copy、strong、weak):标准变量类型默认使用 assign 存储类 型,而对象默认使用 retain 来增加值的引用计数(当值被改变或释放时也只是简单 地调用 release 来释放它)。如果一个对象支持它,可以使用 copy 命令表明这个对象应该被批量复制,而不是简单地执行 retain(用于可变值)。strong 和 weak 是 ARC 新增的限定符。前者是对于对象的强引用(会被保留),而后者是不会被保留的归零 引用。如果值被释放,这个属性的值会自动置为 nil。对于单个属性,只能指定其 中的一项。

  ● 方法(getter=、setter=):这些操作允许对属性设置自定义消息选择器。默认情况下, 一个名为 myProperty 的属性会有一个名为 myProperty 的 getter 方法和一个名为 SetMyProperty:的 setter 方法。这最常用在 Boolean 类型的属性上:hidden 属性可以 使用 isHidden 和 setHidden:作为它的方法。

不允许被声明的时候初始化

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

@interface Person : NSObject {
    @public
        NSString *name;
    @private
        int age;
}
@property(copy) NSString *name;
@property(readonly) int age;
-(id)initWithAge:(int)age;
@end

属性的访问方法由@synthesize关键字来实现,它由属性的声明自动的产生一对访问方法。另外,也可以选择使用@dynamic关键字表明访问方法会由程序员手工提供。使用@synthesize 只有一个目的——给实例变量起个别名,或者说为同一个变量添加两个名字。如果要阻止自动合成,记得使用 @dynamic。经典的使用场景是你知道已经在某处实现了getter/setter 方法,而编译器不知道的情况。

@implementation Person
@synthesize name;
@dynamic age;
-(id)initWithAge:(int)initAge
{
    age = initAge; // 注意:直接赋给成员变量,而非属性
    return self;
}
-(int)age
{
    return 29; // 注意:并非返回真正的年龄
}
@end

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

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"关键字:

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

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

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);
}

类别 (Category)

在Objective-C的设计中,一个主要的考虑即为大型代码框架的维护。结构化编程的经验显示,改进代码的一种主要方法即为将其分解为更小的片段。Objective-C借用并扩展了Smalltalk实现中的"分类"概念,用以帮助达到分解代码的目的。

一个分类可以将方法的实现分解进一系列分离的文件。程序员可以将一组相关的方法放进一个分类,使程序更具可读性。举例来讲,可以在字符串类中增加一个名为"拼写检查"的分类,并将拼写检查的相关代码放进这个分类中。

进一步的,分类中的方法是在运行时被加入类中的,这一特性允许程序员向现存的类中增加方法,而无需持有原有的代码,或是重新编译原有的类。例如若系统提供的字符串类的实现中不包含拼写检查的功能,可以增加这样的功能而无需更改原有的字符串类的代码。

在运行时,分类中的方法与类原有的方法并无区别,其代码可以访问包括私有类成员变量在内的所有成员变量。

若分类声明了与类中原有方法同名的函数,则分类中的方法会被调用。因此分类不仅可以增加类的方法,也可以代替原有的方法。这个特性可以用于修正原有代码中的错误,更可以从根本上改变程序中原有类的行为。若两个分类中的方法同名,则被调用的方法是不可预测的。

使用分类的例子

这个例子创建了Integer类,其本身只定义了integer属性,然后增加了两个分类Arithmetic与Display以扩展类的功能。虽然分类可以访问类的私有成员,但通常利用属性的访问方法来访问是一种更好的做法,可以使得分类与原有类更加独立。这是分类的一种典型应用—另外的应用是利用分类来替换原有类中的方法,虽然用分类而不是继承来替换方法不被认为是一种好的做法。

Integer.h 文件代码:

#import <objc/Object.h>
@interface Integer : Object
{
@private
    int integer;
}
@property (assign, nonatomic) integer;
@end

Integer.m 文件代码:

#import "Integer.h"
@implementation Integer
@synthesize integer;
@end

Arithmetic.h 文件代码:

#import "Integer.h"
@interface Integer(Arithmetic)
(id) add: (Integer *) addend;
(id) sub: (Integer *) subtrahend;
@end

Arithmetic.m 文件代码:

#import "Arithmetic.h"
@implementation Integer(Arithmetic)
(id) add: (Integer *) addend
{
  self.integer = self.integer + addend.integer;
  return self;
}
(id) sub: (Integer *) subtrahend
{
  self.integer = self.integer - subtrahend.integer;
  return self;
}
@end

Display.h 文件代码:

#import "Integer.h"
@interface Integer(Display)- (id) showstars;- (id) showint;@end

Display.m 文件代码:

#import "Display.h"
@implementation Integer(Display)
(id) showstars
{
  int i, x = self.integer;
  for(i=0; i < x; i++)
     printf("*");
  printf("\n");
    return self;
}
(id) showint
{
  printf("%d\n", self.integer);
    return self;
}
@end

main.m 文件代码:

 #import "Integer.h" 
 #import "Arithmetic.h" 
 #import "Display.h" 
int
main(void) 
 { 
 Integer *num1 = [Integer new], *num2 = [Integer new]; 
 int x; 
 printf("Enter an integer: "); 
 scanf("%d", &x); 
 num1.integer = x; 
 [num1 showstars]; 
 printf("Enter an integer: "); 
 scanf("%d", &x); 
 num2.integer = x; 
 [num2 showstars]; 
 [num1 add:num2]; 
 [num1 showint]; 
 return 0; 
 } 

利用以下命令来编译:

gcc -x objective-c main.m Integer.m Arithmetic.m Display.m -lobjc

在编译时间,可以利用省略#import "Arithmetic.h" 与[num1 add:num2]命令,以及Arithmetic.m文件来实验。程序仍然可以运行,这表明了允许动态的、按需的加载分类;若不需要某一分类提供的功能,可以简单的不编译之。

类的继承

在前面的资料我们有讲到在objective-c中,类与类其实是在不断发送和相应消息,所有的通信与控制都是通过收发消息来完成的。有些时候,子类定义的方法需要调用或重写父类的方法来完成,因为继承关系,会造成子类的子类所调用的方法与前面不同,造成生成的方法与我们所想的不同,甚至造成递归循环,所以在使用self(调用自身)和super(调用父类)时要十分小心。下面我将举几个例子来说明。

如下图所示,类A包含了 method 1 、method 2 、method 3。类B时类A的子类,类B重写了method2。类C是类B的子类,类C重写了method1。

我们来看看给类B的实例变量发送消息的情况。首先,假设向类B的实例对象发送了对应method1的消息,即进行了方法调用。虽然类B没有对method1进行定义,但因为类B的父类类A中定义method1,所以会找到类A的mouthed1,调用成功。调用method3的情况下也是同样的道理,类A的method3会被执行。method2与前面不同,因为类B重写了method2,所以会使用自身的定义的method2来响应消息。

而类C的实例对象发送消息会怎么样呢?类C有重写method1,所以会直接使用类C定义的method1来响应消息,类C没有定义method2,所以会使用类B所重写的method2来响应消息。由于类C和类B都没有定义method3,所以当类C的实例对象调用的method3时,会让类A定义的method3来响应消息。

如果实例B要调用类A所定义的method2怎么办呢?可以使用super来调用父类方法,类C的method1也是同理,接下来我来了看看下面几个例子:

如上图所示,类c重写了method1与mouthed3,类c在method1中通过super调用了类a的mouthed3,这时被调用的mouthed3是类a定义的method3。

self与super不同,并不指向某个对象,所以super只用来调用父类的方法,不同通过super来完成赋值,也不能把方法的返回值指定给super。

在⬆️图的例子中,有三个类A、B、C。类a定义了mouthed1,mouthed2,mouthed3三个方法。类b继承了类a,重写了mouthed1和mouthed3,类c继承类b,重写了mouthed2。

类 B的方法method3 想调用method1和method2,通过 self 调用了method1 和method2。我们来分析一下这个过程中到底哪个两数被调用了。对类B的实例对象调用 method3 方法时,首先会通过 self 调用method1,这个method1就是类B自身定义的method1。接着,method3 通过 self 调用method2,因为类 B中并没有method2 的定义,所以就会调用从类 A 中继承而来的 method2.而如果是类C的实例对象调用方法method3 的话会怎么样呢?我们首先来看看method3,因为类C中并没有定义 method3,所以调用的是类B中定义的method3。要注意这个时候 self 指的是类 C的实例对象,当 [self method1]执行时,因为类C中没有定义methodl, 所以调用的是类B中定义的method1。然后,当[self method2] 执行时,因为类C中定义了method2,所以执行的是类C中定义的method2,而不是上例中类A中定义的method2。另外还有一点需要注意,就算类 B中定义了method2,调用的也是类C中定义的 method2。也就是说,self 指的是收到当前消息的实例变量,因此,就算是同一个程序,根据实例的类的不同,实际调用的方法也可能不相同。使用 self 的时候要一定小心,要仔细分辨到底调用了哪个类的方法。

协议(Protocol)

协议是一组没有实现的方法列表,任何的类均可采纳协议并具体实现这组方法。Objective-C在NeXT时期曾经试图引入多重继承的概念,但由于协议的出现而没有实现之。

协议类似于Java与C#语言中的"接口"。在Objective-C中,有两种定义协议的方式:由编译器保证的"正式协议",以及为特定目的设定的"非正式协议"。

非正式协议为一个可以选择性实现的一系列方法列表。非正式协议虽名为协议,但实际上是挂于NSObject上的未实现分类(Unimplemented Category)的一种称谓,Objetive-C语言机制上并没有非正式协议这种东西,OSX 10.6版本之后由于引入@optional关键字,使得正式协议已具备同样的能力,所以非正式协议已经被废弃不再使用。

正式协议类似于Java中的"接口",它是一系列方法的列表,任何类都可以声明自身实现了某个协议。在Objective-C 2.0之前,一个类必须实现它声明匹配的协议中的所有方法,否则编译器会报告错误,表明这个类没有实现它声明匹配的协议中的全部方法。Objective-C 2.0版本允许标记协议中某些方法为可选的(Optional),这样编译器就不会强制实现这些可选的方法。

协议经常应用于Cocoa中的委托及事件触发。例如文本框类通常会包括一个委托(delegate)对象,该对象可以实现一个协议,该协议中可能包含一个实现文字输入的自动完成方法。若这个委托对象实现了这个方法,那么文本框类就会在适当的时候触发自动完成事件,并调用这个方法用于自动完成功能。

Objective-C中协议的概念与Java中接口的概念并不完全相同,即一个类可以在不声明它匹配某个协议的情况下,实现这个协议所包含的方法,也即实质上匹配这个协议,而这种差别对外部代码而言是不可见的。正式协议的声明不提供实现,它只是简单地表明匹配该协议的类实现了该协议的方法,保证调用端可以安全调用方法。

协议声明可以包含除了实例变量以外能包含类接口的任何东西。协议可以使用与声明 类本身相同的语法来指定属性、类和实例方法。唯一的附加项是一对类似于用来为实例变 量设定访问作用域的关键字:@optional 和@required。@required 关键字声明遵循这个协议 的类必须执行@required 关键字后面声明的项。如果没有指定@required 和@optional,默认 选项是@required。相反,@optional 关键字声明其后的项在正确遵守这个协议的过程中并 不是必要的。可选的协议方法(亦称非正式协议)常用来定义委托的接口,委托对象选择只 调用其中一至两个方法,不需要执行很多潜在的委托方法。使用这种方式编写协议,允许 编译器在灵活实现某个对象的同时确保指向该对象的委托符合所必需的协议。

正式协议(一切都是必需的)用来声明和划定类可能实现的特定功能,例如包含对对象 执行完整复制的能力,以及把它序列化为已知方式或相似活动的能力。如果对某个属性添 加 copy 存储性质,复制工作将会使用 NSCoding 协议描述的方法执行。作为协议,任何对 象都能选择实现它描述的方法,而不关心它的类层次结构。Objective-C 的消息传递机制的 动态特性意味着可以使用与任何其他对象同样的方法调用一个对象的 copy 方法。协议的声 明和执行提供了一种运行时和编译期的方法来决定给定对象(甚至可能不知道它的类)是否 实现了一个已知的功能集。

语法

协议以关键字@protocol作为区块起始,@end结束,中间为方法列表。

@protocol Locking
    (void)lock;
    (void)unlock;
@end

这是一个协议的例子,多线程编程中经常要确保一份共享资源同时只有一个线程可以使用,会在使用前给该资源挂上锁 ,以上即为一个表明有"锁"的概念的协议,协议中有两个方法,只有名称但尚未实现。下面的SomeClass宣称他采纳了Locking协议:

@interface SomeClass : SomeSuperClass <Locking>
@end

一旦SomeClass表明他采纳了Locking协议,SomeClass就有义务实现Locking协议中的两个方法。

@implementation SomeClass
(void)lock {
// 实现lock方法...
}
(void)unlock {
// 实现unlock方法...
}
@end

由于SomeClass已经确实遵从了Locking协议,故调用端可以安全的发送lock或unlock消息给SomeClass实体变量,不需担心他没有办法回应消息。插件是另一个使用抽象定义的例子,可以在不关心插件的实现的情况下定义其希望的行为。

动态类型

类似于Smalltalk,Objective-C具备动态类型:即消息可以发送给任何对象实体,无论该对象实体的公开接口中有没有对应的方法。对比于C++这种静态类型的语言,编译器会挡下对(void*)指针调用方法的行为。但在Objective-C中,你可以对id发送任何消息(id很像void*,但是被严格限制只能使用在对象上),编译器仅会发出"该对象可能无法回应消息"的警告,程序可以通过编译,而实际发生的事则取决于运行期该对象的真正形态,若该对象的确可以回应消息,则依旧运行对应的方法。

一个对象收到消息之后,他有三种处理消息的可能手段,第一是回应该消息并运行方法,若无法回应,则可以转发消息给其他对象,若以上两者均无,就要处理无法回应而抛出的例外。只要进行三者之其一,该消息就算完成任务而被丢弃。若对"nil"(空对象指针)发送消息,该消息通常会被忽略,取决于编译器选项可能会抛出例外。

虽然Objective-C具备动态类型的能力,但编译期的静态类型检查依旧可以应用到变量上。以下三种声明在运行时效力是完全相同的,但是三种声明提供了一个比一个更明显的类型信息,附加的类型信息让编译器在编译时可以检查变量类型,并对类型不符的变量提出警告。下面三个方法,差异仅在于参数的形态:

  • setMyValue:(id) foo;

id形态表示参数"foo"可以是任何类的实例。

  • setMyValue:(id ) foo;

id表示"foo"可以是任何类的实例,但必须采纳"aProtocol"协议。

  • setMyValue:(NSNumber*) foo;

该声明表示"foo"必须是"NSNumber"的实例。

动态类型是一种强大的特性。在缺少泛型的静态类型语言(如Java 5以前的版本)中实现容器类时,程序员需要写一种针对通用类型对象的容器类,然后在通用类型和实际类型中不停的强制类型转换。无论如何,类型转换会破坏静态类型,例如写入一个"整数"而将其读取为"字符串"会产生运行时错误。这样的问题被泛型解决,但容器类需要其内容对象的类型一致,而对于动态类型语言则完全没有这方面的问题。

转发

Objective-C允许对一个对象发送消息,不管它是否能够响应之。除了响应或丢弃消息以外,对象也可以将消息转发到可以响应该消息的对象。转发可以用于简化特定的设计模式,例如观测器模式或代理模式。

Objective-C运行时在Object中定义了一对方法:

转发方法:

  • (retval_t) forward:(SEL) sel :(arglist_t) args; // with GCC (id) forward:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

响应方法:

  • (retval_t) performv:(SEL) sel :(arglist_t) args; // with GCC (id) performv:(SEL) sel :(marg_list) args; // with NeXT/Apple systems

希望实现转发的对象只需用新的方法覆盖以上方法来定义其转发行为。无需重写响应方法performv::,由于该方法只是单纯的对响应对象发送消息并传递参数。其中,SEL类型是Objective-C中消息的类型。

以下代码演示了转发的基本概念:

Forwarder.h 文件代码:

#import <objc/Object.h>
@interface Forwarder : Object{
    id recipient; //该对象是我们希望转发到的对象。
}
@property (assign, nonatomic) id recipient;
@end

Forwarder.m 文件代码:

  • #import "Forwarder.h" @implementation Forwarder @synthesize recipient; (retval_t) forward: (SEL) sel : (arglist_t) args { /* *检查转发对象是否响应该消息。 *若转发对象不响应该消息,则不会转发,而产生一个错误。 */ if([recipient respondsTo:sel]) return [recipient performv: sel : args]; else return [self error:"Recipient does not respond"]; }

Recipient.h 文件代码:

  • #import <objc/Object.h> // A simple Recipient object. @interface Recipient : Object (id) hello; @end

Recipient.m 文件代码:

  • #import "Recipient.h" @implementation Recipient (id) hello { printf("Recipient says hello!\n"); return self; } @end

main.m 文件代码:

#import "Forwarder.h"
#import "Recipient.h"
int main(void)
{
    Forwarder *forwarder = [Forwarder new];
    Recipient *recipient = [Recipient new];
    forwarder.recipient = recipient; //Set the recipient.
    /*
     *转发者不响应hello消息!该消息将被转发到转发对象。
     *(若转发对象响应该消息)
     */
    [forwarder hello];
    return 0;
}

利用GCC编译时,编译器报告:

$$ gcc -x objective-c -Wno-import Forwarder.m Recipient.m main.m -lobjc
main.m: In function `main':
main.m:12: warning: `Forwarder' does not respond to `hello'$$

如前文所提到的,编译器报告Forwarder类不响应hello消息。在这种情况下,由于实现了转发,可以忽略这个警告。 运行该程序产生如下输出:

$ ./a.out
Recipient says hello!

参考:

  Objective-C 开发经典教程 www.tup.tsinghua.edu.cn/upload/book…

  Objective-C 30分钟入门教程 zhuanlan.zhihu.com/p/521387087 www.runoob.com/ios/ios-obj…