跳转至

类型

Swift 一个重要的概念是类型。类型是 Swift 中实现面向对象编程(object-oriented programming)、面向协议编程(protocol-oriented programming)以及泛型编程(generic programming)的基础。

在 Swift 中,所有变量的类型要么属于:

  • class
  • struct
  • enum
  • protocol(一定条件下)

要么属于:

  • tuple
  • 函数

我们分别介绍上面的几种类型,在其中会讨论它们之间的联系与区别。

class

Swift 中的 class 与 C++ 类似。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Counter {
    private(set) var count = 0
    func increment() {
        count += 1
    }
    func increment(by amount: Int) {
        count += amount
    }
    func reset() {
        count = 0
    }
}

成员函数、成员变量及其访问控制

Swift 中成员函数和成员变量的概念与 C++ 基本一致,一个类对象拥有独立的成员变量,成员函数可以访问所有的成员变量。

class 的访问控制通过在成员声明前加上修饰符实现,包括:

  • public默认,外界可访问,可写
  • private:外界不可访问
  • private(set)(成员变量):外界可访问,不可写

在类中通过 self 引用自身,如果不会产生歧义,可以省略 self

在类中时常会用到一种特殊的变量:计算变量。计算变量相当于一个没有参数的函数,每次获取其值的时候都要通过调用这个函数来计算:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        return width * height
    }
    ...
}

let rectangle = Rectangle(...)
print(rectangle.area)

计算变量是只读的,而且不会占用储存空间。如果计算的时间复杂度超过 O(1),那么应该使用普通的函数,而不是计算变量。

构造函数和析构函数

构造函数使用关键字 init 声明。由于 Swift 没有零值初始化,所有成员变量都要有初值,要么直接在变量后显式指出,要么在构造函数中设定。构造函数结束时,所有成员变量都要有初值,否则产生编译错误。

1
2
3
4
5
6
7
8
9
class A {
    var bar: Int
    var foo = "Hello"

    init(bar: Int) {
        self.bar = bar
    }
}
let a = A(bar: 1)

如果类的每个成员变量都有默认值,那么可以使用默认构造函数:

1
2
3
4
5
class B {
    var bar = 1
    var foo = "Hello"
}
let b = B()

析构函数使用关键字 deinit 声明,不过在一般的开发中很少用到:

1
2
3
deinit {
    ...
}

静态变量、静态方法

使用 static 定义静态变量和方法:

1
2
3
4
5
6
7
class A {
    static let shared = A()
    static func foo() {...}
}

let a = A.shared
A.foo()

类是引用类型

Swift 中的类是引用类型,传递引用而非副本,相当于指针。比如:

1
2
3
4
5
6
7
8
9
class Counter {
    var count = 0
}

let a = Counter()
let b = a   // b is a reference of a

b.count += 1
print(a.count)  // 1

Swift 中对类的实例的管理使用类似于智能指针的方法,当引用计数为 0 时,实例被销毁。另外,可以在 var 前加上 weak 声明不参与计数的 weak 变量。

继承

Swift 支持类的继承,但不支持多重继承。与 C++ 不同,Swift 的继承没有 privateprotectedpublic 之分,所有继承都相当于 C++ 中的 public

另外,Swift 中所有的函数都相当于 C++ 中的虚函数,在派生类中使用 override 即可重写基类的方法;使用 super 调用基类的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class Base {
    func foo() {
        print("Base")
    }
}

class Derived: Base {
    override func foo() {
        super.foo() // prints "Base"
        print("Derived")
    }
}

let derived = Derived()
derived.foo() // "Base Derived"

使用 as 向下进行类型转换,使用 as? 向上进行动态类型转换,使用 as! 进行强制类型转换:

1
2
3
4
5
6
var a: Base = Derived()
let b = a as? Derived // 类型为 Optional<Derived>
// 或 if let b = a as? Derived {...}

let c = a as! Derived // 类型为 Derived,若转换失败则导致崩溃
let d = c as Base     // 类型为 Base,向下转换必然成功

结构体 struct

