为什么要"类型转换"
Swift 是强类型语言,编译期就必须知道每个变量的真实类型。
但在面向对象、协议、泛型甚至混用 OC 的场景里,变量"静态类型"与"实际类型"常常不一致。
类型转换(Type Casting)就是用来:
- 检查"实际类型"到底是谁(is)
- 把"静态类型"当成别的类型来用(as / as? / as!)
- 处理"任意类型"这种黑盒(Any / AnyObject)
核心运算符速查表
| 运算符 | 返回类型 | 可能失败 | 用途示例 |
|---|---|---|---|
is |
Bool |
不会崩溃 | 判断"是不是"某类型 |
as? |
可选值 | 会返回nil |
安全向下转型(失败不炸) |
as! |
非可选值 | 可能崩溃 | 强制向下转型(失败运行时错误) |
as |
原类型 | 不会失败 | 向上转型或桥接(OC↔Swift) |
建立实验田:先搭一个类层级
swift
// ① 根类:媒体条目
class MediaItem {
let name: String
init(name: String) { self.name = name }
}
// ② 子类:电影
class Movie: MediaItem {
let director: String
init(name: String, director: String) {
self.director = director
super.init(name: name)
}
}
// ③ 子类:歌曲
class Song: MediaItem {
let artist: String
init(name: String, artist: String) {
self.artist = artist
super.init(name: name)
}
}
// ④ 仓库:存放所有媒体
let library: [MediaItem] = [
Movie(name: "卧虎藏龙", director: "李安"),
Song(name: "青花瓷", artist: "周杰伦"),
Movie(name: "星际穿越", director: "诺兰"),
Song(name: "晴天", artist: "周杰伦"),
Song(name: "夜曲", artist: "周杰伦")
]
虽然数组静态类型是[MediaItem],但运行时每个元素仍然是原来的Movie或Song。
想访问director/artist?先检查、再转换------这就是本文主题。
检查类型:is 的用法
需求:统计library里电影、歌曲各多少。
swift
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie { movieCount += 1 }
if item is Song { songCount += 1 }
}
print("电影\(movieCount)部,歌曲\(songCount)首")
// 打印:电影2部,歌曲3首
要点
is只回答"是/否",不改动类型。- 对协议也适用,例如
item is CustomStringConvertible。
向下转型:as? 与 as! 的抉择
需求:把每个元素的详细信息打印出来,需要访问子类独有属性。
swift
for item in library {
// 1. 先尝试当成电影
if let movie = item as? Movie {
print("电影:《\(movie.name)》------导演:\(movie.director)")
continue
}
// 2. 再尝试当成歌曲
if let song = item as? Song {
print("歌曲:《\(song.name)》------歌手:\(song.artist)")
}
}
输出: 电影:《卧虎藏龙》------导演:李安 歌曲:《青花瓷》------歌手:周杰伦 电影:《星际穿越》------导演:诺兰 歌曲:《晴天》------歌手:周杰伦 歌曲:《夜曲》------歌手:周杰伦
经验
- 不确定成功用
as?+可选绑定,几乎不会错。 - 只有100%确定时才写
as!,否则崩溃现场见:
swift
let first = library[0] as! Song // 运行时错误:Could not cast value of type 'Movie' to 'Song'
向上转型:as 的"隐形"场景
向上转是最安全的,因为子类一定能当父类用,Swift甚至允许省略as:
swift
let m: Movie = Movie(name: "哪吒", director: "饺子")
let item: MediaItem = m // 编译器自动向上转
但在某些桥接场景必须显式写as:
swift
// NSArray 只能装 NSObject,Swift String 需要桥接
let ocArray: NSArray = ["A", "B", "C"] as NSArray
Any 与 AnyObject:万金油盒子
| 类型 | 能装什么 | 常见场景 |
|---|---|---|
| Any | 任何类型(含struct/enum/closure) | JSON、脚本语言交互 |
| AnyObject | 任何class(含@objc协议) | OC SDK、UITableView datasource |
示例:把"完全不相干"的值塞进一个数组
swift
var things = [Any]()
things.append(42) // Int
things.append(3.14) // Double
things.append("Hello") // String
things.append((2.0, 5.0)) // 元组
things.append(Movie(name: "哪吒", director: "饺子"))
things.append({ (name: String) -> String in "Hi, \(name)" }) // 闭包
怎么把这些值取出来?switch + 模式匹配最清晰:
swift
for thing in things {
switch thing {
case let int as Int:
print("整数值:\(int)")
case let double as Double:
print("小数值:\(double)")
case let str as String:
print("字符串:\(str)")
case let (x, y) as (Double, Double):
print("坐标:(\(x), \(y))")
case let movie as Movie:
print("任意盒里的电影:\(movie.name)")
case let closure as (String) -> String:
print("闭包返回:\(closure("Swift"))")
default:
print("未匹配到的类型")
}
}
要点
- 用
as模式可以一次性完成"类型检查+绑定"。 - 不要滥用Any/AnyObject,你会失去编译期检查,代码维护成本陡增。
常见踩坑与调试技巧
- "is"对协议要求苛刻
swift
protocol Playable { }
extension Song: Playable { }
let s: MediaItem = Song(name: "x", artist: "y")
print(s is Playable) // true
之前的某个Swift版本,会将s推断为MediaItem,而MediaItem没实现Playable协议,所以返回false
从4.x之后s的实际类型是Sone,返回true
- JSON转字典后全成Any
swift
let json: [String: Any] = ["age": 18]
let age = json["age"] as! Int + 1 // 万一服务器返回String就崩
用as?+guard提前返回,或Codable一步到位。
- as!链式写法
swift
let label = (view.subviews[0] as! UILabel).text as! String // 两层强转,一层失败就崩
建议分步+可选绑定,或使用if let label = view.subviews.first as? UILabel。
实战延伸:类型转换在架构中的身影
- MVVM差异加载
tableView同一cellForRow里根据item is HeaderItem / DetailItem画不同UI。
- 路由/插件
URL路由把参数打包成[String: Any],各插件再as?取出自己关心的类型。
- 单元测试
用XCTAssertTrue(mock is MockNetworkClient)确保测试替身注入正确。
- OC老SDK混编
UIViewController→自定义子类,as!前先用isKind(of:)(OC习惯)或is检查。
总结与私货
- 类型转换不是"黑科技",它只是把运行时类型信息暴露给开发者。
- 优先用
as?+可选绑定,让错误止步于nil;as!留给自己能写单元测试担保的场景。 - Any/AnyObject是"逃生舱",一旦打开就等于对编译器说"相信我"。能不用就不用,实在要用就封装成明确的枚举或struct,把转换工作限制在最小作用域。
- 在团队Code Review里,见到
as!可以强制要求写注释说明为什么不会崩;这是用制度换安全感。 - 如果业务里大量
is/as泛滥,多半协议/泛型抽象得不够,可以考虑重构:- 用协议扩展把"差异化行为"做成多态,而不是if/else判断类型。
- 用泛型把"运行时类型"提前到"编译期类型",减少转换。