Swift 类型转换
类型转换(Type Casting)用来在类层次或协议层次中判断实例的真实类型,并把它当成另一种类型来使用。
Swift 提供了 is 做类型检查,as? 与 as! 做向下转型,type(of:) 取运行时类型。
准备一个类层次
下面定义一套贯穿全文的类层次:媒体资料库的条目。
Movie、Song、Audiobook 都继承自基类 MediaItem。
实例
// 资料库条目的基类
class MediaItem {
var name: String
init(name: String) {
self.name = name
}
}
// 电影
class Movie: MediaItem {
var director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
// 歌曲
class Song: MediaItem {
var artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
// 有声书
class Audiobook: MediaItem {
var narrator: String
init(name: String, narrator: String) {
self.narrator = narrator
super.init(name: name)
}
}
把这些实例放进同一个数组时,数组的元素类型被统一推断为 [MediaItem]。
此时编译器只知道每个元素是 MediaItem,具体是电影还是歌曲,要靠类型转换来判断。
实例
let library: [MediaItem] = [
Movie(name: "RUNOOB 编程入门", director: "张三"),
Song(name: "菜鸟之歌", artist: "李四"),
Movie(name: "Swift 6 实战", director: "王五"),
Song(name: "www.runoob.com", artist: "RUNOOB 乐队"),
Audiobook(name: "Swift 权威指南", narrator: "赵六")
]
类型检查:is
is 只回答「是不是这种类型」,不取出实例,也不做转换。
它返回一个 Bool,最适合用在计数、过滤这类不关心具体数据的场合。
实例
var songCount = 0
var audiobookCount = 0
// is 只做检查,不做转换
for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
} else if item is Audiobook {
audiobookCount += 1
}
}
print("电影 \(movieCount) 部,歌曲 \(songCount) 首,有声书 \(audiobookCount) 本")
执行以上代码输出:
电影 2 部,歌曲 2 首,有声书 1 本
注意:
is判断的是运行时类型,包括子类。因为Movie继承自MediaItem,所以movie is MediaItem也为true。
向下转型:as? 与 as!
向下转型(downcasting)是把父类类型的实例当作子类来用,这样才能访问子类特有的成员。
因为父类引用指向的未必真是那个子类,转换可能失败,所以有两种写法。
| 写法 | 转换失败时 | 返回类型 | 适用场景 |
|---|---|---|---|
as? | 返回 nil | Movie? | 不确定类型,需要安全判断 |
as! | 触发运行时错误,程序崩溃 | Movie | 已经百分百确定类型 |
实际开发中优先用 as?,配合 if let 判断。
实例
for item in library {
if let movie = item as? Movie {
print("电影:\(movie.name),导演:\(movie.director)")
} else if let song = item as? Song {
print("歌曲:\(song.name),歌手:\(song.artist)")
} else if let audiobook = item as? Audiobook {
print("有声书:\(audiobook.name),朗读者:\(audiobook.narrator)")
}
}
执行以上代码输出:
电影:RUNOOB 编程入门,导演:张三 歌曲:菜鸟之歌,歌手:李四 电影:Swift 6 实战,导演:王五 歌曲:www.runoob.com,歌手:RUNOOB 乐队 有声书:Swift 权威指南,朗读者:赵六
已经确定某个位置就是电影时,可以用 as! 一步拿到非可选的结果。
实例
let first = library[0] as! Movie
print("第一条是电影:\(first.name)")
// 更稳妥的写法:先判断再转型
if let second = library[1] as? Song {
print("第二条是歌曲:\(second.artist)")
}
执行以上代码输出:
第一条是电影:RUNOOB 编程入门 第二条是歌曲:李四
注意:如果
as!的目标类型不对,程序会直接终止并打印类似Could not cast value of type 'Song' to 'Movie'.的运行时错误。这个错误无法用do-catch捕获,所以只在逻辑上不可能失败时才用as!。
向上转型与 as
把子类实例当作父类使用叫做向上转型(upcasting),它一定成功,因此不需要问号和感叹号。
多数情况下直接赋值就够了,编译器会自动完成;需要显式说明类型时,可以写 as。
实例
// 隐式向上转型
let item: MediaItem = movie
// 显式写出 as,效果相同
let explicitItem = movie as MediaItem
print("隐式:\(item.name)")
print("显式:\(explicitItem.name)")
// 向上转型后,子类特有成员不可见
// print(item.director) // 编译错误:MediaItem 没有 director
if let backToMovie = item as? Movie {
print("转型回来才能访问导演:\(backToMovie.director)")
}
执行以上代码输出:
隐式:RUNOOB 纪录片 显式:RUNOOB 纪录片 转型回来才能访问导演:张三
向上转型只是「换一个更宽的类型来看同一个对象」,实例本身没有变化,也不需要复制。
type(of:) 获取运行时类型
type(of:) 返回一个值的动态类型,是调试和动态派发时最常用的工具。
注意它的参数带标签,写成 type(of: item);老教程里那种直接取属性拿类型的写法已经被移除,不要再用。
实例
for item in library {
print("运行时类型:\(type(of: item))")
}
执行以上代码输出:
运行时类型:Movie 运行时类型:Song 运行时类型:Movie 运行时类型:Song 运行时类型:Audiobook
返回值的类型是 MediaItem.Type,也就是一个元类型值,可以拿它做比较。
实例
// 比较元类型
if type(of: item) == Movie.self {
print("这一条是电影")
}
// 也可以直接打印元类型的描述
print("类型名:\(type(of: item))")
print("静态类型:\(MediaItem.self)")
执行以上代码输出:
这一条是电影 类型名:Movie 静态类型:MediaItem
注意:
type(of:)取的是运行时类型,MediaItem.self取的是写在代码里的静态类型,两者不要混淆。
Any 与 AnyObject
Any 表示「任意类型的值」,包括结构体、枚举、函数和闭包。
AnyObject 表示「任意类类型的实例」,范围比 Any 窄。
| 类型 | 能装什么 | 典型用途 |
|---|---|---|
Any | 任意类型:类、结构体、枚举、函数、元组 | 混合容器、与动态语言交互 |
AnyObject | 只能是类实例 | 与 Objective-C 的 id 对应 |
取出 Any 里的值时,用 switch 配合 as 逐项匹配最清晰。
实例
things.append(0) // Int
things.append(0.0) // Double
things.append(42) // Int
things.append(3.14159) // Double
things.append("hello runoob") // String
things.append((3.0, 5.0)) // 元组
things.append(Movie(name: "RUNOOB 纪录片", director: "张三")) // 类实例
things.append({ (name: String) -> String in "Hello, \(name)" }) // 闭包
for thing in things {
switch thing {
case let someInt as Int:
print("整数:\(someInt)")
case let someDouble as Double where someDouble > 0:
print("正浮点数:\(someDouble)")
case let someString as String:
print("字符串:\(someString)")
case let (x, y) as (Double, Double):
print("元组:\(x), \(y)")
case let movie as Movie:
print("电影:\(movie.name)")
case let stringConverter as (String) -> String:
print(stringConverter("RUNOOB"))
default:
print("其他")
}
}
执行以上代码输出:
整数:0 其他 整数:42 正浮点数:3.14159 字符串:hello runoob 元组:3.0, 5.0 电影:RUNOOB 纪录片 Hello, RUNOOB
第二个元素是 0.0,因为 where someDouble > 0 不成立,落到了 default 分支。
AnyObject 的用法类似,只是元素必须是类实例。
实例
Movie(name: "菜鸟教程", director: "RUNOOB"),
Song(name: "RUNOOB 之歌", artist: "菜鸟乐队")
]
for object in objects {
print("AnyObject 实际类型:\(type(of: object))")
}
执行以上代码输出:
AnyObject 实际类型:Movie AnyObject 实际类型:Song
注意:不要把可选值直接放进
[Any],Swift 会给出expression implicitly coerced from 'Int?' to 'Any'警告,因为可选值会被包装成一个Optional再存入。确实需要存时,请先解包或显式写成as Any。
存在类型 any P 的转换
协议类型现在写作 any Protocol,表示「任意遵循该协议的类型」。
通过 any P 只能访问协议里声明的成员,要访问具体类型独有的成员,同样需要向下转型。
实例
protocol Describable {
var summary: String { get }
}
extension Movie: Describable {
var summary: String { return "电影《\(name)》,导演 \(director)" }
}
extension Song: Describable {
var summary: String { return "歌曲《\(name)》,歌手 \(artist)" }
}
// 存在类型数组:元素的静态类型是 any Describable
let describables: [any Describable] = [
Movie(name: "RUNOOB 编程入门", director: "张三"),
Song(name: "菜鸟之歌", artist: "李四")
]
for item in describables {
print(item.summary) // 协议要求的成员,直接可用
if let movie = item as? Movie { // 向下转型回具体类型
print("这是电影,导演是 \(movie.director)")
} else if let song = item as? Song {
print("这是歌曲,歌手是 \(song.artist)")
}
print("实际类型:\(type(of: item))")
}
执行以上代码输出:
电影《RUNOOB 编程入门》,导演 张三 这是电影,导演是 张三 实际类型:Movie 歌曲《菜鸟之歌》,歌手 李四 这是歌曲,歌手是 李四 实际类型:Song
从具体类型转成 any P 是隐式的,因为编译器能确认它遵循了协议。
反过来从 any P 转到具体类型则必须显式写 as? 或 as!,因为运行时不一定真是那个类型。
常见问题
下面几个问题在类型转换中最容易出错。
什么时候该用 is,什么时候该用 as?
只想知道类型、不需要访问成员时用 is。
需要拿到转型后的实例并访问子类成员时用 as?。
as? 和 as! 能不能互换
不能。把 as! 换成 as? 只是把崩溃换成返回 nil,还需要额外处理 nil 分支。
反过来把 as? 换成 as! 则是把安全判断变成潜在崩溃,除非逻辑上确定成立,否则不要这么做。
为什么 item as? Movie 对某些元素返回 nil
因为向下转型检查的是运行时类型,只有元素确实是 Movie 或其子类时才成功。
数组中混有 Song 时,对它做 as? Movie 自然得到 nil。
Swift 和 Objective-C 的类型转换一样吗
思路相似,但 Swift 的 as? 与 as! 区分了「可能失败」和「必定成功」,比 Objective-C 的 isKindOfClass: 更明确。
与 Objective-C 交互时,AnyObject 大致对应 id,Any 则没有直接对应物。