与 C++ 不同,Swift 中的 structclass 有共同之处,但并不能等同。

structclass 类似,可以定义成员变量、成员函数、静态成员、构造函数,访问控制与类相同。

它们之间的不同点有:

  • struct值类型。也就是说,传递的是原有实例的副本,而非引用
  • struct 没有继承
  • struct 不能定义析构函数

class 进行对照

使用上面的例子:

1
2
3
4
5
6
7
8
9
struct Counter {
    var count = 0
}

var a = Counter()
var b = a   // b is a copy of a

b.count += 1    // b.count == 1
print(a.count)  // 0

struct 由于没有继承,因此对成员的构造函数(英语:Memberwise initializer)能够自动生成,大多数情况下无需手动写构造函数:

1
2
3
4
5
6
struct A {
    var foo: Int
    var bar: String
}

let a = A(foo: 1, bar: "Hello")

另外,struct 的成员函数默认不可更改成员变量,如果需要更改,则需要在函数前加上 mutating

1
2
3
4
5
6
struct Counter {
    var count = 0
    mutating func increase() {
        count += 1
    }
}

关于 structclass 的使用

看上去 structclass 功能弱了不少,但 struct 通过遵循协议(英语:protocol,在下面介绍)可以实现继承的效果,同时由于传递的不是引用,因此大大降低了意料之外的对某个实例更改的概率。而且,struct 采用 copy-on-write 的策略,只有在更改时才会真正进行复制,因此性能上并没有太多的损失。

实际上,现代的 Swift 框架,如 SwiftUI、Combine,已经几乎全部使用 struct 进行面向对象/面向协议的编程。官方的建议是:除非只能用 class,比如要在多个地方引用同一个实例,否则优先使用 struct

枚举 enum

Swift 的 enum 提供了一种类型安全的枚举方法。

1
2
3
4
5
enum CompassPoint {
    case north, south, east, west
}

var directionToHead = CompassPoint.north

enum 可以使用 switch 语句来匹配:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
directionToHead = .south
switch directionToHead {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east:
    print("Where the sun rises")
case .west:
    print("Where the skies are blue")
}

或者用 == 判断:

1
2
3
if directionToHead == .north {
    print("我飘向北方")
}

类型省略写法

上面 directionToHead 的类型已经明确,因此可以将 CompassPoint.south 缩写为 .south。这种写法对于 classstruct 的静态成员和方法同样适用。如:

1
2
3
4
5
struct Color {
    static let yellow = Color(...)
}

let color: Color = .yellow

迭代

可以在 enum 后面加上 CaseIterable 使得可以按照声明顺序枚举所有的类型:

1
2
3
4
5
6
7
enum CompassPoint: CaseIterable {
    case north, south, east, west
}

for point in CompassPoint.allCases {
    ...
}

原始值

enum 可以有一个原始值(raw value)类型,可以是 IntString 等等,可以用来初始化 enum 类型。如果不指定值,编译期会自动设置对应的值,如 Int 是从 0 开始赋值并依次递增,而 String 对应各个 case 的名称。

1
2
3
4
5
6
7
8
9
enum Status: Int {
    case onVacation     // 0
    case atSchool = 10
    case quit           // 11
}

print(Status.onVacation.rawValue)   // 0
let status = Status(rawValue: 5)    // nil
let status = Status(rawValue: 10)   // Optional(Status.atSchool)

关联值

enum 的每个 case 可以储存一个或多个关联值(associated value)。需要注意,一旦储存了关联值,就不能够指定 raw value,也不能够通过 == 判断是否相等:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
enum Food {
    case hamburger(calories: Double)
    case juice(amount: Double)
}


let breakfast = Food.hamburger(calories: 294.9)

switch breakfast {
case .hamburger(let calories):
    print("I ate a hamburger with \(calories) calories")
default:
    break
}

// or
if case .hamburger(let calories) = breakfast {
    print("I ate a hamburger with \(calories) calories")
}

使用关联值处理互斥情况

enum 关联值的一个重要的作用是处理若干互斥情况。

