objective-c编程语言 第一章 对象,类和消息 第三小节 类(Classes)

时间:2022-12-19 21:52:23

Objective-C编程语言

-这是一份翻译,有关于objective-c,完全出于个人学习目的,共享给大家,如需转载请注明出处
原文地址:http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjectiveC/Introduction/introObjectiveC.html

三、类(classes)

                  面向对象的编程通常是由大量的对象组成的。基于cocoa框架的程序可能用到的NSMatrix、NSWindow、NSDictionary、NSFont、NSText这些类的对象,以及其它类型的对象。程序往往用到一种类型的多个对象,如多个NSArray对象或者多个NSWindow对象。

                  在objc中你通过定义一个类来定义对象。类的定义是一种类型的对象的原型;它描述了类的每个对象会拥有的实例变量,同时它也定义了该类的所有对象会使用到的方法。

                  编译器只为每个类生成一个可以访问的对象,这个对象拥有制造该类的对象的蓝图,由于这个原因传统上我们称之为工厂对象。工厂对象是类的编译版本;通过它创建的对象是类的实例。在程序中做主要工作的对象是有工厂对象在运行时创建的。

                  所有类的对象拥有一组相同的方法,也同样的用于一组从同一个模版中生成的实例变量。每个对应各自拥有自己的实例变量,但方法是共享的。

                  约定,类名以大写字母开头,如Rectangle;实例变量则通常以小写字母开头,如myRectangle。

1、继承

                  类的定义是可以采用附加这种方式的。你所定义的每一个类都是基于其它类的,新的类从其它类中继承方法和实例变量。新的类只需要简单的添加或修改它所继承的东西。它不需要重新复制代码。

                  继承将所有的类在一个只有一个单一根类的继承树中联系到一起。当你基于基础框架编写代码时,这个根类就是NSOject。除了根类的任何类都有一个父类,比自己在继承树中离着根类更近一步,所有的类都可以是任何数目的子类的父类,子类比父类在继承树中离着根类更近远一步。图1-1说明了在一个绘图程序中几个类的继承关系。

图1-1

                  图1-1表明,Square是Rectangle的子类,Rectangle是Shape的一个子类,Shape是Graphic的子类,Graphic又是NSObject的一个子类。继承关系是累加的,所以一个Square的对象会拥有Rectangle、Shape、Graphic、NSObject的方法和实例变量,当然也包含那些特别为Square类所定义的方法和实例变量。简单的说,一个Square类型的对象不仅仅是一个Square(正方形),它同样也是一个Rectangle(长方形),一个Shape(形状),一个Graphic(图像),也是一个NSObject的对象。

                  因此除了NSObject外的类都可以视为是其它类的一种特例或者说是适应体,每个连续的子类都在改变着他们所继承的累加的总量。Square类别定义了由Rectangle转变为Square的最小条件。

                  当你定义一个类时,你可以通过指定它的父类来把它加入到继承树中,你所创建的每个类都必须是其它类的子类,除非你定义的是一个新的根类。我们已经有了许多可用的、潜在的父类供你使用。cocoa框架中定义了NSObject和其它的一些框架,包含了多余250种附加的类。一些你可以直接将他们应用到你的程序中,有些可能需要你自己定义它的子类来适应你的应用程序。

                  一些框架类定义了几乎所有你需要用到的,但会保留一些方法,在子类中实现,因此你可以写出非常精妙的对象,只需要少量的代码,来重复利用框架编写者的代码。

2、NSObject类

                  NSObject是一个根类,因此没有父类。他为objc对象和对象交互定义了一个基本的框架。它赋予了类和类的对象表现的像对象,并且能与运行时系统一起工作。

                  一个不需要从其它类那里继承任何东西的类仍然要定义为NSObject的子类,因为这个类至少要能在运行时表现的是一个objc的对象。从NSObject继承这些特性远比自己在一个新的类中重新创造容易的多。

                  注:实现一个根类是一项昂贵的费时的精妙的工作,并且可能是危机四伏。这个类一定要复制NSObject所能做的事情,如生成实例,将它同它的类联系到一起,并且对运行时系统唯一标识它们。处于这些原因,你通常只要把由cocoa提供的NSObject类做为根类即可。

3、实例变量的继承

              当一个工厂对象创建了一个实例,这个实例对象不仅仅只是包含了为它定义的实例变量,也包含了它的父类的及它的父类的父类的,直到根类的所有的实例变量。因此,在NSObject中定义的isa实例变量就成为了每个对象的一部分,isa就可以将对象与它的类连接到一起。

                  图1-2展示了可能的一个Rectangle类的实现中存在的一些实例变量,并标出它们来自于哪里

                 

                  一个类不一定必须定义声明实例变量,它也可以简单的定义新的方法,并操作它继承来的实例变量,如果它不需要任何其它的实例变量的话。如Square就可以不用定义它自己的实例变量。

