2.20 类型转换

原文链接: https://docs.swift.org/latest/documentation/the-swift-programming-language/typecasting/

2.20 类型转换

确定值的运行时类型,并为它提供更具体的类型信息。

类型转换是一种检查实例类型的方式,也可以把实例当作它所在类层级中另一个父类或子类来处理。

Swift 中的类型转换用 is 和 as 两个运算符实现。这两个运算符提供了一种简单而富有表现力的方式,用来检查值的类型,或者把值转换成另一种类型。

你也可以用类型转换检查某个类型是否遵循某个协议,详见检查协议遵循性。

为类型转换定义类层级

你可以用类型转换处理类与子类的层级关系,检查某个具体类实例的类型,并把该实例转换成同一层级中的另一个类。下面三段代码定义了一个类层级,以及一个包含这些类实例的数组,供类型转换的例子使用。

第一段代码定义了一个名为 MediaItem 的新基类。这个类为数字媒体库中出现的任何条目提供基本功能:它声明了一个 String 类型的 name 属性和一个 init(name:) 构造器。(这里假定所有媒体条目,包括所有电影和歌曲,都有名字。)

1
2
3
4
5
6
class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

第二段代码定义了 MediaItem 的两个子类。第一个子类 Movie 封装关于电影或影片的附加信息,它在基类 MediaItem 的基础上添加了一个 director 属性以及相应的构造器。第二个子类 Song 则在基类基础上添加了 artist 属性和构造器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)
    }
}

最后一段代码创建了一个名为 library 的常量数组,其中包含两个 Movie 实例和三个 Song 实例。library 数组的类型由用来初始化它的数组字面量推断而来:Swift 的类型检查器能够推断出 Movie 和 Song 有一个公共父类 MediaItem,因此为 library 数组推断出类型 [MediaItem]:

1
2
3
4
5
6
7
8
let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
    Movie(name: "Citizen Kane", director: "Orson Welles"),
    Song(name: "The One And Only", artist: "Chesney Hawkes"),
    Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]
// "library" 的类型被推断为 [MediaItem]

存放在 library 中的元素在背后仍然是 Movie 和 Song 实例。不过,如果你遍历这个数组的内容,取回的元素的类型是 MediaItem,而不是 Movie 或 Song。要按它们的本来类型使用,就需要检查它们的类型,或者把它们向下转换为其他类型,如下所述。

检查类型

用类型检查运算符(is)检查某个实例是否属于某个子类类型。如果实例属于该子类类型,类型检查运算符返回 true,否则返回 false。

下面的例子定义了两个变量 movieCount 和 songCount,用来统计 library 数组中 Movie 和 Song 实例的数量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
var movieCount = 0
var songCount = 0

for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    }
}

print("Media library contains \(movieCount) movies and \(songCount) songs")
// 输出 "Media library contains 2 movies and 3 songs"。

这个例子遍历 library 数组中的所有元素。每一轮循环中,for-in 循环都把常量 item 设为数组中的下一个 MediaItem。

如果当前的 MediaItem 是 Movie 实例,item is Movie 返回 true,否则返回 false;类似地,item is Song 检查该元素是否为 Song 实例。for-in 循环结束时,movieCount 和 songCount 的值分别记录了找到的每种类型的 MediaItem 实例数量。

向下转换

某个类类型的常量或变量在背后实际上可能指向子类的实例。当你认为确实如此时,可以用类型转换运算符(as? 或 as!)尝试向下转换到子类类型。

由于向下转换可能失败,类型转换运算符有两种形式:条件形式 as? 返回你要向下转换到的类型的可选值;强制形式 as! 把向下转换和强制解包合并为一次操作。

当你不确定向下转换是否会成功时,请使用条件形式的类型转换运算符(as?)。它总是返回一个可选值,如果无法转换,该值就是 nil,这让你能够检查转换是否成功。

只有在你确信向下转换一定会成功时,才使用强制形式的类型转换运算符(as!)。如果你试图向下转换成错误的类类型,这种形式会触发运行时错误。

下面的例子遍历 library 中的每个 MediaItem,为每个元素打印合适的描述。为此,它需要把每个元素当作真正的 Movie 或 Song 来访问,而不只是当作 MediaItem,这样才能在描述中使用 Movie 或 Song 的 director 或 artist 属性。

