Objective-C基础教程内容总结(一)

November 30 2020 232 移动开发(1)

面向对象基础

中缀符

Objective-C有一种名为中缀符(infix notation) 的语法技术。方法的名称及其参数都是合在一起的。例如,你可以这样调用带一个参数的方法:

[circle setFillColor: kRedColor];

带两个参数的方法调用如下所示:

[textThing setStringValue: @"hello there" color: kBlueColor];

setStringValue:color: 是参数的名称,@"hello there"kBlueColor 是被传递的参数。

这种语法和C不同,在C语言中调用函数时,是把所有的参数都放在函数名之后,如下所示:

setTextStringValue(textString, @"hello there", kBlueColor);

Objective-C中的OOP

@interface 部分

创建某个特定类的对象之前,Objective-C 编译器需要一些有关该类的信息,尤其是对象的数据成员(级对象的C语言类型结构体应该是什么样子)及其提供的功能。可以使用@interface指令把这些信息传递给编译器。 以下是Circle类的接口:

@interface Circle : NSObject
{
    // 数据成员声明
    ShapeColor fillColor;
    ShapeRect bounds;
}

// 方法声明

- (void) setFillColor: (ShapeColor) fillColor;

- (void) setBounds: (ShapeRect) bounds;

- (void) draw;

@end // Circle

@implementation 部分

@implementation部分用于定义类的公共接口。通常,接口被称为API(Application Programming Interface)。而真正使对象能够运行的代码位于@implementation部分中。 以下是Circle类的实现:

@implementation Circle

- (void) setFillColor: (ShapeColor) c 
{
    fillColor = c;
} // setFillColor

- (void) setBounds: (ShapeRect) b
{
    bounds = b;
} // setBounds

实例化对象

实例化(instantiation)对象时,需要分配内存,然后将这些内存初始化并保存为有用的默认值,这些值不同于通过新分配的内存获得的随机值。内存分配和初始化工作完成之后,就意味着新的对象实例已经创建好了。

由于对象的局部变量只在对象的实例中有效,因此我们称它们为实例变量,通常简写为ivar

为了创建一个新的对象,我们需要向相应的类发送new消息。该类接收并处理完new消息后,我们就会得到一个可以使用的新对象实例。

Objective-C具有一个极好的特性,你可以把类当成对象来发送消息。对于那些不局限于某个特定的对象而是对全体类都通用的操作来说,这非常便捷。最好的例子就是给新对象分配空间,如果你要创建一个新的Circle对象,向Circle类发送消息要比向某个已存在的circle对象发送消息更加合适。

以下是Shapes-Object程序的main()函数,它用于创建圆形、矩形和椭圆形。

int main(int argc, const char * argv[])
{
    id shapes[3];

    ShapeRect rect0 = {0, 0, 10, 30};
    shapes[0] = [Circle new];
    [shapes[0] setBounds: rect0];
    [shapes[0] setFillColor: kRedColor];

    ShapeRect rect1 = {30, 40, 50, 60};
    shapes[1] = [Rectangle new];
    [shapes[1] setBounds: rect1];
    [shapes[1] setFillColor: kGreenColor];

    ShapeRect rect2 = {15, 19, 37, 29};
    shapes[2] = [Egg new];
    [shapes[2] setBounds: rect2];
    [shapes[2] setFillColor: kBlueColor];

    drawShapes (shapes, 3);

    return (0);
} // main

继承

继承的语法格式

用于声明新类的语法:@interface Circle: NSObject,冒号后面的标识符是需要继承的类。在Objective-C中你可以选择不继承,但如果你使用的是Cocoa框架,就需要继承NSObject类,因为它提供了大量有用的特性(当继承一个继承自NSObject类的类时,也可以获取这些特性)。

某些编程语言(例如C++)具有多继承性,也就是一个类可以直接从两个或者多个类继承而来。但是Objective-C不支持多继承。如果你尝试在Objective-C中使用多继承(多继承的形式类似于以下语句),是无法正常通过编译器审核的。

@interface Circle: NSObject, PrintableObject

你可以通过Objective-C的其他特性来达到多继承的效果,例如类别(category)和协议(protocol)。

