Swift类型检查

一、前言

作为面向对象的语言,OC 与 Swift 都具有多态性,有时会大量使用类簇,因此描述对象的信息成为了常见需求。

  • 判断对象在继承树上的位置;
  • 判断对象是否遵守了某个协议;

这些都是对象内省能力的一部分,本文将尝试对 OC 和 Swift 中这些场景的实现做一次整理。

二、OC版

NSObject协议中提供了诸多内省方法,本文主要讨论以下三个:

1
2
3
4
5
6
7
8
//NSObject.h
@protocol NSObject
...
- (BOOL)isKindOfClass:(Class)aClass; // 描述对象的完整继承树(包括直属类、父类、基类)
- (BOOL)isMemberOfClass:(Class)aClass; // 描述对象本身直属的类
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
...
@end

我们先定义两个类,以方便接下来的讨论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// protocol
@protocol MakeNoise <NSObject>
- (void)scream;
@end

// Animal
@interface Animal : NSObject<MakeNoise>
@end

@implementation Animal
- (void)scream{
NSLog(@"++++Animal scream~");
}
@end

// Cat
@interface Cat : Animal
@property (nonatomic, copy) NSString *name; //子类独有的属性
@end

@implementation Cat
- (void)scream{
NSLog(@"+++Cat miao~");
}
@end

接下来进入正题~

1.isMemberOfClass

定义:

1
- (BOOL)isMemberOfClass:(Class)aClass;

作用:

Returns a Boolean value that indicates whether the receiver is an instance of a given class.

检查对象是否是某个类的直属对象

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)classCast{
Animal *animal = [Animal new];
Cat *cat = [Cat new];
Animal *aniCat = [Cat new]; // 父类指针指向子类,即把子类转成父类,向上转型
Cat *catAni = [Animal new]; // 子类指针指向父类,即把父类转化成子类,向下转型,OC中不能这么做
// catAni.name; 这里编译器不会报错,但运行时会闪退,因为catAni实际上指向的是父类Animal,而Animal显然没有name属性。

NSLog(@"+++[animal isMemberOfClass:[Animal class]]:%d",[animal isMemberOfClass:[Animal class]]);
NSLog(@"+++[cat isMemberOfClass:[Animal class]]:%d",[cat isMemberOfClass:[Animal class]]);
NSLog(@"+++[cat isMemberOfClass:[Cat class]]:%d",[cat isMemberOfClass:[Cat class]]);
NSLog(@"+++[aniCat isMemberOfClass:[Animal class]]:%d",[aniCat isMemberOfClass:[Animal class]]);
NSLog(@"+++[aniCat isMemberOfClass:[Cat class]]:%d",[aniCat isMemberOfClass:[Cat class]]);
NSLog(@"+++[catAni isMemberOfClass:[Cat class]]:%d",[catAni isMemberOfClass:[Cat class]]);
NSLog(@"+++[catAni isMemberOfClass:[Animal class]]:%d",[catAni isMemberOfClass:[Animal class]]);
}

日志:

1
2
3
4
5
6
7
+++[animal isMemberOfClass:[Animal class]]:1
+++[cat isMemberOfClass:[Animal class]]:0
+++[cat isMemberOfClass:[Cat class]]:1
+++[aniCat isMemberOfClass:[Animal class]]:0
+++[aniCat isMemberOfClass:[Cat class]]:1
+++[catAni isMemberOfClass:[Cat class]]:0
+++[catAni isMemberOfClass:[Animal class]]:1

分析:

  • animal 直属于 Animal 类;
  • cat 直属于 Cat 类,不直属于 Animal 类;
  • aniCat和catAni都直属于它实际指向的类,不直属于声明它的类;

结论:

  • isMemberOfClass 适合用来判断对象是否直属于某个类。
  • 父类指针指向子类时,此对象实质上是子类的实例;
  • 子类指针指向父类时,此对象实质上是父类的实例。

额外信息:

OC中不允许向下转型,不能将父类转化成子类,即子类指针指向父类。

这是因为父类中没有子类的属性和方法,这样转换出来的子类对象实际上还是指向父类,它不能使用子类中的属性和方法。这么做虽然编译期不会报错,但运行时会闪退。

2.isKindOfClass

定义:

1
- (BOOL)isKindOfClass:(Class)aClass;

作用:

Returns a Boolean value that indicates whether the receiver is an instance of given class or an instance of any class that inherits from that class.

