A Swift Tour (Swift 5.2)

https://docs.swift.org/swift-book/GuidedTour/GuidedTour.html

01-Hello world

不需要为了输入输出或者字符串处理导入一个单独的库。
全局作用域中的代码会被自动当做程序的入口点,所以你也不需要 main 函数。
不需要在每个语句结尾写上分号。

// 这行代码就是一个完整的程序
print("Hello world")

02-简单值

使用 let 来声明常量
使用 var 来声明变量

var myVariable = 42
myVariable = 50
let myConstant = 42

常量或者变量的类型必须和你赋给它们的值一样。
然而,声明时类型是可选的,声明的同时赋值的话,编译器会自动推断类型。
在上面的例子中,编译器推断出 myVariable 是一个整数(integer)因为它的初始值是整数。
如果初始值没有提供足够的信息(或者没有初始值),那你需要在变量后面声明类型,用冒号分割。

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

练习
创建一个常量,显式指定类型为 Float 并指定初始值为4。

值永远不会被隐式转换为其他类型。如果你需要把一个值转换成其他类型,请显式转换。

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

练习
删除最后一行中的 String ,错误提示是什么?

有一种更简单的把值转换成字符串的方法:把值写到括号中,并且在括号之前写一个反斜杠。例如:

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

练习
使用 () 来把一个浮点计算转换成字符串,并加上某人的名字,和他打个招呼。