4、方法的继承

         对象不仅仅拥有为自己特定的方法,同样可以使用它继承下来的方法。如一个Square可以使用Rectangle, Shape,Graphic, NSObject包括自己定义的方法。

         因此你自己新定义的类就可以使用在继承树中以及写好的代码,这是面向对象编程一个非常重要的特性和优势。当你使用cocoa提供的任何一个面向对象的框架时,你的程序就可以就可以利用写进框架的类的基本函数功能了,你只需要为你的程序定制这些标准的功能即可。

         工厂对象同样也继承自在继承树中处于它上边的类,但是它们不包含实例变量(只有实例才包含),只包含类的方法。

5、方法的覆盖

         对于继承有一个例外,这个例外是非常有用的。当你定义一个新的类时,你可以实现一个新的与上层继承树中相同名字的方法。新的方法就会覆盖原本的方法,新类的实例和新类的子类的实例都是执行或继承新的方法,而不是被覆盖的方法。

         如,Graphic定义了一个display方法,Rectangle通过定义自己版本的display方法

类型自省

所谓的类型自省,意思就是说对象实例可以在运行时显示其类型(C++是不支持这个特性的)。在NSObject类中的isMemberOfClass:方法可以检查接收消息的对象是否是某种特指的类别,方法如下:

         if( [anObject isMemberOfClass:someClass] )

...

NSObject类也定义了isKindOfClass:方法,它既可以检查消息接收对象是否是继承或者是某种类型:

if ( [anObject isKindOfClass:someClass])

    ...

能够将anObject静态绑定的类(someClass),同样会对isKindOfClass:方法返回YES。

自省不仅仅局限于对象的类型信息,在本章稍后的一些小节中我们会讨论那些可以返回类对象的方法,判断一个对象是否可以对指定的消息有响应的方法,以及反映一些其它的信息。

要了解更多的关于isKindOfClass:,isMemberOfClass:以及相关的一些方法,可以查看NSObject类的说明。

 

 

 

类对象

                   类的定义包含了许多不同类型的信息:

l  类的名字以及它的基类;

l  一组实例变量的模板描述;

l  方法的名字的声明以及它们的返回类型及参数;

l  方法的实现;

这些信息被编译进和记录在运行时的变量中。编译器只生成一个对象来代表一种类,我们称其为类对象(工厂对象)。工厂对象能够得知关于类的所有信息,它可以通过类定义中设计有关信息产生实例对象。

尽管工厂对象保存着一个类实例的所有原型信息,但它本身并不是一个实例,它没有自己的实例变量也不能执行为类的实例对象设计的方法。不过类定义可以包含只是为工厂对象设计的方法,我们称其为类方法,为实例变量设计方法我们称其为实例方法,工厂对象的类方法的继承与实例方法的继承特点是一样的。

         在源代码中,工厂对象是由类名代替的。在下边的这个实例中,Rectangle类通过它从NSObject类中继承的方法返回类的版本信息:

         int versionNumber = [Rectangleversion];

         类名只有在消息表达式中才能代表工厂对象,在任何其它地方要用到工厂对象,你必须向实例对象或者类名发送Class消息来返回类id:

         id aClass = [anObject class];

id rectClass = [Rectangle class];

正如上边这个例子所示范的,工厂对象可以像任何其它对象一样被绑定为id类型,但工厂对象也可以用Class类型代替id(而其他的实例对象不可以):

Class aClass = [anObject class];

Class rectClass = [Rectangle class];

所有的工厂对象都是Class类型的,对于一个类使用这个类型名与对一个实例变量使用类名是等价的。

因此工厂对象也完全可以在运行时动态绑定,接收消息以及从其它的类继承,它们特殊的地方就是它们是由编译器创建,与那些通过类定义产生的实例对象相比缺少数据结构(实例变量),而且他们是运行时产生实例对象的代理,除此之外,所有特性及使用方法与实例对象没有谁什么不同。

注意:编译器也会为每个类生成一个元对象,就想工厂对象是用来描述其它实例对象的一样,元对象是用来描述工厂对象的。你可以向实例对象和工厂对象发送消息,但不能向元对象发送消息,它们只是提供给运行时系统内部使用的。

创建实例对象

         工厂对象的一个最重要的方法是用来创建实例对象的,下边这个例子告诉Rectangle类创建一个新的Rectangle对象,并且将其与myRectangle对象变量绑定到一起:

         id  myRectangle;