检查对象是否在某个继承树上,包括对象直属的类、对象的父类、基类。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)classCast{
Animal *animal = [Animal new];
Cat *cat = [Cat new];
Animal *aniCat = [Cat new]; // 父类指针指向子类,即将子类转成父类,向上转型
Cat *catAni = [Animal new]; // 子类指针指向父类,即把父类转化成子类,向下转型,OC中不能这么做

NSLog(@"+++[animal isKindOfClass:[Animal class]]:%d",[animal isKindOfClass:[Animal class]]);
NSLog(@"+++[cat isKindOfClass:[Animal class]]:%d",[cat isKindOfClass:[Animal class]]);
NSLog(@"+++[cat isKindOfClass:[Cat class]]:%d",[cat isKindOfClass:[Cat class]]);
NSLog(@"+++[aniCat isKindOfClass:[Animal class]]:%d",[aniCat isKindOfClass:[Animal class]]);
NSLog(@"+++[aniCat isKindOfClass:[Cat class]]:%d",[aniCat isKindOfClass:[Cat class]]);
NSLog(@"+++[catAni isKindOfClass:[Cat class]]:%d",[catAni isKindOfClass:[Cat class]]);
NSLog(@"+++[catAni isKindOfClass:[Animal class]]:%d",[catAni isKindOfClass:[Animal class]]);
}

日志:

1
2
3
4
5
6
7
+++[animal isKindOfClass:[Animal class]]:1
+++[cat isKindOfClass:[Animal class]]:1
+++[cat isKindOfClass:[Cat class]]:1
+++[aniCat isKindOfClass:[Animal class]]:1
+++[aniCat isKindOfClass:[Cat class]]:1
+++[catAni isKindOfClass:[Cat class]]:0
+++[catAni isKindOfClass:[Animal class]]:1

分析:animal、cat、aniCat、catAni 是其直属类、父类、基类的所属类。

结论:只要在对象直属类及其之上的继承树上,isKindOfClass都会返回 YES。

3.conformsToProtocol

定义:

1
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;

作用:

Returns a Boolean value that indicates whether the receiver conforms to a given protocol.

检测对象是否遵守了某个协议。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)protocolCast{
Animal *animal = [Animal new];
Cat *cat = [Cat new];
Animal *aniCat = [Cat new]; // 父类指针指向子类,即将子类转成父类,向上转型

NSLog(@"+++[animal conformsToProtocol:@protocol(MakeNoise)]:%d",
[animal conformsToProtocol:@protocol(MakeNoise)]);

NSLog(@"+++[cat conformsToProtocol:@protocol(MakeNoise)]:%d",
[cat conformsToProtocol:@protocol(MakeNoise)]);

NSLog(@"+++[aniCat conformsToProtocol:@protocol(MakeNoise)]:%d",
[aniCat conformsToProtocol:@protocol(MakeNoise)]);
}

日志:

1
2
3
+++[animal conformsToProtocol:@protocol(MakeNoise)]:1
+++[cat conformsToProtocol:@protocol(MakeNoise)]:1
+++[aniCat conformsToProtocol:@protocol(MakeNoise)]:1

分析:

  • animal 对象所属类显式声明并遵守了 MakeNoise 协议;
  • cat 直属类虽未直接声明遵守 MakeNoise 协议,但它会继承其父类的特点,默认也遵守了父类的协议;
  • aniCat 实质上是 Cat 类型,所以跟 cat 一样,默认继承父类的特点,也遵守了父类中的协议。

结论:若父类遵守了某协议,而子类未直接遵守此协议,则子类依然会从父类中继承此协议。

三、Swift版

Type casting is a way to check the type of an instance, or to treat that instance as a different superclass or subclass from somewhere else in its own class hierarchy.
Type casting in Swift is implemented with the is and as operators. These two operators provide a simple and expressive way to check the type of a value or cast a value to a different type.
You can also use type casting to check whether a type conforms to a protocol, as described in Checking for Protocol Conformance.

isas操作符的作用:

  • 描述实例的类型;
  • 检查类型是否遵守了某个协议。

先定义两个类,方便接下来的讨论:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protocol MakeNoise {
func scream()
}

class Animal: MakeNoise {
func scream() {
print("++\(self)++xxxxx~")
}
}

class Cat: Animal {
override func scream() {
print("++\(self):++Miao~")
}
}

1.is类型检查

Use the type check operator (is) to check whether an instance is of a certain subclass type. The type check operator returns true if the instance is of that subclass type and false if it is not.

is用来检查实例是否属于某个类或子类。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func checkType() {
let animal = Animal()
let cat = Cat()
let aniCat : Animal = Cat() //父类指针指向子类,向上转型

// is 类型检查
if animal is Cat {
print("+++animal is Cat~")
}
if cat is Animal {
print("+++cat is Animal~")
}
if aniCat is Animal {
print("+++aniCat is Animal~")
}
if aniCat is Cat {
print("+++aniCat is Cat~")
}
}

日志:

1
2
3
+++cat is Animal~
+++aniCat is Animal~
+++aniCat is Cat~

日志信息显示:is不仅可检测出实例直属的类,也可以检测其所属的父类~

结论:is的作用与OC中isKindOfClass类似,用于检测实例所在的继承树。

2.as向上转型

转型分为两种:向上转型和向下转型,as主要用于向上转型,Swift不支持向下转型。