接下来我们将进一步改进架构,使我们的类从Shape类继承而来,将CircleRectangle的接口代码更改为以下形式:

@interface Circle: Shape
@end // Circle

@interface Rectangle: Shape
@end // Rectangle

以下是Shape类的接口声明:

@interface Shape : NSObject
{
    // 数据成员声明
    ShapeColor fillColor;
    ShapeRect bounds;
}

// 方法声明

- (void) setFillColor: (ShapeColor) fillColor;

- (void) setBounds: (ShapeRect) bounds;

- (void) draw;

@end // Shape

Shape类的实现代码也很简单并且很常见:

@implementation Shape

- (void) setFillColor: (ShapeColor) c
{
    fillColor = c;
} // setFillColor

- (void) setBounds: (ShapeRect) b
{
    bounds = b;
} // setBounds

- (void) draw
{
} // draw

@end // Shape

虽然draw方法没有实现任何功能,但我们仍需定义它,以便Shape的所有子类都能通过它来实现各自的方法。在方法的定义中不写任何内容或者返回一个虚(dummy)值都是可以通过编译的。现在我们来看看Circle类的实现:

@implementation Circle

- (void) draw
{
    NSLog(@"drawing a circle at (%d %d %d %d) in %@", bounds.x, bounds.y, bounds.width, bounds.height,  colorName(fillColor));
} // draw

@end // Circle

接下来是新简化过的Rectangle类的实现:

@implementation Rectangle

- (void) draw
{
    NSLog(@"drawing a rectangle at (%d %d %d %d) in %@", bounds.x, bounds.y, bounds.width, bounds.height,  colorName(fillColor));
} // draw

@end // Circle

这种移植和优化代码的方式称为重构(refactoring) 。这在OOP中是一个非常流行的主题。进行重构时,会通过移植某些代码来改进程序的架构,正如我们在这里删除重复代码一样,而不必改变代码的行为和运行结果。通常开发周期包括向代码中添加某些特性,然后通过重构删除所有重复的代码。

继承的工作机制

方法调度

对象在收到消息时,如何知道要执行哪个方法呢?加入我们已经将setFillColor:的代码移出了Circle类和Rectangle类,当向Circle的对象发送到setFillColor:方法时,Shape类的代码如何响应呢?秘密在于:当代码发送消息时,Objective-C的方法调度机制将在当前的类中搜索相应的方法。如果无法在接收消息的对象的类文件中找到相应的方法,它就会在该对象的超类中进行查找

不支持继承程序中的方法调度

上图展示了在还没有引入Shape父类版本的程序中,方法调度机制是如何通过代码向Circle类的对象发送setFillColor:消息的。对于类似[shapesetFillColor: kRedcolor]的代码, Objective-C的方法调度机制首先会寻找接收消息的对象,在本例中是Circle类的对象。该对象拥有一个指向Circle类的指针,Circle类也有一个指向其相应代码的指针。调度程序通过这些指针来查找正确的代码。

继承和类代码

再来看看上图,它展示了新增加了继承特性的架构。在该代码中,Circle类拥有一个指向其超类Shape的引用指针。消息传递时,Objective-C的方法调度机制会使用该信息来找到正确的方法实现。

下图展示了在支持继承的程序中方法调度机制的工作流程。当向Circle类的对象发送setFillColor:消息时,调度程序首先询问Circle中的代码能否响应setFillColor:消息。在本例中,调度程序会发现Circle类中没有为setFillColor:定义方法,所以答案是不能。因此接下来它将在超类Shape中查找相应的方法实现。调度程序会溯流而上进入Shape类中,找到setFillColor:定义,之后运行这段代码。

支持继承程序中的方法调度

这种操作就像是在说“我在这里没有找到它,因此我将在它的超类中继续找”,必要时它将会在继承链的每一个类中重复地执行此操作。假如某个方法在Circle类和Shape类中都没有找到,调度程序会继续在NSObject类中寻找,因为它是继承链中的下一个超类。如果在最顶层的NSObject类中也没有找到该方法,则会出现一个运行时错误同时还会出现一个编译时( compile-time)警告信息。

实例变量

