第十四条 理解"类对象"的用意

2017/8/24 posted in  第二章 对象,消息,运行期

OC是一个极其动态的语言,那么运行期时,如何知道某个对象的类型呢? 对象类型并非在编译期就绑定好了,而是要在运行期査找。而 且,还有个特殊的类型叫做id,它能指代任意的Objective-C对象类型。一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。

“在运行期检视对象类型”这一操作也叫做“类型信息査询”(introspection, “内省”),这 个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class,即NSObject与NSProxy)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息査询方法”

我们先来了解Objective-C对象的本质:

每个Objective-C对象实例都是指向某块内存数据的指针。所以在声明变量时,类型后面要跟一个字符:

NSString *pointerVariable = @"Some string";

对于通用的对象类型id,由于其本身已经是指针了,所以我们能够这样写:

id genericTypedString = @"Some string";

上面这种定义方式与用NSString*来定义相比,其语法意义相同。唯一区别在于,如果声明时指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。

描述Objective-C对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也
在定义在这里:

typedef struct objc_object {
    Class isa;
} *id;

由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类, 通常称为“is a”指针。例如,刚才的例子中所用的对象“是一个”(is a) NSString,所以其“is a”指针就指向NSString

Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol list *protocols;
);

此结构体存放类的“元数据"(metadata),例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为Objective-C对象。 结构体里还有个变量叫做superclass,它定义了本类的超类。类对象所属的类型(也就是isa 指针所指向的类型)是另外一个类,叫做“元类"(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如图所示。

superclass 指针确立了继承关系,而 isa 指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息査询”。我们可以査出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”(class hierarchy)的哪一部分。

在类继承体系中查询类型信息

可以用类信息査询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”则能够判断出对象是否为某类或其派生类的实例,例如:

NSMutableDictionary *dict = [NSMutableDictionary new];
[diet isMemberOfClass: [NSDictionary class] ] ; ///< NO 
[diet isMemberOfClass:[NSMutableDictionary class】】; ///< YES 
[diet isKindOfClass: [NSDictionary class】];///< YES 
[diet isKindOfClass2 [NSArray class]]; ///< NO

像这样的类型信息査询方法使用isa指针获取对象所属的类,然后通过superclass指针 在继承体系中游走。由于对象是动态的,所以此特性显得极为重要。Objective-C与你可能熟 悉的其他语言不同,在此语言中,必须査询类型信息,方能完全了解对象的真实类型。

由于Objective-C使用“动态类型系统"(dynamic typing),所以用于査询对象所属类的类 型信息査询功能非常有用。从collection中获取对象时,通常会査询类型信息,这些对象不 是“强类型的”(strongly typed),把它们从collection中取出来时,其类型通常是id。如果想 知道具体类型,那就可以使用类型信息査询方法。例如,想根据数组中存储的对象生成以逗 号分隔的字符串(comma-separated string),并将其存至文本文件,就可以使用下列代码:

-(NSString*)commaSeparatedStringFromObjects:(NSArray*)array {
    NSMutablestring *string = [NSMutableStringnew]; 
    for (id object in array) {
        if ([object isKindOfClass:[NSStringclass]]) {
            [string appendFormat:@"%@,", object];
        }else if ([object isKindOfClass:[NSNumberclass]){
            [string appendFormat:@"%d", [object intValue]];
        } else if ([object isKindOfClass:[NSDataclass]])    {
            NSString *base64Encoded = /* base64 encoded data */;
            [string appendFormat: @"%@", base64Encoded];
        } else {
              //Type not supported
        }
    }   
    return string;
}

我们应该尽量使用类型信息査询方法,而不应该直接比较两个类对象是否等同(例如if([object class] == [EOCSomeClass class])),因为前者可以正确处理那些使用了消息传递机制(参见第12条)的对象。比方 说,某个对象可能会把其收到的所有选择子都转发给另外一个对象。这样的对象叫做“代理” (proxy),此种对象均以NSProxy为根类。

通常情况下,如果在此种代理对象上调用class方法,那么返回的是代理对象本身(此类 是NSProxy的子类),而非接受的代理的对象所属的类。然而,若是改用“isKindOfClass:” 这样的类型信息査询方法,那么代理对象就会把这条消息转给“接受代理的对象”(proxied object)。也就是说,这条消息的返回值与直接在接受代理的对象上面査询其类型所得的结果 相同。因此,这样査出来的类对象与通过class方法所返回的那个类对象不同,class方法所返回的类表示发起代理的对象,而非接受代理的对象。

要点

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
  • 如果对象类型无法在编译期确定,那么就应该使用类型信息査询方法来探知。
  • 尽量使用类型信息査询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。