myRectangle =[Rectangle alloc];

alloc方法为新实例对象分配内存,并将其实例变量的内存全部初始化为0,当然这排除了isa变量,isa变量把新的实例对象与它的类联系到一起。要想对象有用,一般情况下还要对其做更特殊的初始化工作。这个方法就是init方法。初始化一般紧接在创建对象的代码之后:

myRectangle= [[Rectangle alloc] init];

这行代码或者是类似这行的代码,在myRectangle可以接收任何本章之前列举的例子的任何消息之前是必须的。alloc方法返回了一个新的对象实例,新的对象实例执行了init方法来设置使其处于初始化的状态。每个工厂对象都有一个像alloc一样的方法来使它能够创建新的对象,每个实例对象至少有一个像init的方法使它作好可以被使用的准备工作。初始化方法一般会带有参数并有标签来标识参数(如initWithPosition:size:就可能是一个可以用来初始化myRectangle对象的方法),但是初始化方法一般都是以init作为开头的。

通过对象初始化

         把类当作是对象并不仅仅是Object-C语言的一个奇思异想,它提供了一种选择,有时候对于设计是出其不意的有益的。例如,我们可以通过那些open-ended的对象来定制一个对象。在AppKit框架中,一个NSMatrix对象可以通过一种特别的NSCell对象来定制。

         NSMatrix对象可以负责为它的每个单元格创建不同的独立的对象,它可以选择矩阵在初始化时或者需要新的单元格时创建。一个可视化的NSMatrix的对象,可以在屏幕上增大或缩小,这可能是通过响应用户输入而产生的,当它增大时,矩阵要有能力产生新的对象来填充新的增加的空白。

         但是,它们应该是哪种对象呢?每个矩阵都只能显示一种类型的NSCell,但我们有很多种不同的类型,在图1-3中继承树中展示了有AppKit提供的几种不同的类型,它们都是继承自NSCell类:

当一个矩阵对象创建一个NSCell对象时,它到底应该是用来展示一组按钮的NSButtonCell对象,还是一个可以允许我们输入或编辑的文本狂对象NSTextFieldCell,或者是其它类型的NSCell?NSMatrix对象必须允许任何类型的cell,甚至是那些还没有被开发出来的cell.

         一种解决方案可能是将NSMatrix定义为虚类,并且要求每个使用它的人声明子类并实现产生新的cell的方法。由于我们可以自己实现方法,用户当然可以确定他们创建的对象的类型。

         但是这种解决方案让NSMatrix的用户来完成本应该由NSMatrix类自己作的事情,并且这没有必要的引入了大量的类。由于一个应用程序可能使用不止一种的matrix,每种matrix又包含着不同的cell,这样NSMatrix的子类就会变得很庞杂。你每次发明一个新的NSCell,你就需要定义一个新的NSMatrix子类。更重要的是,在不同项目中工作的人,可能写出完全用来做同样工作的相同的类来,都是因为NSMatrix没有做的这一点(上一段落提出的需求)。

         更好的解决方法就是(并且NSMatrix采用了这种方案)允许NSMatrix的实例对象可以通过一种NSCell工厂对象来初始化。NSMatirx也定义了一个setCellClass:方法,可以将需要用的类别的NSCell对象最为参数传递给NSMatrix:

         [myMatrixsetCellClass:[NSButtonCell class]];

         NSMatrix对象就可以通过传递进来的的不同种类的cell工厂对象来创建新的cells,无论时在它初始化或者是改变大小需要新的cell填充时。这种定制对象的方法会比较困难,如果那些类的对象不能通过消息传递并且与变量关联起来的话。

 

变量与工厂对象

         当你定义了一个新的类,你可以指定其实例变量。每个实例对象都会维护它自己的一份在类中声明的变量的拷贝,即每个对象控制着自己的数据。因此就没有一个与实例变量对等的类变量(参考实例方法与类方法的理解),工厂对象没有权限访问任何实例对象的的实例变量,它不能初始化、读取或者改变实例变量。

         要让所有的实例对象共享数据,你必须定义一个外部的变量,最简单的方法就是在类的实现文件中定义一个变量:

         intMCLSGlobalVariable;

         @implementationMyClass

//implementation continues

@end

静态变量赋予了工厂对象更多的功能,而不仅仅是用来产生新的实例对象的工厂;它可以在自身范围内变身为一个接近全能的实例对象。工厂对象可以用来定位它所产生的实例的,可以从已经产生的实例变量中分发实例变量,或者是管理其它应用程序的处理要素。当在你只需要某个类的一个对象的时,你可以把这个对象的所有状态设置为static的变量,并且只用类方法。这可以省去创建和初始化对象的工作。

