Jeffrey · Chiang

Swift 快速之旅

探索Swift的功能和语法

Hello, World!

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

变量和常昊

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

var myVariable = 42
myVariable = 50
let myConstant = 30

与ts类似,常量与变量须分配与其类型对应的值,但因编译器的类型推断功能而无需总是显式编写类型,仅在初始值未能提供足够信息(或无初始值)时,才需要明确指定相应类型:

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

常/变量的值永远不会隐式转换为另一种类型,如需转换,须显式创建所需类型的实例:

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

如果要在字符串中包含值,可以使用Swift中的占位符\( variable / constant ),此时无须显式转换为字符类型:

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

使用三个双引号(””“)实现多行广西,引号内的每行开头的缩进,与结束标记(””“)对齐的部分会被移除!

let quotation = """
        Even though there's whitespace to the left,
        the actual lines aren't indented.
            Except for this line.
        Double quotes (") can appear without being escaped.

        I still have \(apples + oranges) pieces of fruit.
        """

使用一对中括号([])创建数组和字典,并通过在方括号内写入索引或键名来访问元素,最后一个元素允许有逗号:

var fruits = ["strawberries", "limes", "tangerines"]
fruits[1] = "grapes"


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

可以直接添加元素,无需考虑数组长度:

fruits.append("blueberries")
print(fruits)
// Prints "["strawberries", "grapes", "tangerines", "blueberries"]"

使用( [ ] )或( [ : ] )创建一个空数组或字典的实例:

fruits = []
occupations = [:]

如果要将创建的空数组或字典分配给新的变量或常量,或其他没有任何类型信息的地方,需要指定类型:

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

控制流

使用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)
// Prints "11"

if/ switch

  • if/switch中,条件必须是布尔表达式,无法像js/ts中一样进行与泛空值进行隐式比较
  • if/switch可以写在赋值符号(=)或 return 后,实现类似的三目运算
let scoreDecoration = if teamScore > 10 {
    "🎉"
} else {
    ""
}
print("Score:", teamScore, scoreDecoration)
// Prints "Score: 11 🎉"

if结合let用来处理可选值:

// 在值类型后面加问号(?)表示可选值(要么包含一个值,要么包含nil表示缺少一个值)
var optionalString: String? = "Hello"
print(optionalString == nil)
// Prints "false"


var optionalName: String? = "John Appleseed"
var greeting = "Hello!"
// 仅optionalName有值时,才会赋值给name并进入if结构逻辑
if let name = optionalName {
    greeting = "Hello, \(name)"
}
// 也可使用双问号(??)处理可选值
let nickname: String? = nil
let fullName: String = "John Appleseed"
let informalGreeting = "Hi \(nickname ?? fullName)"

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.")
} 
// 每个匹配的case执行完毕后会自动跳出,所以不需要在每个case内显式break

for-inwhilerepeat-while

使用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
// key-value pair iterate over dictionary
for (_, 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)
// Prints "128"


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

for-in循环中可以使用( ..< )创建仅用于循环中的索引:

var total = 0
// include 0, and exclude 4
for i in 0..<4 {
    total += i
}
// include 0 and 4
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()

函数是流类型,即:函数可以返回另一个函数作为返回值:

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)

函数是闭包的特例:可以稍后调用的代码块。即使在不同的作用域中执行,闭包中的代码也可以访问创建闭包的作用域中的常/变量和函数。

可以使用花括号(braces)( { } )将代码插括起来定义没有名称的闭包:

numbers.map({ (number: Int)->Int in //  in 用于分隔参数、返回类型和主体
	let result = 3 * number
	return result
})
// 闭包更简单的形式:省略闭包已知的参数类型、返回值类型,单语句闭包隐式返回唯一语句值:
let mappedNumbers = numbers.map({ number in 3 * number })
print(mappedNumbers)


func outter (_ a:Int,_ b:Int,adder:(Int,Int)->Int)->Void {
    print(adder(a,b))
}

// 如果闭包是函数的最后一个参数,可以立即出现在括号后面:
outter(5,29) {(a:Int,b:Int)->Int in a+b}
// 使用dolla符号($)加零基索引引用闭包参数,可以省略闭包的参数定义
outter(4,9) {$0+$1}

let scores=[5,3,100,4,9]
// 当闭包是函数的唯一参数时,可以完全省略括号
scores.map { $0 + 2 }

对象和类

声明

使用class后跟类名创建类,类中的属性声明的编写方式与常/变量声明方式相同,方法与函数声明的编写方式相同,只是它们都位于类的上下文中:

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

实例化

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

var shape = Shape() // no [new] keyword
shape.numberOfSides = 7
var shapeDescription = shape.simpleDescription()

生命周期

类的初始化,通过init方法实现创建类实例时的初始化逻辑,deinit实现类实例释放时的清理逻辑:

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


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

		deinit(){
				//todo:clearn...
				self.name = nil
		}
		
    func simpleDescription() -> String {
       return "A shape with \(numberOfSides) sides."
    }
}

继承

通过冒号( : )实现类的继承,如果要覆写超类中的方法,需要显式标记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

属性可以包含getter和setter,也可以仅包含getter,但不能只有setter

class EquilateralTriangle: NamedShape {
    var perimeter: Double {
        get {
             return 3.0 * sideLength
        }
        set {
            sideLength = newValue / 3.0 // newValue为约定名称,可以在set后跟括号,并在括号内指定其他名称
        }
    }
}