在创建一个新类时,其对象首先会从它的超类继承实例变量,然后根据自身情况添加自己的实例变量。为了理解实例变量的继承机制,我们创建一个新的形状(圆角矩形)来添加一个新的实例变量。这个新类RoundedRectangle需要一个变量来记录绘制拐角时的半径。这个类的接口定义如下所示。

@interface RoundedRectangle: Shape
{
@private
 int radius;
}
@end // RoundedRectangle

对象中实例变量的布局

上图展示RoundedRectangle了对象的内存布局。NSObject类声明了一个名为isa的实例变量,该变量保存一个指向对象当前类的指针。接下来是由Shape类声明的两个实例变量fillColorbounds。最后是由RoundedRectangle声明的实例变量radius

因为继承在子类和超类之间建立了一种“is a”(是一个)的关系,所以NSObject的实例变量叫做isa。即RectangleShape是一个ShapeCircle是一个Shape。使用Shape类的代码也可以使用Rectangle类或Circle类。 使用更具体种类的对象(RectangleCircle)来代替一般类型(Shape),这种能力被称为多态性( polymorphism),该词源于希腊语,意思是“多种形状”,恰好符合这个程序的特点。

重写方法

在你制作全新的子类时,经常会添加自己的方法。有时你会添加一个能够向类中引入特有功能的新方法,有时你会替换或改进某个超类定义的现有方法。例如,你可以从Cocoa的NSTableView类(用来显示滚动列表以供用户选择)着手,添加一个新行为,比如使用语音合成器来朗读列表的内容。可以添加一个名为speakRows的新方法来向语音合成器提供表格的内容。

创建Shape类时,我们知道它的所有子类都要用于绘制图形,但不知道它们如何实现这一功能。因此我们为Shape类提供一个draw方法,但该方法的实现内容为空,这样每个子类都能实现各自的功能。当类(比如CircleRectangle)实现各自的方法时,我们就说它们重写了draw方法。

Circle对象发送draw消息时,方法调度机制将运行重写后的方法:Circle类的draw实现。超类(如Shape)中定义的所有draw实现都会被完全忽略掉。在当前情况下这没有问题,因为Shape类的draw实现中没有任何代码,但有时你可能不想忽掉超类中定义的方法。

super关键字

Objective-C提供了一种方法,让你既可以重写方法的实现,又能调用超类中的实现方式。当需要超类实现自身的功能,同时在之前或之后执行些额外的工作时,这种机制非常有用。为了调用继承的方法在父类中的实现,需要使用super作为方法调用的目标。

Circle类的@interface部分不需要修改,因为我们没有添加任何新方法或实例变量。只需向@implementation部分中添加以下代码:

@implementation Circle

- (void) setFillColor: (ShapeColor) c
{
 if (c == kRedColor)
 {
  c = kGreenColor;
 }
 [super setFillColor: c];
} // setFillColor
// and the rest of the Circle @implementation // is unchanged
@end // Circle

在这个新的setFillColor:实现中,我们会检查ShapeColor类型的参数是否是红色,如果是,就将它改成绿色,然后请求超类响应信息[super setFillColor: c]并将该颜色放入到实例变量中。

Super来自哪里呢?它既不是参数也不是实例变量,而是由Objective-C编译器提供的一种神奇的功能。当你向super发送消息时,实际上是在请求 Objective-C向该类的超类发送消息。如果超类中没有定义该消息, Objective-C会和平常一样继续在继承链上一级中查找。

调用超类的方法

上图展示了Circle类的setFilColor:方法的执行流程。 Circle对象接收setFillColor:消息,方法调度机制找到了自定义的setFillColor:方法,这是由Circle类实现的。Circle类的setFillColor:方法会检查参数是不是kRedColor,并在必要时更改颜色,通过调用[super setFillColor:c]来调用超类的方法。这个super的调用将会运行Shape类的setFillColor:方法。

重写方法时,调用超类方法总是一个明智之举,这样可以实现更多的功能。在本例中,我们获取了Shape的源代码,因此知道所有 Shape在其setFillColor:方法中只是将新颜色赋给了实例变量。但是,如果我们不熟悉Shape的内部实现细节,就不知道Shape是否还有其他功能。而且即使我们现在知道Shape的功能,但是修改或改进了类之后,可能又不知道了。调用继承的方法可以确保获得方法实现的所有特性。

说两句

 表情