Swift5.6 官方文档part1

3,643 阅读17分钟

About Swift — The Swift Programming Language (Swift 5.6)

欢迎使用swift

关于swift

swift是一门极好的编写软件的语言, 无论是用来写手机软件、桌面软件、服务器或者是其他跑代码的东西。它是安全、快速、交互的编程语言, 在现代编程语言中结合了最好的、来自于广泛的苹果工程文化和其开源社区的各种贡献。编译器为性能进行了优化, 语言为开发做了优化, 不将就。

swift对于新手来说是友好的。它是一门具备工业品质的编程语言, 同时也是富有表现力和令人愉快的脚本语言。在playground编写swfit让你体验编程并能够马上看到结果, 不需要build和run的开销

swift通过采用现代编程模式, 避免了常规编程的常见错误

  • 变量永远在使用之前初始化
  • 数组的索引会做越界检查
  • Integer会做溢出检查
  • 可选项确保nil会被显示处理
  • 自动内存管理
  • 错误处理允许从异常中进行可控的恢复

swift代码为了能充分利用现代硬件,是编译和优化过的。语法和标准库被设计建立在引导原则上, 按照提示的方式去书写你的代码得到最佳运行效果。它结合了安全和速度, 是从hello world到完整的可操作系统的构建的不错的选择

swift结合了强大的类型接口和现代的、轻量级的语法、允许复杂的想法以简洁明了的方式实现的模式. 代码不单单更容易书写而且更容易阅读和掌握

swift已经发展多年, 也在持续地进化出新功能和能力。我们对于swift充满信心。已经等不及看你使用swift进行创作了。

版本兼容性

书中描绘的是swift5.6, XCode13默认的swift版本, 你也可以使用XCode13构建swift4.2或者swift4。当你使用XCode13构建swift4、swift4.2的代码时, 多数的swift5.6的功能是可用的。以下变化只针对使用swift5.6及以后的代码有效

  • 返回不透明类型的函数需要swift5.1 runtime
  • try?对于已经返回可选项的表达式不再需要引入一个可选值
  • large integer初始化的时候会推断为正确的integer类型

并发需要swift5.6或之后的版本, 以及一个提供了应对并发类型的swift标准库。在苹果平台, 设置目标运行环境最低为iOS15、macOS12、tvOS15 或者 watchOS8.0

一个swift5.6的项目可以依赖于一个swift4.2或者swift4的项目, 反之亦然。如果你存在一个大的项目划分成了多个frameworks, 你可以一次将代码从swift4.0迁移到swift5.6

总览

Hello World

print("Hello, world!")
// Prints "Hello, world!"

这一行代码直接就能执行 不需要为输入输出功能或者字符串处理导入单独的一个库。在全局范围编写的代码作为程序的入口, 所以也不需要main(), 你也不需要在每句代码后面加分号

  • 建议配合playground使用这一章节, 它能够让你在写代码的时候就看见结果

简单的值

let标识常量, var标识变量。常量的值在编译的时候不需要被知道它的类型, 但你必须声明时给它赋值。你可以声明一个常量然后在其他地方使用

var myVariable = 42
myVariable = 50
let myConstant = 42

常量和变量必须和你想给它赋的值是同一个类型。你不需要显式地指定类型, 在你给他们赋值的时候编译器会指定他们的类型。

如果初始值不能提供足够的信息或者没有初始值, 在变量名后面加个冒号, 显式指定类型

let implicitInteger = 70
let implicitDouble = 70.0
let explicitDouble: Double = 70

值不会隐式地转化为其他类型, 你需要操作不同类型的值的时候需要显示转换

let label = "The width is "
let width = 94
let widthLabel = label + String(width)

对于字符串来说有更加简便的转化方式, 字符串内使用\加括号包裹值

let apples = 3
let oranges = 5
let appleSummary = "I have \(apples) apples."
let fruitSummary = "I have \(apples + oranges) pieces of fruit."