比如,Optional 类型实际上就是一个带有 associated value 的 enum(泛型稍后介绍):

1
2
3
4
enum Optional<Wrapped> {
    case .some(Wrapped)     // has value
    case .none              // nil
}

再如,包装结果和错误的 Result

1
2
3
4
enum Result<Success, Failure> where Failure : Error {
    case success(Success)
    case failure(Failure)
}

一个请求要么成功(此时返回有效的结果),要么失败(此时返回错误)。使用 enum 就可以在不使用 Optional 的情况下以类型安全的方法返回一个唯一的结果:

1
2
3
4
5
6
switch result {
case .success(let data):
    print("Request succeeded with data: \(data)")
case .failure(let error):
    print("Request failed with error: \(error)")
}

函数、构造函数和变量

enum 是 Swift 的一等类型,它可以有成员函数、构造函数、计算变量、静态函数、静态变量,但不能储存值,即不能拥有成员变量。

能够给 enum 添加函数等给编程带来了极大的便利:

 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
enum HTTPError: Int {
    case unknown
    case badRequest = 400
    case notFound = 404
    case forbidden = 403

    init(code: Int) {
        if let error = HTTPError(rawValue: code) {
            self = error
        } else {
            self = .unknown
        }
    }

    var description: String {
        switch self {
        case .badRequest: return "Bad Request"
        case .notFound: return "Not Found"
        ...
        }
    }
}

let error = HTTPError(code: 404)
print(error.description)            // "Not Found"

另外,一般使用 enum 加上静态变量来限定命名空间:

1
2
3
4
5
6
enum Constant {
    static let accentColor = Color.blue
    static let backgroundColor = Color.white
}

let color = Constant.accentColor

协议 protocol

Swift 中的 protocol(协议)与 C++ 的抽象类概念对应,其作用是定义一组需要实现的接口。比如,Swift 中的 Equatable,其作用是一个判等器:

1
2
3
protocol Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool
}

满足协议要求的类型称为遵循(conform to)此协议。如果一个类型想要遵循 Equatable,需要:

  1. 在类型名称后面加上 Equatable
  2. 实现 == 方法

比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct Rectangle: Equatable {
    var width: Double
    var height: Double

    static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
        return lhs.width == rhs.width && lhs.height == rhs.height
    }
}

let r1 = Rectangle(width: 1, height: 3)
let r2 = Rectangle(width: 1, height: 2)
print(r1 == r2)     // false

let rectangles = [r1, r2]   // An array of rectangles
print(rectangles.contains(Rectangle(width: 1, height: 2)))  // true

在这里,Array 本身并不知道如何判断它其中是否有元素与提供的 Rectangle 相等,但由于 Rectangle 遵循了 Equatable 协议,使得 Array 可以调用 Equatable 规定的判等接口来实现判断。

Note

值得注意的是,这种接口的规定是严格的。Arraycontains 方法当且仅当其元素类型 ElementEquatable 时才存在:

1
2
3
extension Array where Element: Equatable {
    func contains(_ element: Element) -> Bool {...}
}

有关泛型和 extension,我们在后面介绍。

接口的规定方法

protocol 可以规定几类接口:

  • 成员函数
  • 成员变量
  • 静态函数
  • 静态变量
  • 构造函数
1
2
3
4
5
6
7
8
9
protocol SomeProtocol {
    var foo: Int { get set }
    var bar: Int { get }
    static var staticMember: Int { get }

    func someMethod(a: Int) -> Int
    static func staticMethod() -> Bool
    init(code: Int)
}

其中,规定变量的接口时,只能用 var,而且需要在类型名后加上 { get }{ get set } 以指定需要满足的最小权限要求。比如,某个类型遵循 SomeProtocol 时,它的 foo 变量需要可读写,而 bar 变量至少可以读,但不能限制其是否可写

协议作为类型

当没有 associated type(稍后介绍)时,协议可以作为类型,并且其真正的类型可以改变,如 Swift 中内置的 Encodable

1
2
var someEncodable: Encodable = 1
someEncodable = "Hello"

协议方法的默认实现