注意:采用没有声明为static的变量也是可行的,但是static变量的作用域限制能够更好的满足将数据封装到区分的对象中的目的。

初始化工厂对象

如果你想一个工厂对象来作其它的任何不是为了产生实例对象的事情,那么你可能就需要初始化这个工厂对象,就象你要初始化一个实例变量一样。尽管应用程序并不分配实例对象,Objc确实提供了一种方法使应用程序能够初始化工厂对象。

如果一个类用到了static或者global的变量,那么initialize方法就是一个设置其初始值的合适的地方。如一个类维护着一组变量,initialize方法可以设置好一个array,甚至可以分配一两个默认的变量使它们就绪。

运行时系统会在任何类接收任何其它消息之前,并在其基类接收了initialize方法之后发送一个initialize方法给这个类,尽管基类已经收到过initialize方法。假设类A实现了initialize方法,类B继承自类A,但没有实现initialize方法,就在类B接收它的第一个消息之前运行时系统发送initialize方法给类B,但由于类B没有实现initialize方法,那么类A的initialize方法就会被执行。那么类A就要保证它的初始化逻辑只被执行过一次,并且为适当的类执行过初始化动作。

为了避免初始化逻辑会被执行多次,在实现initialize方法时用Listing1-3中的模版:

Listing 1-3Implementation of the initialize method

+(void)initialize

{

  if (self == [ThisClass class]) {

        // Perform initialization here.

        ...

    }

}

注意:记住initialize方法是由运行时系统给每个类发送的消息,因此在一个类的initialize方法的实现中,你就不能发送initialize方法给它的基类了。

根类的方法

所有的对象、类和实例等,需要一个通向运行时系统的接口。工厂对象和实例对象都要有能力自省他们的能力和报告他们在继承树中的位置。这就是NSObject类的职责来提供这个接口了。

因此NSObject方法就不必要实现两次了,一次为实例对象一次为工厂对象。工厂对象被给予了特别的权限来执行在根类中定义的实例方法。当一个工厂对象收到了一个它不能响应的类方法时,运行时系统就会判断是否有根类的实例方法可以响应这个消息。工厂对象只能执行在根类中定义的实例方法,而不能执行在非根类中的实例方法,当然前提是这个方法没有被定义为类的类方法。

更多的关于工厂对象执行根类实例方法的信息,请查看NSObject Class Reference.

源代码中的类名字

在源代码中类名可以用于两种不同的上下文中。这些反映了一个类作为类型名和作为一个对象的重要角色:

l  类名可以被用做一种数据类型,如Rectangle *anObject;

在这里anObject被静态的绑定为一个Rectangle对象的指针,编译器就期望这它拥有Rectangle类所继承或拥有的实例变量和方法。静态类型绑定使编译器作更好的类型检查,并且使源代码更易于阅读

只有实例对象才可以被静态绑定;而工厂对象确不可以,因为他们并不是一个类的方法,而是属于Class这数据类型。

l  在消息表达式中作为消息接收者,类名就代表着工厂对象。

这种用法已经在之前的示例中多次提到。类名可以作为工厂对象使用,只有在它是消息接收者时才可以。在任何其它的上下文中,你必须要求工厂对象返回它的id类型(通过向它发送一个Class方法)。这个例子在isKindOfClass:消息中把Rectangle类作为参数传递:

if ( [anObject isKindOfClass:[Rectangle class]] )

    ...

如果只把Rectangle作为参数是不合法的,因为类名只能作为消息的接收者。

如果在编译时你不知道类名,但可以在运行时提供一个字符串的话,那么你可以用NSClassFromString方法来返回工厂对象:

NSString *className;

    ...

if ( [anObject isKindOfClass:NSClassFromString(className)])

    ...

如果传递给NSClassFromString的字符串不是一个可用的类名,它将会返回nil。

类名和全局变量和函数在同一个命名空间中,在Object-C语言中,类名和全局变量不能有相同的名字。类名是唯一全局可见的名字。

测试类的相等

你可以直接用指针来比较两个类的相等性。尽管如此,获取正确的类就显得尤为重要。在cocoa框架中有几个不同的特征可以动态或者显式的创建已有类的子类来扩它们的能力。在动态创建的子类中,class方法通常会被重载,来使子类能够伪装成它代替的类。在测试子类的相等性时,你需要比较应该是又Class方法返回的值而不是由其它更低级的方法返回的值,从API的角度来说,下边的不等性比较对动态子类是适用的:

[object class] != object_getClass(object) != *((Class*)object)

因此,你需要用下边的方法来测试两个类是否相等

if ([objectA class] == [objectB class]) { //...