作用:检查实例A是否可以向上转型成目标类及其子类的实例,如果可以则返回实例A,否则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func upCast() {
let animal = Animal()
let cat = Cat()
let aniCat : Animal = Cat() //父类指针指向子类
// as 向上转型
let catAsAnimal = (cat as Animal)

// 获取地址
print(Unmanaged.passUnretained(cat).toOpaque())
print(Unmanaged.passUnretained(catAsAnimal).toOpaque())

//检查父类继承树
if catAsAnimal is Animal {
print("++++catAsAnimal is Animal~")
}else{
print("++++catAsAnimal is NOT Animal~")
}

//检查子类继承树
if catAsAnimal is Cat {
print("++++catAsAnimal is Cat~")
}else{
print("++++catAsAnimal is NOT Cat~")
}

// animal as Cat // 这里会报错
aniCat as Animal
// aniCat as Cat // 这里会报错
}

日志:

1
2
3
4
0x00006000030dc660
0x00006000030dc660
++++catAsAnimal is Animal~
++++catAsAnimal is Cat~

catAsAnimalcat实例的内存地址相同,说明实例as转型成功后得到的返回值与其本身是同一个对象。

as可以代替is的功能,可以用于检查实例对象是否在某个类的继承树中;

as不能用于向下转型,即父类转子类,会直接报错,原因在上面OC版中已经分析过~

3.as?\as!转型

作用:as类似,只不过它强调的是你是否确定转型一定成功。

  • as?,用于转型未必成功时,返回可选类型,用可选绑定查看是否转型成功;
  • as!,用于转型一定成功时,如果转型失败则会抛出运行时异常。

as?返回的是目标类的可选类型,如果转型失败则可选值为nil,通过可选绑定即可判断。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func downCast() {
let animal = Animal()
let cat = Cat()
let aniCat : Animal = Cat() //父类指针指向子类,向上转型

// as?\as! 转型
if let _ = aniCat as? Animal {
print("+++aniCat as? Animal~") // 成立
}
if let _ = aniCat as? Cat {
print("+++aniCat as? Cat~") // 成立
}

if let _ = animal as? Animal {
print("+++animal as? Animal~") // 成立
}

// 强制向下转型
if let _ = animal as? Cat {
print("+++animal as? Cat~") // 不成立
}

if let _ = cat as? Animal {
print("+++cat as? Animal~") // 成立
}
if let _ = cat as? Cat {
print("+++cat as? Cat~") // 成立
}

cat as! Animal // 运行正常
animal as! Cat // 运行时错误
}

日志:

1
2
3
4
5
+++aniCat as? Animal~
+++aniCat as? Cat~
+++animal as? Animal~
+++cat as? Animal~
+++cat as? Cat~

结论:as?as!更多的是用来检测异常情况,转型失败则可选值为nil。

4.检测协议

isas操作符除了可以检测继承关系,还可以用来检测是否遵守了某种协议:

You can use the is and as operators described in Type Casting to check for protocol conformance, and to cast to a specific protocol. Checking for and casting to a protocol follows exactly the same syntax as checking for and casting to a type:

The is operator returns true if an instance conforms to a protocol and returns false if it doesn’t.

The as? version of the downcast operator returns an optional value of the protocol’s type, and this value is nil if the instance doesn’t conform to that protocol.

The as! version of the downcast operator forces the downcast to the protocol type and triggers a runtime error if the downcast doesn’t succeed.

  • 变量遵守了协议时is会返回真;否则返回假;
  • as?返回一个遵守了指定协议的可选值,如果实例未遵守协议则可选值为nil;
  • as!将实例强制转型成遵守指定协议的对象,如果转型失败则抛出运行时异常。

#示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func conformsTo() {
let animal = Animal()
let cat = Cat()
let aniCat : Animal = Cat() //父类指针指向子类,向上转型

if animal is MakeNoise {
print("+++animal conforms to MakeNoise~")
}
if cat is MakeNoise {
print("+++cat conforms to MakeNoise~")
}
if aniCat is MakeNoise {
print("+++aniCat conforms to MakeNoise~")
}

animal as? MakeNoise // 运行正常
cat as! MakeNoise // 运行正常
}

日志:

1
2
3
+++animal conforms to MakeNoise~
+++cat conforms to MakeNoise~
+++aniCat conforms to MakeNoise~

四、结尾

从上面的示例和分析来看,OC中的三个内省方法各司其职:

  • isMemberOfClass 只能用来判断对象的直属类;
  • isKindOfClass 不仅能判断对象的直属类,也能判断其间接父类和基类;
  • conformsToProtocol 用来判断对象是否直接或间接实现了某协议;

而 Swift 则对 OC 中的内省进行了抽象,is和as都能胜任以上三种任务,只是:

  • is 更偏向单纯的检查功能,类似于 isKindOfClass 的作用;
  • as 除了检查功能外,还多了尝试去转型的功能,作用更丰富一些。

所以,在实际开发中,我们可以结合语言和功能需要,选择合适的内省方法来描述对象的类型和协议信息~


相关参考:

#©doc.swift

#©Dev.apple


Swift类型检查
https://davidlii.cn/2018/08/20/swift-isa.html
作者
Davidli
发布于
2018年8月20日
许可协议