在规定了协议的接口后,可以在 extension 中对除了 init 以外的接口进行默认实现,但不能在 extension 中实现储存变量:

1
2
3
4
5
6
7
8
extension SomeProtocol {
    var bar: Int {
        return 1
    }
    func someMethod(a: Int) -> Int {
        return a
    }
}

这样,当某个类型遵循 SomeProtocol 时,可以不用实现这些已经进行默认实现的接口。

协议的组合与继承

一个类型可以遵循多个协议,需要实现所有协议规定的接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protocol A {
    var foo: Int { get }
}
protocol B {
    func bar()
}

struct: SomeStructure: A, B {
    var foo: Int
    func bar() {...}
}

一个协议可以继承多个协议,其作用为添加这些协议的规定的接口作为自身的限制条件,需要实现所有的接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protocol A {
    var foo: Int { get }
}
protocol B: A {
    func bar()
}

struct: SomeStructure: B {
    var foo: Int
    func bar() {...}
}

Potocol-oriented Programming

通过遵循协议、对协议进行默认的实现、协议的继承,使得 structenum 这些没有继承功能的类型可以因此实现类似于继承的效果。

现代 Swift 框架大量使用 protocol 进行面向协议的编程(potocol-oriented programming)。其思想类似于 OOP,但在某些问题的处理上比类继承要更加简洁,且不易出错。

extension

classstructenumprotocol 可以通过 extension 添加方法(计算变量或函数),包括所有的内置类型:

1
2
3
4
5
6
7
extension Int {
    var string: String {
        return "\(self)"
    }
}

print(1.string)

使用 extension 可以以面向对象的方式给任何类型添加方法。

另外,一般为了区分不同的 protocol 的接口,在遵循不同的协议时,往往将各个协议规定的接口实现在不同的 extension 中:

1
2
3
4
5
6
7
8
9
struct A {
    // Original members
}
extension A: ProtocolA {
    // Implementation of ProtocolA
}
extension A: ProtocolB {
    // Implementation of ProtocolB
}

元组

元组(tuple)用于传递复合值,用括号组织,可以带标签,用标签或者成员(而非下标)0、1、2 等取出其中的值:

1
2
3
4
5
6
7
8
func getCurrentTime() -> (hour: Int, minute: Int, second: Int) {
    return (18, 20, 36)
}

let time = getCurrentTime()
print(time.hour)
// or
print(time.0) // hour

元组的元素数量是固定的,不存在越界的情况。

函数

Swift 中,函数也是一种类型,可以储存在变量中,也可以作为参数/返回值参与另一个函数。函数类型的声明为 (参数列表) -> 返回类型

1
2
var someHandler: (Int, Int) -> Void
var optionalHandler: ((Int, Int) -> Void)? // Optional

使用大括号来生成匿名函数(闭包,closure),在括号内使用 参数列表 -> 返回类型 in 给传入的各个参数命名:

1
2
3
let incrementer: (Int) -> Int = { value -> Int in
    return value + 1
}

调用方法与函数相同:

1
let result = incrementer(1) // 2

上下文明确时,可以省略括号内的 -> Int

1
2
3
let incrementer: (Int) -> Int = { value in
    return value + 1
}

另外,可以省略参数列表,用 $0$1 等指代第一、第二个元素:

1
2
3
4
5
let incrementer: (Int) -> Int = {
    return $0 + 1
}

[1, 2, 3].removeAll(where: { $0 < 2 })  // [2, 3]

Note

闭包的一个典型应用场景是作为参数传入函数中作为回调函数。如,进行网络请求,当请求完成时,调用传入的回调函数来进行结果的处理:

1
2
3
4
5
6
7
let task = session.dataTask(with: url) { data, response, error in
    if let data = data {
        let string = String(data: data, encoding: .utf8)!
        print(string)
    }
}
task.resume()

上面的例子使用了一种叫做 trailing closures 的语法,使得回调函数的函数体可以写在函数的“外面”,详见 Swift 官方教程


最后更新: 2022年1月17日
作者: Ashitemaru (4.48%), 梁业升 (95.52%)