willSet/didSet

如果仅想在个性某变量之前或之后执行某些逻辑,可以使用willSet/didSet:

class TriangleAndSquare {
    var triangle: EquilateralTriangle {
    		// willSet/didSet不能和get或set一起出现
        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,否则,可选值将被展开

枚举和结构

枚举

使用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中,枚举的引用采用缩写形式,即不需要指定Rank
        case .ace:
            return "ace"
        case .jack:
            return "jack"
        case .queen:
            return "queen"
        case .king:
            return "king"
        default:
            return String(self.rawValue)
        }
    }
}
// 在赋值给常量时,因为ac没有类型限定,需要全名引用,即通过Rank限定,
let ace = Rank.ace
//如果指定的类型,则也可以使用缩写引用
let queen:Rank = .queen

let aceRawValue = ace.rawValue

默认情况下,Swift分配的原始值从0开始,每次递增1,但可以通过指定值来修改此行为。

使用init?.(rawValue:)初始化一个枚举的实例,如果有与此原始值匹配的定义,则返回初始化后的枚举实例,否则返回nil:

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

枚举值的引用有两种:在缺少类型限定的地方,使用全量名称,即枚举类型加字段名:Rank.queen,如果有类型限定的地方,可以使用缩写:.queen

如果枚举具有原始值,则这些值作为声明的一部分,意味着特定枚举实例始终有相同的原始值;枚举也可以具有与具体实例关联的值,这些关联值是在创建枚举实例时确定的,且每个相同的枚举实例,可以有不同的关联值:

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

// 后续多个ServerResponse.result,可以关联多对不同的时间
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创建结构,结构支持许多与类相同的行为,包括方法和初始值设定项。结构和类之间最重要的区别之一是结构在代码中传递时总是初始复制,而类则是引用。

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

并发性

使用async标记异步执行的函数:

func fetchUserID(from server: String) async -> Int {
    if server == "primary" {
        return 97
    }
    return 501
}
//通过await调用异步函数
func fetchUsername(from server: String) async -> String {
    let userID = await fetchUserID(from: server)
    if userID == 501 {
        return "John Appleseed"
    }
    return "Guest"
}
// async let以异步方式调用异步函数,真正使用返回值时,写入await:
func connectUser(to server: String) async {
    async let userID = fetchUserID(from: server)
    async let username = fetchUsername(from: server)
    let greeting = await "Hello \(username), user ID \(userID)"
    print(greeting)
}

// Task用于从同步代码调用异步函数,而无需等待
Task {
    await connectUser(to: "primary")
}
// Prints "Hello Guest, user ID 97"
//使用任务组组织并发代码
let userIDs = await withTaskGroup(of: Int.self) { group in
    for server in ["primary", "secondary", "development"] {
        group.addTask {
            return await fetchUserID(from: server)
        }
    }
    var results: [Int] = []
    for await result in group {
        results.append(result)
    }
    return results
}

Actor

actor与类类似,但actor确保不同的异步函数可以同时安全的与同一Actor的实例进行交互:

actor ServerConnection {
    var server: String = "primary"
    private var activeUsers: [Int] = []
    func connect() async -> Int {
        let userID = await fetchUserID(from: server)
        // ... communicate with server ...
        activeUsers.append(userID)
        return userID
    }
}

// 调用actor上的方法或属性时,可以将代码标记为await,以指示它可能必须等待actor上已运行的其他代码完成,即异步特性的扩散性
let server = ServerConnection()
let userID = await server.connect()

protocol

使用protocol声明协议(即接口):

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

类、枚举和结构都可以采用协议:

struct SimpleStructure: ExampleProtocol {
     var simpleDescription: String = "A simple structure"
     // 在采用协议的结构中,使用mutating关键字来标记修改结构中的方法
     mutating func adjust() {
          simpleDescription += " (adjusted)"
     }
}
var b = SimpleStructure()
b.adjust()
let bDescription = b.simpleDescription
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

// 使用extension关键字来向现有类型追加协议并实现功能
extension Int: ExampleProtocol {
    var simpleDescription: String {
        return "The number \(self)"
    }
    mutating func adjust() {
        self += 42
    }
 }
print(7.simpleDescription)
// Prints "The number 7"

// a运行时类型为SimpleClass,但因限定类型为ExampleProtocol,通过protocolClass,所以仅能访问ExampleProtocol定义的成员
let protocolClass:any ExampleProtoc为l = a
print(protocolClass.simpleDescription)

错误处理

可以通过任何采用Error协议的任何类型来表示错误

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

// throws用于标记引发错误的函数,如果函数中引发错误,该函数将立即返回,且调用该的代码将处理该错误
func send(job: Int, toPrinter printerName: String) throws -> String {
    if printerName == "Never Has Toner" {
        throw PrinterError.noToner
    }
    return "Job sent"
}

// 1、使用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后可以像switch的case一样,跟一个匹配模式
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"

// 2、使用try?将结果转换为可选项,如果函数引发错误,将丢弃对应的错误并返回nil结果
let printerSuccess = try? send(job: 1884, toPrinter: "Mergenthaler")
let printerFailure = try? send(job: 1885, toPrinter: "Never Has Toner")

使用defer可以在函数内写入一段将在该函数所有其他代码都执行完毕后,函数返回之前执行的代码块,该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
}
if fridgeContains("banana") {
    print("Found a 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)

定义泛型的正文前可以通过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])