用id
修饰和NSObject *
修饰有何不同?
要详细了解两者的不同,需要先说一说Objective-C中的动态类型和静态类型。
- 动态类型
动态类型指的是对象指针类型的动态性,具体是指使用id
修饰后将对象的类型确定推迟到运行时,由赋给它的对象类型决定对象指针的类型。也就是说id
修饰的对象为动态类型对象,其他在编译器指明类型的为静态类型对象,通常如果不需要涉及到多态的话还是要尽量使用静态类型(原因:错误可以在编译器提前查出,可读性好)。
// 动态类型
id obj = [[TestObject alloc] init];
- 静态类型
一个指针变量指向特定类的对象时,使用的是静态类型,在编译的时候就知道这个指针变量所属的类。使用静态类型时,编译器在编译期间,会做许多的类型检查:因为编译器需要知道哪个对象该如何使用。
// 静态类型
TestObject obj = [[TestObject alloc] init];
苹果官方objc.h文件里有关于id
的定义:
/// A pointer to an instance of a class.
typedef struct objc_object *id;
-
id
可以用于指向所有的Objective-C对象,是一种万能指针,类似于C语言中的void *
。 -
与
id
类似NSObject *
可以指向所有继承自NSObject
的对象,因为在Objective-C中NSObject
是基类,绝大多数的类继承自NSObject
,因此根据面向对象编程的多态特性,NSObject *
可以指向Objective-C中绝大多数的类的实例对象。
下面是我们用来测试的TestObject
类的源码:
// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
- (void)run;
@end
// TestObject.m
#import "TestObject.h"
@implementation TestObject
- (void)run {
NSLog(@"%s",__func__);
}
@end
接下来我们在ViewController
中运行以下测试id
修饰时的代码:
- (void)viewDidLoad {
[super viewDidLoad];
id p1 = [[TestObject alloc] init];
[p1 run];
}
运行情况:
编译通过,并且p1
成功调用run
方法。
接下来我们在ViewController
中运行以下测试NSObject *
修饰时的代码:
编译无法通过,报错:NSObject
中没有声明run
方法。
-
使用
id
修饰的对象是动态类型,编译器在编译期不会去判断其真实类型,因此id
指向的对象不管向其发送任何消息,编译器在编译期都不会有任何报错
-
使用
NSObject *
修饰的对象是静态类型,在编译期就已经明确该对象是NSObject
对象,因此当我们对该对象发送NSObject
没有声明的方法时,编译器就会果断报错
id
与instancetype
的区别与联系
id
与instancetype
的区别要从关联返回类型和非关联返回类型说起:
- 关联返回类型
根据Cocoa
的命名规则,满足下述规则的方法:
1. 类方法中,以alloc
或new
开头
2. 实例方法中,以autorelease
,init
,retain
或self
开头
当方法返回值为id
类型时,编译器不会返回一个类型不明的对象,会返回一个方法所在类类型的对象,这些方法就被称为是关联返回类型的方法。换句话说,这些方法的返回结果以方法所在的类为类型。
- 非关联返回类型
与关联返回类型相反
1. 类方法中,不以alloc
或new
开头
2. 实例方法中,不以autorelease
,init
,retain
或self
开头
当方法返回值为id
类型时,编译器会返回一个类型不明的对象,即id
类型的对象。
此处划重点!!!此处划重点!!!此处划重点!!!
许多网上的资料并没有拿出证据来证明上述结论的正确性。
一、比如当我们自己的类方法以alloc
等开头后,怎么证明它是关联返回类型,也就是说怎么证明返回值是方法所在类的类型
二、我们的类方法不以alloc
等开头,怎么证明它是非关联返回类型,也就是说怎么证明返回值不是方法所在类的类型
以下是我给出的证明:
// TestObject.h
#import <Foundation/Foundation.h>
@interface TestObject : NSObject
+ (id)newTestObject;
+ (id)allocTestObject;
+ (id)fuckTestObject;
- (void)smallMethod;
@property (nonatomic,copy) NSString *name;
@end
// TestObject.m
#import "TestObject.h"
@implementation TestObject
+ (id)newTestObject {
return [[TestObject alloc] init];
}
+ (id)allocTestObject {
return [[TestObject alloc] init];
}
+ (id)fuckTestObject {
return [[TestObject alloc] init];
}
- (void)smallMethod {
NSLog(@"%s",__func__);
}
@end
我们可以看到,我定义了三个类方法,返回值都是id
类型,按照结论:
-
+ (id)newTestObject
以new
开头的类方法,按照结论应该为关联返回类型,即返回值是TestObject
类型 -
+ (id)allocTestObject
以alloc
开头的类方法,按照结论应该为关联返回类型,即返回值是TestObject
类型 -
+ (id)fuckTestObject
不以alloc
等开头的类方法,按照结论应该为非关联返回类型,即返回值是id
类型
我证明结论的思路是,既然返回值都是id
类型,那么如果我们利用调用实例方法来证明的话,在编译期调用类所属的实例方法是不会报错的,因为id
是动态类型,在编译期不会判断其真实类型,程序启动运行后,由于返回的的确是TestObject
对象,所以我们不论是在编译期还是运行时都看不出其返回值到底是不是如结论所说的TestObject
对象。因此,我们可以通过调用实例对象的属性来证明,id
类型可以接收任何消息,但是却无法使用点语法获取属性。若返回值是TestObject
对象,那么使用点语法便不会报错,如果不是TestObject
对象,使用点语法获取属性就一定会报错。
代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
[[TestObject allocTestObject] smallMethod];
[[TestObject newTestObject] smallMethod];
[[TestObject fuckTestObject] smallMethod];
NSString *temp;
temp = [TestObject allocTestObject].name;
temp = [TestObject newTestObject].name;
temp = [TestObject fuckTestObject].name;
}
如图所示,所有返回值调用方法都可以通过编译,因为返回的都是
id
类型。但是使用点语法时fuckTestObject
的返回值报错,因为此方法的返回值不是关联返回类型,返回的不是TestObject
类型,而是id
类型。因此证明结论是正确的。说完了关联返回类型和非关联返回类型,该说说instancetype
了
instancetype
其实就是为了扩大关联返回类型的范围,让不是以上述关键字开头的方法的返回值也是关联返回类型。
我们将代码稍作调整:
// TestObject.h
+ (instancetype)fuckTestObject;
// TestObject.m
+ (instancetype)fuckTestObject {
return [[TestObject alloc] init];
}
当我们把
fuckTestObject
方法的返回值类型从id
换成instancetype
后,编译就通过了,也就是说返回值是关联返回类型了。
总结一下区别与联系:
-
id
和instancetype
都可以做方法的返回值。 -
id
类型的返回值在编译期不能判断对象的真实类型,即非关联返回类型,instancetype
类型的返回值在编译期可以判断对象的真实类型,即关联返回类型。 -
id
可以用来定义变量, 可以作为返回值, 可以作为形参,instancetype
只能用于作为返回值。
参考资料
卡布达巨人--iOS开发笔记(六):Objective-C动态特性
imy博--OC中的id类型
Mattt--instancetype
网友评论