"""对于占用多行的字符串,请使用三个双引号()。只要每个引用行的缩进都与右引号的缩进匹配,就将其删除。例如:

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

使用方括号 [] 来创建数组和字典,并使用下标或者键(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 emptyArary = [String]()
let emptyDictionary = [String: Float]()

如果类型信息可以被推断出来,你可以用 [] 和 [:] 来创建空数组和空字典——就像你声明变量或者给函数传参数的时候一样。

shoppingList = [] // 去逛街并买点东西
occupations = [:]

03-控制流

使用 if 和 switch 来进行条件操作
使用 for-in 、 while 和 repeat-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)

在 if 语句中,条件必须是一个布尔表达式——这意味着像 if score { ... } 这样的代码将报错,而不会隐形地与 0 做对比。

你可以一起使用 if 和 let 来处理值缺失的情况。有些变量的值是可选的。一个可选的值可能是一个具体的值或者是 nil ,表示值缺失。在类型后面加一个问号来标记这个变量的值是可选的。

var optionalString: String? = "Hello"
optionalString == nil

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

练习
把 optionalName 改成 nil ,greeting会是什么?添加一个 else 语句,当 optionalName 是 nil 时给greeting赋一个不同的值。

如果变量的可选值是 nil ,条件会判断为 false ,大括号中的代码会被跳过。如果不是 nil ,会将值赋给 let 后面的常量,这样代码块中就可以使用这个值了。

处理可选值的另一种方法是使用??运算符提供默认值。如果缺少可选值,则使用默认值。

let nickName: String? = nil
let fulName: String = "John Appleseed"
let informalGreeting = "Hi \(nickName ?? fulName)"

switch 支持任意类型的数据以及各种比较操作——不仅仅是整数以及测试相等。

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.")
}

练习
删除 default 语句,看看会有什么错误?

运行 switch 中匹配到的子句之后,程序会退出 switch 语句,并不会继续向下运行,所以不需要在每个子句结尾写 break

你可以使用 for-in 来遍历字典,需要两个变量来表示每个键值对。

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 (kind, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print(largest)

练习
添加另一个变量来记录哪种类型的数字是最大的。

使用 while 来重复运行一段代码直到不满足条件。循环条件可以在开头也可以在结尾

var n = 2
while n < 100 {
    n *= 2
}
print(n)

var m = 2
repeat {
    m *= 2
} while m < 100
print(m)

你可以在循环中使用 ..< 来表示范围

var total = 0
for i in 0..<4 {
    total += i
}
print(total)
  • ..<省略了其上限值
  • ...包括值的范围。

04-函数和闭包

使用 func 来声明一个函数,使用名字和参数来调用函数。使用 -> 来指定函数返回值。

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

练习
删除 day 参数,添加一个参数来表示今天吃了什么午饭。

默认情况下,函数使用其参数名称作为其参数的标签。在参数名称前写一个自定义参数标签,或写 _ 为不使用任何参数标签。

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)
print(statistics.2)

使用一个元组来返回多个值。

func getGasPrices() ->(Double, Double, Double) {
    return (3.59, 3.68, 3.79)
}
print(getGasPrices())

函数可以带有可变个数的参数,这些参数在函数内表现为数组的形式:

func sumOf(numbers: Int...) -> Int {
    var sum = 0
    for number in numbers {
        sum += number
    }
    return sum
}
sumOf()
sumOf(numbers: 42, 100, 12)

练习
写一个计算参数平均值的函数。

函数可以嵌套。被嵌套的函数可以访问外侧函数的变量,你可以使用嵌套函数来重构一个太长或者太复杂的函数。

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

函数是第一等类型,这意味着函数可以作为另一个函数的返回值。

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
})

练习
重写闭包,对所有奇数返回0。

有很多种创建闭包的方法。如果一个闭包的类型已知,比如作为一个回调函数,你可以忽略参数的类型和返回值。单个语句 闭包会把它语句的值当做结果返回。

let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)

你可以通过参数位置而不是参数名字来引用参数——这个方法在非常短的闭包中非常有用。当一个闭包作为最后一个参数传给一个函数的时候,它可以直接跟在括号后面。

let sortedNumbers = numbers.sorted{ $0 > $1}
print(sortedNumbers)

05-对象和类

使用 class 和类名来创建一个类。类中属性的声明和常量、变量声明一样,唯一的区别就是它们的上下文是类。同样,方法和函数声明也一样。

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

练习
使用 let 添加一个常量属性,再添加另一个接收一个参数的方法。

通过在类名称后加上括号来创建类的实例。使用点语法来访问实例的属性和方法。

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

Shape该类的版本缺少重要的内容:创建实例时用于设置类的初始化程序。使用init创建一个。

class NamedShape {
    var numberOfSlides: Int = 0
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func simpleDescription() -> String {
        return "A shape with \(numberOfSlides) sides."
    }
}

注意 self 被用来区别实例变量。当你创建实例的时候,像传入函数参数一样给类传入构造器的参数。每个属性都需要赋值——无论是通过声明(就像 numberOfSides )还是通过构造器(就像 name )。
如果你需要在删除对象之前进行一些清理工作,使用 deinit 创建一个析构函数。 子类的定义方法是在它们的类名后面加上父类的名字,用冒号分割。创建类的时候并不需要一个标准的根类,所以你可以忽略父类。
子类如果要重写父类的方法的话,需要用 override 标记——如果没有添加 override 就重写父类方法的话编译器会报错。编译器同样会检测 override 标记的方法是否确实在父类中。

class Square: NamedShape {
    var sideLength: Double
    
    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSlides = 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()

练习
创建 NamedShape 的另一个子类 Circle ,构造器接收两个参数,一个是半径一个是名称,实现 area 和 describe 方法。

属性可以有 getter 和 setter 。

class EquilateralTriangle: NamedShape {
    var sideLength: Double = 0.0
    
    init(sideLength: Double, name: String) {
        self.sideLength = sideLength
        super.init(name: name)
        numberOfSlides = 3
    }
    
    var perimeter: Double {
        get {
            return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0
        }
    }
    
    override func simpleDescription() -> String {
        return "An equilateral triagle with sides of length \(sideLength)."
    }
}
var triangle = EquilateralTriangle(sideLength: 3.1, name: "a triangle")
print(triangle.perimeter)
triangle.perimeter = 9.9
print(triangle.sideLength)

在 perimeter 的 setter 中,新值的名字是 newValue 。你可以在 set 之后显式的设置一个名字。 注意 EquilateralTriangle 类的构造器执行了三步:

    1. 设置子类声明的属性值
    1. 调用父类的构造器
    1. 改变父类定义的属性值。其他的工作比如调用方法、getters和setters也可以在这个阶段完成。

如果你不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用 willSet 和 didSet 。 比如,下面的类确保三角形的边长总是和正方形的边长相同。

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)
print(triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 50, name: "larger square")
print(triangleAndSquare.triangle.sideLength)

类中的方法和一般的函数有一个重要的区别,函数的参数名只在函数内部使用,但是方法的参数名需要在调用的时候显式说 明(除了第一个参数)。默认情况下,方法的参数名和它在方法内部的名字一样,不过你也可以定义第二个名字,这个名字 被用在方法内部。

class Counter {
    var count: Int = 0
    func incrementBy(amount: Int, numberOfTimes times: Int) {
        count += amount * times
    }
}
var counter = Counter()
counter.incrementBy(amount: 2, numberOfTimes: 7)

处理变量的可选值时,你可以在操作(比如方法、属性和子脚本)之前加 ? 。如果 ? 之前的值是 nil , ? 后面的东西都会被忽 略,并且整个表达式返回 nil 。否则, ? 之后的东西都会被运行。在这两种情况下,整个表达式的值也是一个可选值。

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

06-枚举和结构体

使用 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

练习
写一个函数,通过比较它们的原始值来比较两个 Rank 值。

默认情况下,Swift会分配从零开始的原始值,并每次递增一,但是您可以通过显式指定值来更改此行为。在上面的示例中,Ace显式指定了的原始值1,其余原始值按顺序分配。您也可以使用字符串或浮点数作为枚举的原始类型。使用该rawValue属性访问枚举案例的原始值。

使用init?(rawValue:)初始化程序从原始值创建枚举实例。它返回任一枚举的情况下的匹配的原始值或nil如果不存在匹配Rank。

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

枚举的成员值是实际值,并不是原始值的另一种表达方法。实际上,如果原始值没有意义,你不需要设置。

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()

实验
给 Suit 添加一个 color() 方法,对 spades 和 clubs 返回“black”,对 hearts 和 diamonds 返回“red”。

请注意hearts上面引用枚举大小写的两种方式:为hearts常量分配值时,枚举大小写Suit.hearts以其全名引用,因为常量没有指定显式类型。在交换机内部,枚举用缩写形式表示,.hearts因为self已知的值是合适的。只要知道值的类型,就可以使用缩写形式。

一个枚举成员的实例可以有实例值。相同枚举成员的实例可以有不同的值。创建实例的时候传入值即可。实例值和原始值是 不同的:枚举成员的原始值对于所有实例都是相同的,而且你是在定义枚举的时候设置原始值。

例如,考虑从服务器获取日出和日落的时间。服务器会返回正常结果或者错误信息。

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)")
}

实验
给 ServerResponse 和 switch 添加第三种情况。

注意如何从 ServerResponse 中提取日升和日落时间。

使用 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()

实验
给 Card 添加一个方法,创建一副完整的扑克牌并把每张牌的 rank 和 suit 对应起来。

07-协议和扩展

使用 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% addjusted."
    }
}
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

练习
写一个实现这个协议的枚举。

注意声明 SimpleStructure 时候 mutating 关键字用来标记一个会修改结构体的方法。 SimpleClass 的声明不需要标记任何方法,因为类中的方法经常会修改类。

使用 extension 来为现有的类型添加功能,比如新的方法和参数。你可以使用扩展来改造定义在别处,甚至是从外部库或者框 架引入的一个类型,使得这个类型遵循某个协议。

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

练习
给 Double 类型写一个扩展,添加 absoluteValue 功能。

你可以像使用其他命名类型一样使用协议名——例如,创建一个有不同类型但是都实现一个协议的对象集合。当你处理类型 是协议的值时,协议外定义的方法不可用。

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 ,编译器会把它的类型当做 ExampleProtocol 。这表示你不能调用类在它实现的协议之外实现的方法或者属性。

08-错误处理

您可以使用采用该Error协议的任何类型来表示错误。

enum PrinterError: Error {
    case outOfPage
    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)
}

实验
将打印机名称更改为,以便该函数引发错误。"Never Has Toner"send(job:toPrinter:)

您可以提供多个catch处理特定错误的块。您catch就像case在切换之后一样,编写了一个模式。

do {
    let printerResponse = try send(job: 1440, toPrinter: "Gutenbergg")
    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)
}

实验
添加代码以在do块内引发错误。您需要引发哪种错误,以便由第一个catch块处理该错误?那第二块和第三块呢?

处理错误的另一种方法是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)

09-泛型

在尖括号里写一个名字来创建一个泛型函数或者类型。

func makeArray(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result = [Item]()
    for _ in 0.. {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue = .none
possibleInteger = .some(100)

where在正文之前使用权利来指定需求列表,例如,要求类型实现协议,要求两个类型相同或要求一个类具有特定的超类。

func anyCommonElements(_ 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])

实验
修改该anyCommonElements(::)函数以使该函数返回任何两个序列共同具有的元素的数组。

简单起见,你可以忽略 where ,只在冒号后面写协议或者类名。 是等价的。

你可能感兴趣的:(A Swift Tour (Swift 5.2))