使用三个引号"""包裹住string, string可以占多行, 每个引用行的开头缩进被移除, 只要匹配右引号的锁进

let quotation = """
I said "I have \(apples) apples."
And then I said "I have \(apples + oranges) pieces of fruit."
"""

创建字典和数组使用方阔号, 通过[index]或者[key]去访问某一个值

var shoppingList = ["catfish", "water", "tulips"]
shoppingList[1] = "bottle of water"

var occupations = [
    "Malcolm": "Captain",
    "Kaylee": "Mechanic",
]
occupations["Jayne"] = "Public Relations"

当你向数组里添加元素的时候数组自动增长

shoppingList.append("blue paint")
print(shoppingList)

使用初始化器语法创建一个空的字典或者数组

let emptyArray: [String] = []
let emptyDictionary: [String: Float] = [:]

如果类型信息能够被推断出来, 你可以用以下的方式去写一个空的数组、空的字典,

shoppingList = []
occupations = [:]

控制流

使用ifswitch去构建条件语句, 使用for-inwhilerepeat-while去构建循环。使用圆括号去包裹条件或者循环变量是可选的, 主体内容的大阔号是必须的

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print(teamScore)
// Prints "11"

在if语句当中, 条件值必须是布尔值表达式, 这意味着if score {...}这样的代码是错误的, 它不会将其和0值进行比较。

你可以使用if和let一起处理可能缺失的值, 这个可能缺失的值是一个可选项, 一个可选项的值可以是一个值, 也可以是nil表示这个值是缺失的。在变量的类型后面加上问号表示这个变量是一个可选项。

var optionalString: String? = "Hello"
print(optionalString == nil)
// Prints "false"

var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
if let name = optionalName {
    greeting = "Hello, \(name)"
}

如果可选项为nil, 上面的条件值是false, 大括号里面的代码不会执行。如果可选项不为nil, 则会被解包并赋值给let常量, 在大括号内, 这个let常量是有效的。

另外一种处理可选项的方式是使用??提供一个默认值。如果可选项的值是缺失的, 默认的值会代替被使用

let nickname: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickname ?? fullName)"

switch支持任何类型的数据和广泛种类的比较操作。不再限制成integer, 而是检测值是否相等

let vegetable = "red pepper"
switch vegetable {
case "celery":
    print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
    print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):
    print("Is it a spicy \(x)?")
default:
    print("Everything tastes good in soup.")
}
// Prints "Is it a spicy red pepper?"

注意上面let常量是如何在模式中被使用的, 用来将一个匹配模式的值赋值给常量。

执行完switch内某一个匹配的switch之后, 程序会从switch语句中退出, 不会继续执行下一个case, 你不再需要显示地去在每一个case的末尾break;

你可以使用for-in去遍历字典里面的每个item, 通过提供一对名字去使用每一个键值对。字典是一个无序的集合, 所以键值对按照一个任意的顺序被遍历。

let interestingNumbers = [
    "Prime": [2, 3, 5, 7, 11, 13],
    "Fibonacci": [1, 1, 2, 3, 5, 8],
    "Square": [1, 4, 9, 16, 25],
]
var largest = 0
for (_, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)
// Prints "25"

使用while去执行代码直到条件发生改变, 也可以将while放到后面, 保证循环体至少执行一次

var n = 2
while n < 100 {
    n *= 2
}
print(n)
// Prints "128"

var m = 2
repeat {
    m *= 2
} while m < 100
print(m)
// Prints "128"

有index的循环, 配合..<限制范围

var total = 0
for i in 0..<4 {
    total += i
}
print(total)
// Prints "6"

..<表示不包含右边的值, ...表示包含右边的值

函数和闭包

使用func声明一个函数, 使用函数的时候, 方法名 + 括号包裹的参数列表。在申明一个函数的时候, 使用->分割参数列表和返回类型

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet(person: "Bob", day: "Tuesday")

默认的函数使用参数名作为参数外部标签, 你可以在参数名前面设置自定义的参数标签, 也可以使用_表示不需要外部标签

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
greet("John", on: "Wednesday")

使用元组去创建一个组合值, 比如, 从一个函数返回多个值。元组的值可以通过名字或者序号获取。

func calculateStatistics(scores: [Int]) -> (min: Int, max: Int, sum: Int) {
    var min = scores[0]
    var max = scores[0]
    var sum = 0

    for score in scores {
        if score > max {
            max = score
        } else if score < min {
            min = score
        }
        sum += score
    }

    return (min, max, sum)
}
let statistics = calculateStatistics(scores: [5, 3, 100, 3, 9])
print(statistics.sum)
// Prints "120"
print(statistics.2)
// Prints "120"

函数可以被嵌套, 嵌套的函数可以访问在函数外部的变量, 你可以使用嵌套函数去组织较长的或者复杂函数的代码

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
returnFifteen()

函数是first-class类型, 一个函数可以作为一个函数的返回值

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
increment(7)

一个函数可以是另一个函数的参数

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [20, 19, 7, 12]
hasAnyMatches(list: numbers, condition: lessThanTen)

函数是特殊类型的闭包, 闭包是一段可以在之后被调用的代码, 闭包内的代码可以访问到闭包创建环境下的变量和函数, 即便闭包在另外一个完全不同的环境中被执行,比如上边的嵌套函数。你可以通过用花括号包裹去写一个匿名闭包, 使用in去分割参数和返回类型 以及 闭包主体

numbers.map({ (number: Int) -> Int in
    let result = 3 * number
    return result
})

你有好几种方式去简化闭包。当一个闭包的类型已经被确认, 比如delegate的回调, 你可以省略参数的类型, 或者返回值的类型, 或者都省略。单个语句的闭包会隐式地返回这个仅有的语句的值

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)
// Prints "[60, 57, 21, 36]"

你可以通过数字代替参数名去访问参数, 这种方式对于小的闭包来说是非常有用的。一个闭包如果是函数的最后一个参数, 可以直接跟在参数列表圆括号后面。如果是唯一的参数, 那么圆括号可以省略

let sortedNumbers = numbers.sorted { $0 > $1 }
print(sortedNumbers)
// Prints "[20, 19, 12, 7]"

对象和类

使用class后面跟着class的名字去创造一个类。类当中属性申明写法和常量/变量申明一致, 除了属性的声明位于类的语境当中。同样地, 方法和函数申明也是同样的写法

class Shape {
    var numberOfSides = 0
    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

在类名后面加一对圆括号可以创建一个类的实例, 使用点语法去访问实例的属性和方法。

var shape = Shape()
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

类创建一个实例的时候会使用初始化方法init进行设置

class NamedShape {
    var numberOfSides: Int = 0
    var name: String

    init(name: String) {
        self.name = name
    }

    func simpleDescription() -> String {
        return "A shape with \(numberOfSides) sides."
    }
}

注意上边在init方法里面通过self. 区分了属性和初始化的参数name。每一个属性都需要初始值, 可以在申明属性的时候给到, 也可以在初始化器里面给到。

使用deinit去创建一个去初始化器, 如果你需要在对象deallocated之前做一些清理。

子类需要在自己的class名字后跟上父类的名字, 使用冒号:分隔。没有必要让一个类显式申明为某一标准根类的子类, 你可以按照自己的需要添加或者省略父类声明。

子类的方法如果需要覆盖父类的实现, 需要使用override标识。没有使用override标识意外覆盖了父类的方法会被编译器检测为一个错误, 编译器也会检测到使用了override标识但是父类当中没有对应的方法。

class Square: NamedShape {
    var sideLength: Double

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 4
    }

    func area() -> Double {
        return sideLength * sideLength
    }

    override func simpleDescription() -> String {
        return "A square with sides of length \(sideLength)."
    }
}
let test = Square(sideLength: 5.2, name: "my test square")
test.area()
test.simpleDescription()

除了简单的存储性属性, 属性可以有getter和setter方法

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0

    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSides = 3
    }

    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0
        }
    }

    override func simpleDescription() -> String {
        return "An equilateral triangle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
// Prints "9.3"
triangle.perimeter = 9.9
print(triangle.sideLength)
// Prints "3.3000000000000003"

在setter当中, newValue作为隐式的传入的参数名, 你可以使用()跟在set后面提供一个显式的名称

在上边等边三角形的初始化当中有3个不同的步骤:

  • 设置子类声明的属性值

  • 调用父类的初始化方法

  • 改变父类当中的定义的属性的值, 其他额外的使用getter、setter方法的设置工作都可以在这一步进行

    如果你不需要计算属性, 但是想要在设置一个新值之前或者之后运行一段代码, 可以使用willSetdidSet。被提供的代码将在每次属性发送变化的时候被运行, 除了初始化以外。以下代码保证了正方形和三角形的边长一直保持一致

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
        willSet {
            square.sideLength = newValue.sideLength
        }
    }
    var square: Square {
        willSet {
            triangle.sideLength = newValue.sideLength
        }
    }
    init(size: Double, name: String) {
        square = Square(sideLength: size, name: name)
        triangle = EquilateralTriangle(sideLength: size, name: name)
    }
}
var triangleAndSquare = TriangleAndSquare(size: 10, name: "another test shape")
print(triangleAndSquare.square.sideLength)
// Prints "10.0"
print(triangleAndSquare.triangle.sideLength)
// Prints "10.0"
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)
// Prints "50.0"

当使用可选项的时候, 可以在可选项的方法、属性、下标之前写上?。如果?前面值是nil, 任何?后面的都会被忽略, 而且整个表达式的值是nil。不写上?的话, 可选项会被解包, 之后的一切?作用于被解包的值。两种方式来说, 整个表达式都是可选项.

let optionalSquare: Square? = Square(sideLength: 2.5, name: "optional square")
let sideLength = optionalSquare?.sideLength

枚举和结构体

使用enum创造枚举, 像类和其他命名类型一样, 枚举可以有自己相关的方法

enum Rank: Int {
    case ace = 1
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king

    func simpleDescription() -> String {
        switch self {
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
let ace = Rank.ace
let aceRawValue = ace.rawValue

默认地, swift枚举初始值从0开始依次增大, 但是你可以通过显式地指定值去改变这一点。上面的类型当中使用1作为Ace的初始值, 剩下的初始值依次增大。你也可以使用字符串或者浮点数作为枚举的初始值。使用rawValue属性去访问一个枚举项的初始值。

使用init?(rawValue:)初始化器去用一个初始值创造一个枚举实例。返回对应值的枚举项, 或者没有对应枚举项, 返回nil

if let convertedRank = Rank(rawValue: 3) {
    let threeDescription = convertedRank.simpleDescription()
}

枚举项的值就是真正的值, 不单单只是另一种表示他们初始值的方式。如果有没有意义的初始值, 也没有必要提供一个枚举项表示它。

enum Suit {
    case spades, hearts, diamonds, clubs

    func simpleDescription() -> String {
        switch self {
        case .spades:
            return "spades"
        case .hearts:
            return "hearts"
        case .diamonds:
            return "diamonds"
        case .clubs:
            return "clubs"
        }
    }
}
let hearts = Suit.hearts
let heartsDescription = hearts.simpleDescription()

注意上面hearts的枚举项的两种推导方式, 将hearts枚举项给到常量的时候, 使用的是Suit.hearts, 因为常量没有显式指定类型。而在枚举内部的Description当中, 直接使用了.hearts, 因为self已经被认定为是Suit的枚举。在已经知道类型的情况下, 你可以使用缩写。

如果一个枚举设定了初始值, 这些值被确定为声明的一部分, 这就意味着每一种枚举项的实例都是相同的初始值。另外一种方式就是让枚举项和值进行关联, 只有在你创建实例的时候才会确定, 同一枚举项的值可以是不同的。你可以认为关联的值类似于枚举项实例的属性。比如, 从服务器请求日出、日落时间, 服务器要么同时返回这两个时间, 要么返回出错信息

enum ServerResponse {
    case result(String, String)
    case failure(String)
}

let success = ServerResponse.result("6:00 am", "8:09 pm")
let failure = ServerResponse.failure("Out of cheese.")

switch success {
case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
    print("Failure...  \(message)")
}
// Prints "Sunrise is at 6:00 am and sunset is at 8:09 pm."

使用struct去创建一个结构体, 结构体支持许多和类相同的行为, 包括类和初始化器。结构体和类之间最大的区别就是, 对于赋值来说, 结构体是做值拷贝, 但是类是通过引用计数, 传递指针。

struct Card {
    var rank: Rank
    var suit: Suit
    func simpleDescription() -> String {
        return "The \(rank.simpleDescription()) of \(suit.simpleDescription())"
    }
}
let threeOfSpades = Card(rank: .three, suit: .spades)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()

协议和扩展

使用protocol去申明一个协议

protocol ExampleProtocol {
    var simpleDescription: String { get }
    mutating func adjust()
}

类、枚举和结构体都支持协议

class SimpleClass: ExampleProtocol {
    var simpleDescription: String = "A very simple class."
    var anotherProperty: Int = 69105
    func adjust() {
        simpleDescription += "  Now 100% adjusted."
    }
}
var a = SimpleClass()
a.adjust()
let aDescription = a.simpleDescription

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure"
    mutating func adjust() {
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription

注意到上面结构体当中使用了mutating, 去标识一个将会修改结构体的方法。而类不需要这个标识, 因为类当中的方法总是能够修改类。

使用extension去对已经存在的类型添加功能, 比如新的方法和计算型属性。你可以使用扩展对申明在任何其他位置的类型添加协议的实现, 甚至是你从库或者framework当中导入的类型。

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
}
print(7.simpleDescription)
// Prints "The number 7"

像其他命名类型一样, 你可以使用协议名表示一个类型, 比如创建一个类型不同但是遵循同一协议的集合。但是当你操作这些类型为协议类型的对象的时候, 协议以外的方法是不可以使用的

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)
// Prints "A very simple class.  Now 100% adjusted."
// print(protocolValue.anotherProperty)  // Uncomment to see the error

虽然上面的protocolValue运行时类型是SimpleClass, 但是编译器会将其作为ExampleProtocoll来处理, 这意味着你不能访问除了在协议里面规定的属性和方法

错误处理

你可以使用遵循了Error协议的任何类型来代表errors

enum PrinterError: Error {
    case outOfPaper
    case noToner
    case onFire
}

使用throw去抛出错误, 使用throws去标记一个可能抛出错误的函数。如果你在一个函数内抛出了错误, 函数会立刻返回, 调用这个函数的代码需要处理这个错误

func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

存在几种处理错误的方式, 一种是使用do-catch。在do的代码块当中, 你需要在可能抛出错误的代码前面标识try。在catch代码块当中, 错误会使用error作为错误名

do {
    let printerResponse = try send(job: 1040, toPrinter: "Bi Sheng")
    print(printerResponse)
} catch {
    print(error)
}
// Prints "Job sent"

你可以提供多个catch代码块去处理特定的错误, 一个catch紧接上一个catch, 类似于switch

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenberg")
    print(printerResponse)
} catch PrinterError.onFire {
    print("I'll just put this over here, with the rest of the fire.")
} catch let printerError as PrinterError {
    print("Printer error: \(printerError).")
} catch {
    print(error)
}
// Prints "Job sent"

另外一种处理错误的方式是使用try?去将结果转化为可选项。如果抛出错误, 错误会被丢弃, 返回值是nil, 否则返回值是一个包含了函数返回值的可选项。

let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

使用defer去写一段在这个函数所有代码之后执行的代码块, 在函数返回之前。 无论是否抛出异常都会执行。你可以使用defer去写设置和清理的代码, 成对出现但是在不同时间执行。

var fridgeIsOpen = false
let fridgeContent = ["milk", "eggs", "leftovers"]

func fridgeContains(_ food: String) -> Bool {
    fridgeIsOpen = true
    defer {
        fridgeIsOpen = false
    }

    let result = fridgeContent.contains(food)
    return result
}
fridgeContains("banana")
print(fridgeIsOpen)
// Prints "false"

泛型

使用尖括号去构造一个范型函数或者类型

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result: [Item] = []
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
makeArray(repeating: "knock", numberOfTimes: 4)

函数、方法、类、枚举和结构体都支持范型

// Reimplement the Swift standard library's optional type
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)

使用where在函数体右边去指定一个需求列表, 比如要求类型服从某个协议、两个参数是同一种类型、有一个特定的父类

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
    where T.Element: Equatable, T.Element == U.Element
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
anyCommonElements([1, 2, 3], [3])

<T: Equatable>是等价于<T>... where T: Equatable