Swift Learning (1) - First See (Fully Ver.)

Swift Learning (1) - First See (Fully Ver.)

August 4, 2021·Jensen
Jensen

image

Swift简介

Swift是美国苹果公司推出的编程语言,专门针对苹果桌面操作系统macOS和苹果移动操作系统iOS、iPadOS以及watchOS和tvOS的应用开发。Swift 在各个方面优于 Objective-C,也不会有那么多复杂的符号和表达式。同时,Swift 更加快速、便利、高效、安全。除此之外,新的 Swift 语言依旧会与 Object-C 相兼容。(更多关于Swift的信息可以访问苹果公司官方网址

一般来说,一行Swift代码就是一个完整的程序。

print("Hello, World!")
---
output: Hello, World!

简单值

Swift中常量用let来修饰,常量值只能赋值一次,可以在多个地方使用。

let constValue = 20
let constInteger: Int
print(constValue)
---
output: 20

Swift中变量用var来修饰,与JavaScript的语法很像,变量可以多次赋值。

var variableValue = 20
variableValue = 30
print(variableValue)
---
output: 30

常量和变量的类型必须和赋给它的值一样,然而并不用明确地声明类型。当通过一个值来声明常量和变量时,编译器会自动推断其类型。如果初始值没有提供足够的信息,或者没有初始值时,需要在变/常量后面声明类型,用冒号分割。

let implicitInteger = 70
print("Implicit Integer:", implicitInteger)
let implicitDouble = 71.0
print("Implicit Double:", implicitDouble)
let explicitDouble: Double = 72
print("Explicit Double:", explicitDouble)
---
output: Implicit Integer: 70
Implicit Double: 71.0
Explicit Double: 72.0

小练习

创建一个常量,显示指定类型为Float,并指定初始值为4

参考答案:

let explicitFloat: Float = 100
print("Practice Result:", explicitFloat)
---
output: Practice Result: 100.0

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

let label = "The width is "
let width = 94
let widthLabel = label + String(width)
print("Explicit Conversion Result:", widthLabel)
---
output: Explicit Conversion Result: The width is 94

小练习:

删除上面倒数第二行中的String,错误提示是什么?

参考答案:

Binary operator '+' cannot be applied to operands of type 'String' and 'int'


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

let apples = 3
let oranges = 5
let fruitSummary = "I have \(apples) apples, and \(oranges) oranges. So I have \(apples + oranges) pieces of fruits totaly."
print(fruitSummary)
---
output: I have 3 apples, and 5 oranges. So I have 8 pieces of fruits totaly.

小练习:

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

参考答案:

let constScore = 301.92
let nameSocre = "Jensen \(constScore)"
print("Hello, ", nameSocre)
---
output: Hello,  Jensen 301.92

使用三个双引号"""来包含多行字符串内容,每行行首的缩进会被去除,直到和结尾引号的缩进相匹配。

let quotation = """
I said "I have \(apples) apples, and \(oranges) oranges"
And then I said "I have \(apples + oranges) pieces of fruits totaly."
"""
print(quotation)
---
output: I said "I have 3 apples, and 5 oranges"
And then I said "I have 8 pieces of fruits totaly."

使用方括号[]来创建数组和字典,并使用下标或者键(Key)来访问元素,最后一个元素后面允许有个逗号。

var shoppingList = ["catfish", "water", "tulips", "blue paint", ]
print(shoppingList[0])
shoppingList[2] = "Jensen"
print(shoppingList)

var occupations = [
    "Country": "China",
    "City": "Hefei"
]
occupations["University"] = "HFUT"
print(occupations)
---
output: catfish
["catfish", "water", "Jensen", "blue paint"]
["University": "HFUT", "Country": "China", "City": "Hefei"]

数组在添加元素时会自动变大。

shoppingList.append("apples")
print("数组添加元素后的长度:", shoppingList.count)
---
output: 数组添加元素后的长度: 5

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

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

如果变量的类型信息可以被推断出来,可以用[][:]来创建空数组和空字典。

shoppingList = []
occupations = [:]

控制流

使用ifswitch来进行条件操作,使用for-inwhilerepeat-while来进行循环,包裹条件和循环变量的括号可以省略。在if语句中,条件必须是一个布尔表达式,这意味着像if score { ... }这样的代码将报错,而不会隐式地与0做对比。

let individualScores = [75, 43, 103, 87, 12]
var teamScore = 0
for score in individualScores {
    if score > 50 {
        teamScore += 3
    } else {
        teamScore += 1
    }
}
print("The value of teamScore: ", teamScore)
---
output: The value of teamScore:  11

可以一起使用iflet一起来处理值缺失的情况。这些值可由可选值来代表。一个可选的值是一个具体的值或者是nil以表示值缺失在类型后面加一个问号?来标记这个变量的值是可选的。

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

var optionalName: String? = "Jensen Jon"
var greeting = "Hello"
if let name = optionalName {
    greeting = "Hello, \(name)"
    print(greeting)
}
---
output: false
true
Hello, Jensen Jon

小练习:

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

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


另一种处理可选值的方法是通过使用??操作符来提供一个默认值。如果可选值缺失的话,可以使用默认值来代替。

let nickName: String? = nil
let fullName: String = "Jensen Jon"
let informalGreeting = "Hi \(nickName ?? fullName)!"
print("informalGreeting: ", informalGreeting)
---
output: informalGreeting:  Hi Jensen Jon!

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

let vegetable = "red pepper"  // Red Pepper 红胡椒
switch vegetable {
case "celery":
    print("Add some raisins and make ants on a log.")  // Swift中运行switch中匹配的case语句后,程序会自动退出switch语句,不需要break语句
case "cucumber", "watercress":
    print("That would make a good tea sandwich.")
case let x where x.hasSuffix("pepper"):  // Warning: let在上述例子中将匹配等时的值赋给常量x
    print("Is it a spicy \(x)?")
default:
    print("Everything testes good in soup.")
}
---
output: Is it a spicy red pepper?

小练习:

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

参考答案: Switch must be exhaustive


可以使用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 (_, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
        }
    }
}
print("interestingNumbers字典中的最大值是:", largest)
---
output: interestingNumbers字典中的最大值是: 25

小练习:

将_替换成变量名,以确定哪种类型的值是最大的

参考答案:

var largCls = ""; largest = 0
for (varClass, numbers) in interestingNumbers {
    for number in numbers {
        if number > largest {
            largest = number
            largCls = varClass
        }
    }
}
print("字典中最大值的类型是:", largCls)
---
output: 字典中最大值的类型是: Square

使用while来重复运行一段代码直到条件改变。循环条件也可以在结尾,保证至少能循环一次。

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

var m = 2
repeat {
    m *= 2
} while m < 100
print("m: ", m)
---
output: n:  128
m:  128

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

var total = 0
for i in 0..<5 {
    print(i)
    total += i
}
print("Total(..<): ", total)
---
output: 0
1
2
3
4
Total(..<):  10

使用..<创建的范围不包含上届,如果想包含的话需要使用...

total = 0
for i in 0...5 {
    print(i)
    total += i
}
print("Total(...): ", total)
---
output: 0
1
2
3
4
5
Total(...):  15

函数和闭包

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

func greet(person: String, day: String) -> String {
    return "Hello \(person), today is \(day)."
}
print(greet(person: "Jensen", day: "Thursday"))
---
output: Hello Jensen, today is Thursday.

小练习:

删除day参数,在这个欢迎语中添加一个参数来表示今天的特价菜

参考答案:

func greet(person: String, delicacy: String) -> String {
    return "Hello \(person), our special delicacy is \(delicacy) today!"
}
print(greet(person: "Jensen", delicacy: "beef"))
---
output: Hello Jensen, our special delicacy is beef today!

默认情况下,函数使用它们的参数名称作为它们参数的标签,在参数名称前可以自定义参数标签,或者使用_表示不使用参数标签。

func greet(_ person: String, on day: String) -> String {
    return "Hello \(person), today is \(day)."
}
print(greet("Jensen", on: "Thursday"))
---
output: Hello Jensen, today is Thursday.

使用元组来生成复合值,比如让一个函数返回多个值,该元组的元素可以用名称或数字来获取。

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, -9, 0])
print("Max value is ", statistics.max, "\nMin value is ", statistics.min, "\nSum value is ", statistics.sum)
print(statistics.2)
---
output: Max value is  100 
Min value is  -9 
Sum value is  99
99

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

func returnFifteen() -> Int {
    var y = 10
    func add() {
        y += 5
    }
    add()
    return y
}
print("经过嵌套函数处理后的y值是:", returnFifteen())
---
output: 经过嵌套函数处理后的y值是: 15

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

func makeIncrementer() -> ((Int) -> Int) {
    func addOne(number: Int) -> Int {
        return 1 + number
    }
    return addOne
}
var increment = makeIncrementer()
print("Increment is ", increment(7))
---
output: Increment is  8

函数也可以作为一个参数传入另一个函数。

func hasAnyMatches(list: [Int], condition: (Int) -> Bool) -> Bool {
    for item in list {
        if condition(item) {  // if条件语句的条件必须是一个布尔表达式
            return true
        }
    }
    return false
}
func lessThanTen(number: Int) -> Bool {
    return number < 10
}
var numbers = [10, 20, 9, 2, 11]
print(hasAnyMatches(list: numbers, condition:  lessThanTen))
---
output: true

函数实际上是一种特殊的闭包:它是一段能之后被调取的代码。闭包中的代码能访问闭包作用域中的变量和函数,即使闭包是在一个不同的作用域被执行的,正如上面的嵌套函数。可以使用{}来创建一个匿名闭包,使用in将参数和返回值类型的声明与闭包函数体进行分离。

var result = numbers.map({
    (number: Int) -> Int in
    let result = 3 * number
    return result
})
print("闭包值:", result)
---
output: 闭包值: [30, 60, 27, 6, 33]

小练习:

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

参考答案:

result = numbers.map({
    (number: Int) -> Int in
    if number % 2 != 0 {
        return 0
    }
    return number
})
print("闭包值:", result)
---
output: 闭包值: [10, 20, 0, 2, 0]

如果一个闭包的类型已知,比如作为一个代理的回调,你可以忽略参数,返回值,甚至两个都忽略。单个语句闭包会把它语句的值作为结果返回。

let mappedNumbers = numbers.map({number in 3 * number})
print("简洁闭包:", mappedNumbers)
---
output: 简洁闭包: [30, 60, 27, 6, 33]

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

let sortedNumbers = numbers.sorted {$0 > $1}
print("简洁闭包(无圆括号):", sortedNumbers)
---
output: 简洁闭包(无圆括号): [20, 11, 10, 9, 2]

类和对象

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

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

小练习:

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

参考答案:

class Shapes {
    let constVal = 10
    var numberOfSize = 20
    func setter(_ number: Int) {
        numberOfSize = number
    }
    func shwoInfo() -> String {
        return "Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is \(constVal), the value of numberOfSize is \(numberOfSize)."
    }
}
var shapes = Shapes()
shapes.setter(100)
print(shapes.shwoInfo())
---
output: Class Shape has a const value constVal and a variable value numberOfSize, the value of constVal is 10, the value of numberOfSize is 100.

要创建一个类的实例,在类名后面加上括号。使用点语法来访问实例的属性和方法。

var shape = Shape()
shape.numberOfSides = 7
print("shape对象已创建:", shape.simpleDescription())
---
output: shape对象已创建: A shape with 7 sides.

述的Shape/Shapes类还缺少了一个非常重要的东西:一个构造函数来初始化实例。可以使用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."
    }
}

注意,类似于Python,在上述例子中self被用来区别实例变量name和构造器参数name。当需要创建实例的时候,就需要像传入函数参数一样给类的构造器传入参数。创建实例时,类的每个属性都需要赋值,无论是通过声明还是通过构造器。

如果需要在对象释放前进行一些清理工作,需要使用deinit创建一个析构函数。

子类的定义方法是在它们的类名后面加上父类的名字,并用冒号分割。创建类时,并不需要一个标准的根类(Root),所以可以根据需要添加或者忽略父类。子类如果需要重写父类的方法的话,就需要使用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). Producted by \(name)."
    }
}
let test = Square(sideLength: 5.2, name: "Jensen")
print("Square: Area", test.area())
print(test.simpleDescription(), test.numberOfSides)
---
output: Square: Area 27.040000000000003
A square with sides of length 5.2. Producted by Jensen. 4

小练习:

创建NamedShape的另一个字类Circle,构造器接收两个参数,一个是半径,另一个时名称,在子类中实现area()和simpleDescription()方法

参考答案:

class Circle: NamedShape {
    var radius: Double
    
    init(radius: Double, name: String) {
        self.radius = radius
        super.init(name: name)
        numberOfSides = 1
    }
    
    func area() -> Double {
        return radius * radius * Double.pi
    }

    override func simpleDescription() -> String {
        return "A circle has \(numberOfSides) side and with radius of \(radius), its area is \(area()). Producted by \(name)."
    }
}
let circle = Circle(radius: 5.2, name: "Jensen")
print(circle.simpleDescription())
---
output: A circle has 1 side and with radius of 5.2, its area is 84.94866535306801. Producted by Jensen.

除了简单的存储属性,还有使用gettersetter的计算属性。

class EquilateralTriangle: NamedShape {
    var sideLength: Double
    
    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: "Jensen")
print(triangle.simpleDescription())
triangle.perimeter = 9.9
print("Setter后的新值:", triangle.sideLength)
---
output: An equilateral triangle with sides of length 3.1.
Setter后的新值: 3.3000000000000003

perimetersetter中,新值的名字时newValue,可以在set之后的圆括号中显式地设置一个名字。注意,EquilateralTriangle类的构造器执行了三个步骤:

设置子类声明的属性值

调用父类的构造器

改变父类定义的属性值,其他的工作比如调用方法、getters 和 setters 也可以在这个阶段完成。

如果不需要计算属性,但是仍然需要在设置一个新值之前或者之后运行代码,使用willSetdidSet。写入的代码会在属性值发生改变时调用,但不包含init中发生值改变的情况。

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: "Jensen")
print("第一次调用 > Square.sideLength:", triangleAndSquare.square.sideLength)
print("第一次调用 > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
triangleAndSquare.square = Square(sideLength: 30, name: "Jensen")
print("第二次调用 > EquilaterTriangle.sideLength:", triangleAndSquare.triangle.sideLength)
---
output: 第一次调用 > Square.sideLength 10.0
第一次调用 > EquilaterTriangle.sideLength 10.0
第二次调用 > EquilaterTriangle.sideLength 30.0

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

var optionalSquare: Square? = Square(sideLength: 2.5, name: "Jensen")
var sideLength = optionalSquare?.sideLength
print("有可选值:", sideLength)
optionalSquare = nil
sideLength = optionalSquare?.sideLength
print("无可选值:", sideLength)
---
output: 有可选值: Optional(2.5)
无可选值: nil
---
warning: Expression implicitly coerced from 'Double?' to 'Any'

枚举和结构体

使用enum来创建一个枚举,就像类和其他所有命名类型一样,枚举可以包含方法。


enum RankTest: Double {
    case ace
    case two, three, four, five, six, seven
}
print("枚举测试:", RankTest.two.rawValue)

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
print("ace: ", aceRawValue)
print("Rank.xx.simpleDescription(): ", Rank.king.simpleDescription())
---
output: 枚举测试: 1.0
ace:  1
Rank.xx.simpleDescription():  king

小练习:

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

参考答案:

enum Rank1: Int {
    case ace = 10
    case two, three
}

enum Rank2: Int {
    case ace = 7
    case next
}

func compareRanks(rankVal1: Int, rankVal2: Int) -> String {
    if rankVal1 < rankVal2 {
        return "Rank1"
    } else if rankVal2 < rankVal1 {
        return "Rank2"
    } else {
        return "Null"
    }
}

print("具有较大的原始值的Rank是:", compareRanks(rankVal1: Rank1.ace.rawValue, rankVal2: Rank2.ace.rawValue))
---
output: 具有较大的原始值的Rank是: Rank2

默认情况下,Swift按照从0开始每次加1的方式为原始值进行赋值,原始值可以通过显式赋值来修改。当然了,也可以选择使用字符串或者浮点数作为枚举的原始值,使用rawValue属性来访问一个枚举成员的原始值。

使用init?(rawValue:)初始化构造器从原始值创近啊一个枚举实例。如果存在于原始值相应的枚举成员就返回该枚举成员,否则就返回nil

if let convertedRank = Rank(rawValue: 11) {
    let elevenDescription = convertedRank.simpleDescription()
    print(elevenDescription)
}
---
output: jack

枚举的关联值是实际值,并不是原始值的另一种表达方式。实际上,如果没有比较有意义的原始值,就不要提供原始值。

enum Suit {
    case spaeds, hearts, diamonds, clubs
    func simpleDescription() -> String {
        switch self {
        case .spaeds:
            return "speedsDescription"
        case .hearts:
            return "heartsDescription"
        case .diamonds:
            return "diamondsDescription"
        case .clubs:
            return "clubsDescription"
        }
    }
}
let hearts = Suit.hearts
print("Suit.hearts: ", hearts)
let heartsDescription = hearts.simpleDescription()
print("heartsDescription: ", heartsDescription)
---
output: Suit.hearts:  hearts
heartsDescription:  heartsDescription

小练习:

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

参考答案:

enum SuitTest {
    case spaeds, hearts, diamonds, clubs
    func simpleDescription() -> String {
        switch self {
        case .spaeds:
            return "speedsDescription"
        case .hearts:
            return "heartsDescription"
        case .diamonds:
            return "diamondsDescription"
        case .clubs:
            return "clubsDescription"
        }
    }
    
    func color() -> String {
        switch self {
        case .spaeds:
            return "black"
        case .clubs:
            return "black"
        case .hearts:
            return "red"
        case .diamonds:
            return "red"
        }
    }
}
let clubs = SuitTest.clubs
print("Suits.clubs: ", clubs)
let clubsColor = clubs.color()
print("clubsColor: ", clubsColor)
---
output: Suits.clubs:  clubs
clubsColor:  black

注意在上面的例子中使用了两种方式引用hearts枚举成员:

clubs常量赋值时,枚举成员Suits.clubs需要使用全名来引用,因为常量没有显示指定类型

switch里,枚举成员使用缩写.clubs来引用,因为self的值已经是一个Suits类型

在任何已知变量类型的情况下都可以使用缩写。

如果枚举成员的实例有原始值,那么这些值是在声明时候就已经决定了,这意味着不同枚举实例的枚举成员总会有一个相同的原始值。当然我们也可以为枚举成员设定关联值,关联值是在创建实例时决定的。这意味着同一枚举成员不同实例的关联值可以不同。

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

var response = ServerResponse.result("6:00 am", "8:09 pm")
response = ServerResponse.failure("Out of memory.")

switch response {
case let .result(sunrise, sunset):
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .failure(message):
    print("Failure... \(message)")
}
---
output: Failure... Out of memory.

小练习:

给ServerReponse和switch添加第三种情况

参考答案:

enum ServerResponses {
    case result(String, String)
    case rain(String)
    case failure(String)
}

var responses = ServerResponses.result("6:00 am", "8:09 pm")
responses = ServerResponses.failure("Out of memory.")
responses = ServerResponses.rain("Today is rain.")

switch responses {
case let .result(sunrise, sunset):  // 注意ServerResponses的值在于switch的分支匹配时,日升和日落时间是如何从该值中提取出来的
    print("Sunrise is at \(sunrise) and sunset is at \(sunset).")
case let .rain(message):
    print("Sorry! \(message)")
case let .failure(message):
    print("Failure... \(message)")
}
---
output: Sorry! Today is rain.

使用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: .spaeds)
let threeOfSpadesDescription = threeOfSpades.simpleDescription()
print("结构体传引用:", threeOfSpadesDescription)
---
output: 结构体传引用: The 3 of speedsDescription.

小练习:

写一个方法,创建一副完整的扑克牌,这些牌是所有rank和suit的组合

参考答案:

enum Ranks: CaseIterable {
    case ace
    case two, three, four, five, six, seven, eight, nine, ten
    case jack, queen, king
}
enum Suits: CaseIterable {
    case spades, clubs, hearts, diamonds
}

struct Cards {
    var rank: Ranks
    var suit: Suits
}

func allCards() {
    for suit in Suits.allCases {
        for rank in Ranks.allCases {
            Cards(rank: rank, suit: suit)
        }
    }
}

协议与拓展

使用protocol来声明一个协议。

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

类、枚举和结构体都可以遵循协议,有点类似Java中的接口(Interface)

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()
print("Adjusted Class's Description: ", a.simpleDescription)

struct SimpleStructure: ExampleProtocol {
    var simpleDescription: String = "A simple structure."
    mutating func adjust() {  // 实现协议中的mutating方法时,若是类类型,则不用写mutating关键字,对于结构体和枚举(即值类型),则必须写mutating关键字
        simpleDescription += " (adjusted)"
    }
}
var b = SimpleStructure()
b.adjust()
print("Adjusted Struct's Description: ", b.simpleDescription)
---
output: Adjusted Class's Description:  A very simple class. Now 100% adjusted.
Adjusted Struct's Description:  A simple structure. (adjusted)

小练习:

给ExampleProtocol再增加一个要求。需要怎么改SimpleClass和SimpleStructure才能保证它们仍旧遵循这个协议?

参考答案: 在SimpleClass和SimpleStructure增加一个方法实现该要求


注意声明SimpleStructure时候,mutating关键字用来标记一个会修改结构体的方法。SimpleClass的声明不需要标记任何方法,因为类中的方法通常是可以修改类的属性(类的性质)。

使用extension来为现有的类型添加功能,比如新的方法和计算属性。可以使用扩展让某个在别处声明的类型类遵守某个协议,这同样适用于从外部库或者框架引入的类型。

extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 0
    }
}
print("Extension > Int: ", 7.simpleDescription)
---
output: Extension > Int:  The number 7

小练习:

给Double类型写一个扩展,添加roundValue方法

参考答案:

protocol DoubleProtocol {
    func roundValue() -> Int
}

extension Double: DoubleProtocol {
    func roundValue() -> Int {
        return Int((self).rounded())
    }
}
print("Extension > Double: ", (3.14).roundValue())
---
output: Extension > Double:  3

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

let protocolValue: ExampleProtocol = a
print(protocolValue.simpleDescription)  
// print(protocolValue.anotherProperty)  // 如果取消前面的注释,这句程序便会报错
---
output: A very simple class. Now 100% adjusted.

即使protocolValue变量运行时的类型时simpleClass,编译器还是会把它的类型当作ExampleProtocol,此时表示不能调用在协议之外的方法或者属性。


错误处理

使用采用Error协议的类型来表示错误。

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 "\(printerName), Job sent."
}
print("ErrorDemo: ", try send(job: 10, toPrinter: "Okay"))
---
output: ErrorDemo:  Okay, Job sent.

有多种方式可以用来进行错误处理。一种方式时使用do-catch。在do代码块中,使用try来标记可以抛出错误的代码。在catch代码块中,除非另外命名,否则错误会自动命名为error

do {
    let printerResponse = try send(job: 1040, toPrinter: "Jensen")
    print(printerResponse)
} catch {
    print(error)
}
---
output: Jensen, Job sent.

小练习:

将printerName改成“Never has Toner”使得send(job:toPrinter:)函数抛出错误

参考答案:

do {
    let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
    print(printerResponse)
} catch {
    print(error)
}
---
output: noToner

可以使用多个catch块来处理特定的错误,参照switch中的case风格来写catch

do {
    let printerResponse = try send(job: 0, toPrinter: "Never has Toner")
    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)
}
---
output: Printer error: noToner

小练习:

在do代码块中添加抛出错误的代码,需要抛出哪种错误来使第一个catch块进行接收?怎么使第二个和第三个catch进行接收呢

参考答案: 要使第一个catch块接收,需要抛出onFire错误;抛出PrinterError中除onFire之外其他错误即可被第二个catch块接收;抛出非PrinterError的错误即可被第三个catch块接收


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

let printerSuccess = try? send(job: 1884, toPrinter: "Jensen is handsome")
let printerFailure = try? send(job: 1885, toPrinter: "Never has Toner")
print("printerSuccess: \(printerSuccess)\nprinterFailure: \(printerFailure)")
---
output: printerSuccess: Optional("Jensen is handsome, Job sent.")
printerFailure: nil
---
warning: String interpolation produces a debug description for an optional value; did you mean to make this explicit?

使用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
}
print("Do this fridge contains banana? ", fridgeContains("banana"))
print("FridgeIsOpen: ", fridgeIsOpen)
---
output: Do this fridge contains banana?  false
FridgeIsOpen:  false

泛型

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

func makeArray<Item>(repeating item: Item, numberOfTimes: Int) -> [Item] {
    var result: [Item] = []
    for _ in 0..<numberOfTimes {
        result.append(item)
    }
    return result
}
let repeatNum: [String] = makeArray(repeating: "Test", numberOfTimes: 5)
print(repeatNum)
---
output: ["Test", "Test", "Test", "Test", "Test"]

也可以创建泛型函数、方法、类、枚举和结构体

// (重新实现Swift标准库中的可选类型)
enum OptionalValue<Wrapped> {
    case none
    case some(Wrapped)
}
var possibleInteger: OptionalValue<Int> = .none
possibleInteger = .some(100)
print(possibleInteger)
---
output: some(100)

在类型名后面使用where来指定对类型的一系列需求,比如,限定类型实现某一个协议,限定两个类型是相同的,或者限定某个类必须有一个特定的父类。

func anyCommonElements<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> Bool
where T.Element: Equatable, T.Element == U.Element  // 遵循Equatable协议可以包含对 == 和 != 的实现
{
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                return true
            }
        }
    }
    return false
}
print("WHERE Test: ", anyCommonElements([1, 2 ,3], [3]))
---
output: WHERE Test:  true

小练习:

修改anyCommonElements(_ :_ :)函数来创建一个函数,返回一个数组,内容是两个序列的共有元素

参考答案:

func anyCommonElementsInArray<T: Sequence, U: Sequence>(_ lhs: T, _ rhs: U) -> [T.Element]
where T.Element: Equatable, T.Element == U.Element
{
    var outArray: [T.Element] = []
    for lhsItem in lhs {
        for rhsItem in rhs {
            if lhsItem == rhsItem {
                outArray.append(lhsItem)
            }
        }
    }
    return outArray
}
print("CommonArray: ", anyCommonElementsInArray([0, 1, 2, 3, 4], [0, 3, 4, 5, 6]))
---
output: CommonArray:  [0, 3, 4]

<T: Equatable><T> ... where T: Equatable的写法是等价的。


本帖只是对Swift语言做一个概括性的介绍,后续将给大家继续分享Swift语言的详细教程。

Last updated on