现在位置: 首页 > Swift 教程 > 正文

Swift 类型转换

类型转换(Type Casting)用来在类层次或协议层次中判断实例的真实类型,并把它当成另一种类型来使用。

Swift 提供了 is 做类型检查,as? 与 as! 做向下转型,type(of:) 取运行时类型。


准备一个类层次

下面定义一套贯穿全文的类层次:媒体资料库的条目。

Movie、Song、Audiobook 都继承自基类 MediaItem。

实例

import Foundation

// 资料库条目的基类
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,具体是电影还是歌曲,要靠类型转换来判断。

实例

// 静态类型是 [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 只回答「是不是这种类型」,不取出实例,也不做转换。

Swift 类型转换的三个运算符:is 返回 Bool 只判断、as? 失败返回 nil、as! 失败直接崩溃;左侧为 MediaItem、Movie、Song 类层次,底部标注 Any 与 AnyObject 的区别

它返回一个 Bool,最适合用在计数、过滤这类不关心具体数据的场合。

实例

var movieCount = 0
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?返回 nilMovie?不确定类型,需要安全判断
as!触发运行时错误,程序崩溃Movie已经百分百确定类型

实际开发中优先用 as?,配合 if let 判断。

实例

// as? 失败返回 nil,用 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! 一步拿到非可选的结果。

实例

// 已知第一条是电影,用 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 movie = Movie(name: "RUNOOB 纪录片", director: "张三")

// 隐式向上转型
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);老教程里那种直接取属性拿类型的写法已经被移除,不要再用。

实例

// 变量的静态类型都是 MediaItem,但运行时类型各不相同
for item in library {
    print("运行时类型:\(type(of: item))")
}

执行以上代码输出:

运行时类型:Movie
运行时类型:Song
运行时类型:Movie
运行时类型:Song
运行时类型:Audiobook

返回值的类型是 MediaItem.Type,也就是一个元类型值,可以拿它做比较。

实例

let item = library[0]

// 比较元类型
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 逐项匹配最清晰。

实例

var things: [Any] = []
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 的用法类似,只是元素必须是类实例。

实例

let objects: [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 则没有直接对应物。