在这个例子里,数组中的每个元素可能是 Movie,也可能是 Song。你事先并不知道每个元素实际的类,因此适合使用条件形式的类型转换运算符(as?),在每轮循环中检查向下转换是否成功:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for item in library {
    if let movie = item as? Movie {
        print("Movie: \(movie.name), dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: \(song.name), by \(song.artist)")
    }
}

// Movie: Casablanca, dir. Michael Curtiz
// Song: Blue Suede Shoes, by Elvis Presley
// Movie: Citizen Kane, dir. Orson Welles
// Song: The One And Only, by Chesney Hawkes
// Song: Never Gonna Give You Up, by Rick Astley

这个例子先尝试把当前的 item 向下转换成 Movie。由于 item 是 MediaItem 实例,它可能是 Movie,同样也可能是 Song,甚至只是基类 MediaItem。正因存在这种不确定性,在尝试向下转换成子类类型时,as? 形式的类型转换运算符返回可选值:item as? Movie 的结果类型是 Movie?,也就是"可选 Movie"。

对库数组中的 Song 实例向下转换成 Movie 会失败。为应对这种情况,上面的例子用可选绑定检查可选 Movie 是否真的包含值(也就是检查向下转换是否成功)。这个可选绑定写作 if let movie = item as? Movie,可以读作:

“尝试把 item 作为 Movie 访问。如果成功,就把返回的可选 Movie 中存放的值赋给一个名为 movie 的新临时常量。”

如果向下转换成功,就用 movie 的属性打印该 Movie 实例的描述,包括其 director 的名字。检查 Song 实例时采用类似的思路,每在库中找到一个 Song,就打印相应的描述(包括 artist 名字)。

注意:转换并不会真正修改实例或改变它的值。 底层实例保持不变,只是把它当作转换目标类型的实例来对待和访问。

Any 与 AnyObject 的类型转换

Swift 提供了两个特殊类型来处理非特定类型:

  • Any 可以表示任何类型的实例,包括函数类型。
  • AnyObject 可以表示任何类类型的实例。

只有当你确实需要 Any 和 AnyObject 提供的行为和能力时,才使用它们。在代码中明确说明你期望处理的类型总是更好的做法。

下面是一个用 Any 处理多种不同类型的例子,其中包括函数类型和非类类型。这个例子创建了一个名为 things 的数组,可以存放 Any 类型的值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var things: [Any] = []

things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })

things 数组包含两个 Int 值、两个 Double 值、一个 String 值、一个 (Double, Double) 类型的元组、电影 “Ghostbusters”,以及一个接受 String 值并返回另一个 String 值的闭包表达式。

要找出只知其为 Any 或 AnyObject 类型的常量或变量的具体类型,你可以在 switch 语句的 case 中使用 is 或 as 模式。下面的例子遍历 things 数组中的元素,并用 switch 语句查询每个元素的类型。有几个 switch case 把匹配到的值绑定到指定类型的常量上,以便打印它的值:

 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
33
for thing in things {
    switch thing {
    case 0 as Int:
        print("zero as an Int")
    case 0 as Double:
        print("zero as a Double")
    case let someInt as Int:
        print("an integer value of \(someInt)")
    case let someDouble as Double where someDouble > 0:
        print("a positive double value of \(someDouble)")
    case is Double:
        print("some other double value that I don't want to print")
    case let someString as String:
        print("a string value of \"\(someString)\"")
    case let (x, y) as (Double, Double):
        print("an (x, y) point at \(x), \(y)")
    case let movie as Movie:
        print("a movie called \(movie.name), dir. \(movie.director)")
    case let stringConverter as (String) -> String:
        print(stringConverter("Michael"))
    default:
        print("something else")
    }
}

// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called Ghostbusters, dir. Ivan Reitman
// Hello, Michael

注意:Any 类型可以表示任何类型的值,包括可选类型。 如果你在需要 Any 类型值的地方使用可选值, Swift 会给出警告。 如果你确实需要把可选值当作 Any 值使用, 可以用 as 运算符把可选值显式转换成 Any, 如下所示。

1
2
3
let optionalNumber: Int? = 3
things.append(optionalNumber)        // 警告
things.append(optionalNumber as Any) // 无警告