Xcode — Swift

官方文档 #

The Swift Programming Language

https://developer.apple.com/documentation/swift

Swift 中文指南

The Swift Programming Language 中文版

语言特点 #

https://www.swift.org/about/

Swift is a general-purpose programming language that’s approachable for newcomers and powerful for experts. It is fast, modern, safe, and a joy to write.

变量声明 #

swift 使用 let 来声明常量,使用 var 来声明变量。常量只能赋值一次。即:let 的内存地址不能变,var 的内存地址可变。

// 只能赋值一次,再次赋值会报错
let hello = "world"
print(hello)

// hello 可以多次赋值
var hello = "world"
print(hello)
print("hello \(hello)")

声明时可以不指定类型,编译器会自动推断其类型,当然也可以像下面这样指定:

let label: String = "The width is"

数据类型 #

Int #

Int 是有符号整型,默认是当前平台的字长(如 32 位或 64 位),Uint 是无符号整型。

let age: Int = 25
let smallNumber: Int8 = 127
let largeNumber: Int64 = 9223372036854775807

Float 和 Double #

Double 是默认使用的浮点类型,64 位精度,Float 是 32 位。

let pi: Float = 3.14159
let pi: Double = 3.141592653589793

在 Swift 语言中,你可以使用下划线来提高长数字的可读性。比如:

let latitude: Double = 34.011_286
let longitude: Double = -118.495_308

print("Latitude: \(latitude)")  // 输出: Latitude: 34.011286
print("Longitude: \(longitude)") // 输出: Longitude: -118.495308

另外,在 JavaScript 中,同样可以使用下划线来提高可读性:

const latitude = 34.011_286;
const longitude = -118.495_308;

console.log(`Latitude: ${latitude}`);  // 输出: Latitude: 34.011286
console.log(`Longitude: ${longitude}`); // 输出: Longitude: -118.495308

Bool #

Bool 类型的可选值只有 truefalse

let isSwiftAwesome: Bool = true

String #

双引号在 Swift 中用于定义字符串(String)类型。字符串可以是多个字符的组合,也可以包含转义字符和字符串插值。

// 定义字符串
let greeting: String = "Hello, World!"

// 包含转义字符的字符串
let quote: String = "He said, \"Swift is awesome!\""

// 字符串插值
let name: String = "Alice"
let message: String = "Hello, \(name)"

Character #

特别需要注意的是,Swift 实际上不支持单引号来定义字符。单引号使用在 Swift 语言中并没有像某些其他编程语言(如 C 或 Java)那样的用途。在 Swift 中,字符也是用双引号表示的,只是字符类型是 Character

// 正确的字符定义(注意仍然使用双引号)
let letter: Character = "A"
let emoji: Character = "😊"
// 错误示例:let invalidCharacter: Character = "AB"  // 会报编译错误

恒等运算符 #

详细了解请参考:

在 Swift 中,恒等运算符(Identity Operators)用于判断两个对象实例是否引用同一个内存地址。为了进行这种比较,Swift 提供了两个恒等运算符:

  • ===(恒等): 判断两个引用类型(如类实例)是否引用同一个对象。
  • !==(不恒等): 判断两个引用类型是否引用不同的对象。

=== 运算符用于判断两个引用类型实例是否完全相同,即它们是否引用了同一个内存地址。

class Person {
    var name: String
    init(name: String) {
        self.name = name
    }
}

let person1 = Person(name: "Alice")
let person2 = person1
let person3 = Person(name: "Alice")

// person1 和 person2 指向同一个实例
if person1 === person2 {
    print("person1 and person2 refer to the same instance.")
} else {
    print("person1 and person2 do not refer to the same instance.")
}

// person1 和 person3 指向不同的实例,但其内容相同
if person1 === person3 {
    print("person1 and person3 refer to the same instance.")
} else {
    print("person1 and person3 do not refer to the same instance.")  // 这一句会执行
}

!== 运算符用于判断两个引用类型实例是否不同,即它们是否引用了不同的内存地址。

if person1 !== person3 {
    print("person1 and person3 do not refer to the same instance.")
} else {
    print("person1 and person3 refer to the same instance.")
}

注意事项

  1. 引用类型: 恒等运算符仅适用于引用类型(如类实例),因为它们比较的是内存地址。对于值类型(如结构体、枚举和基本数据类型),你应该使用等号 == 来比较其内容。

  2. 内容比较: 如果你需要判断两个实例的内容是否相同(而不是它们是否引用同一个对象),你应当实现相应的 Equatable 协议并使用 == 运算符。

值类型比较示例

struct Point: Equatable {
    var x: Int
    var y: Int
}

let point1 = Point(x: 1, y: 1)
let point2 = Point(x: 1, y: 1)

if point1 == point2 {
    print("point1 and point2 have the same content.")
} else {
    print("point1 and point2 have different content.")
}

实现 Equatable 协议 #

如果你创建了一个自定义类或结构体,并希望比较其内容相等性,你可以通过实现 Equatable 协议来实现内容比较。

class Person: Equatable {
    var name: String

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

    // 实现 Equatable 协议的方法
    static func == (lhs: Person, rhs: Person) -> Bool {
        return lhs.name == rhs.name
    }
}

let person1 = Person(name: "Alice")
let person2 = Person(name: "Alice")

if person1 == person2 {
    print("person1 and person2 have the same content.")
} else {
    print("person1 and person2 have different content.")
}

总之,使用恒等运算符(===!==)可以帮助你确保你在处理的是同一个对象实例,而在比较值类型或对象内容时,应使用 ==!= 运算符。了解这两者的区别和使用场景可以帮助你编写更安全且高效的代码。

可选类型 ?! #

Swift 的可选类型(Optional)是一种非常强大的特性,用于表示变量可能有值,也可能为 nil,即没有值。可选类型确保在处理未初始化或缺失值时代码更加安全和清晰,减少了运行时错误。

可选类型的定义 #

在 Swift 中,你可以使用 ? 语法将任何类型变为可选类型。例如:

var optionalString: String? = "Hello"

这里,optionalString 是一个可选的字符串类型,它可以包含一个字符串值,也可以为 nil

可选类型的使用 #

1. 强制解包(Forced Unwrapping) #

你可以使用感叹号 ! 强制解包一个可选类型以获取其中的值,但如果该可选类型为 nil,程序将崩溃。因此,在使用强制解包时需要确保变量一定有值。

var optionalString: String? = "Hello"
if optionalString != nil {
    print(optionalString!)  // 输出: Hello
}

2. 可选绑定(Optional Binding) #

可选绑定使用 if letguard let 语法安全地解包可选类型,如果解包成功,可以在特定作用域内使用解包后的值。

// if let 语法
if let unwrappedString = optionalString {
    print(unwrappedString)  // 输出: Hello
}

// guard let 语法
func printOptionalString(_ string: String?) {
    guard let unwrappedString = string else {
        print("No value")
        return
    }
    print(unwrappedString)
}
printOptionalString(optionalString)  // 输出: Hello

3. 隐式解包(Implicitly Unwrapping) #

隐式解包的可选类型申明使用 ! 而不是 ?。这种类型在初始化后,可以直接当作非可选类型使用,但如果未被赋值并访问会导致运行时错误。

var implicitUnwrappedString: String! = "Hello"
print(implicitUnwrappedString)  // 输出: Hello

4. 可选链(Optional Chaining) #

通过可选类型的可选链功能,你可以在一系列调用中使用 ? 链接多个可选类型,如果其中任意一个为 nil,整个表达式将返回 nil

class Person {
    var name: String?
    var address: Address?
}

class Address {
    var city: String?
}

let person = Person()
person.address = Address()
person.address?.city = "San Francisco"

// 使用可选链
if let city = person.address?.city {
    print("City is \(city)")  // 输出: City is San Francisco
} else {
    print("City is not set")
}

5. Nil Coalescing 操作符 #

使用 ?? 操作符提供默认值,如果可选类型为 nil,则返回默认值。

let optionalString: String? = nil
let defaultValue = optionalString ?? "Default Value"
print(defaultValue)  // 输出: Default Value

类型定义中的可选 #

你也可以将可选用在函数的参数和返回值类型中:

// 可选参数
func greet(name: String?) {
    guard let name = name else {
        print("Hello, guest!")
        return
    }
    print("Hello, \(name)!")
}

greet(name: "Alice")  // 输出: Hello, Alice!
greet(name: nil)      // 输出: Hello, guest!

// 可选返回类型
func findNameInDictionary(dict: [String: String], key: String) -> String? {
    return dict[key]
}

let names = ["Alice": "Engineer", "Bob": "Manager"]
if let title = findNameInDictionary(dict: names, key: "Alice") {
    print("Alice's title is \(title).")
} else {
    print("Title not found.")
}

总结 #

Swift 的可选类型提供了一种强大且安全的方式来处理可能没有值的变量,使得代码更具鲁棒性。通过强制解包、可选绑定、隐式解包、可选链和 Nil Coalescing 操作符等技术,你可以灵活地处理各种场景中的可选类型,根据上下文选择最适合的方法。

类型转换 #

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

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

数据结构 #

数组 Array #

有序的集合,可以存储相同类型的元素。

let emptyArray: [String] = []
let numbers: [Int] = [1, 2, 3, 4, 5]
let names: [String] = ["Alice", "Bob", "Charlie"]

集合 Set #

无序且唯一的集合,可以存储相同类型的元素。

let uniqueNumbers: Set<Int> = [1, 2, 3, 4, 4, 5]
// uniqueNumbers = [1, 2, 3, 4, 5]

字典 Dictionary #

无序的键值对集合,其中键是唯一的。

let studentGrades: [String: String] = ["Alice": "A", "Bob": "B"]
let emptyDictionary: [String: Float] = [:]

元组 Tuple #

用于将多个值组合成一个复合值,不要求类型相同。

let http404Error = (404, "Not Found")
let person = (name: "Alice", age: 25)
print(person.name)  // 输出: Alice
print(person.age)   // 输出: 25

结构体 Struct #

struct ReplaxModel: Hashable, Codable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    
    private var imageName: String
}

下标操作(subscript) #

在 Swift 中,下标(Subscript)是用于访问集合、列表或序列中特定位置的元素的一种语法。下标允许你使用类似数组的语法来读取和写入集合内的值。下标可以用于各种数据结构,如数组(Array)、字典(Dictionary)、字符串(String)等。除此之外,Swift 还允许自定义类型定义自己的下标操作。

数组中的下标 #

对于数组,Swift 提供了基于整数索引的下标操作,这和其他编程语言类似。

var fruits = ["Apple", "Banana", "Cherry"]
let firstFruit = fruits[0]  // 访问第一个元素,返回 "Apple"
fruits[1] = "Blueberry"     // 修改第二个元素的值为 "Blueberry"
print(fruits)               // 输出 ["Apple", "Blueberry", "Cherry"]

字典中的下标 #

字典的下标操作允许使用键来访问或修改值。

var ages = ["Alice": 25, "Bob": 28]
let aliceAge = ages["Alice"]  // 访问 Alice 的年龄,返回 25
ages["Bob"] = 30               // 修改 Bob 的年龄为 30
print(ages)                    // 输出 ["Alice": 25, "Bob": 30]

自定义下标 #

Swift 允许你为自己的自定义类型定义下标。通过实现下标,可以让你自己定义的数据结构也支持下标操作。

struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        self.grid = Array(repeating: 0.0, count: rows * columns)
    }
    
    func indexIsValid(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }
    
    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValid(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

var matrix = Matrix(rows: 2, columns: 2)
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2
print(matrix[0, 1])  // 输出 1.5
print(matrix[1, 0])  // 输出 3.2

在这个示例中,Matrix 结构体定义了一个二维数组(矩阵)的下标,使得你可以通过 matrix[row, column] 这种方式来访问和修改矩阵中的元素。

下标的定义 #

自定义下标时,可以在结构体、类、枚举中定义一个或多个下标。下标可以接受一个或多个输入参数,并可以返回一个值。下标语法如下:

subscript(parameters) -> ReturnType {
    get {
        // 返回适当的值
    }
    set(newValue) {
        // 执行适当的赋值操作
    }
}
  • parameters:输入参数的名称和类型。
  • ReturnType:下标操作返回值的类型。
  • get 和 set 代码块:定义了如何获取和设置值。

下标定义支持计算属性的所有功能,例如可以读取和写入,也可以定义只读下标。

总结 #

  • 在 Swift 中,下标(Subscript)提供了一种简洁的语法,用于访问集合类型(如数组和字典)中的元素。
  • 下标可以重载,允许自定义类型提供自己的下标操作。
  • 下标可以是读取和写入的,也可以是只读的。

通过下标,可以使代码更直观和简洁,特别是在处理集合类型的数据结构时。

程序控制 #

if 判断语句 #

let username = "admin"
let password = "1234"

if username == "admin" && password == "1234" {
    print("Login successful")
} else {
    print("Login failed")
}

if-let 语句 #

You can use if and let together to work with values that might be missing. These values are represented as optionals. An optional value either contains a value or contains nil to indicate that a value is missing. Write a question mark (?) after the type of a value to mark the value as optional.

if let name = optionalName {
    greeting = "Hello, \(name)"
}

switch 语句 #

goto 关键字 #

for 语句 #

case 1

let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
    print("Hello, \(name)!")
}

case 2

for _ in 0..<10 {
    let newItem = Item(context: viewContext)
    newItem.timestamp = Date()
}

同样支持 continue 和 break

do-catch 语句与 try 关键字 #

在 Swift 中,try 关键字用于异常处理(也称为错误处理)。它与 do-catch 语句结合使用,以处理可能在运行过程中发生的错误。Swift 的错误处理模型与许多其他编程语言类似,通过抛出和捕获异常来处理错误情况。

  1. 抛出错误(Throwing Errors):定义哪些函数或方法可以抛出错误。
  2. 处理错误(Handling Errors):捕获并处理抛出的错误。

在 Swift 中,try 关键字用于调用可能抛出错误的函数或方法。它可以与 do-catch 语句结合使用,也可以单独使用。除了在 do-catch 语句中使用 try,Swift 还提供了 try?try! 两种特殊的 try 用法:

1. trydo-catch 结合使用 #

同时可以看到 throw 关键字的使用。

enum FileError: Error {
    case fileNotFound
    case unreadable
    case encodingFailed
}

func readFile(filename: String) throws -> String {
    // 模拟抛出错误
    throw FileError.fileNotFound
}

do {
    let content = try readFile(filename: "example.txt")
    print(content)
} catch FileError.fileNotFound {
    print("File not found.")
} catch FileError.unreadable {
    print("File is unreadable.")
} catch FileError.encodingFailed {
    print("Failed to encode the file.")
} catch {
    print("An unexpected error occurred: \(error).")
}

2. try? #

try? 将尝试执行可能抛出错误的表达式,如果抛出错误,则返回 nil,否则返回可选值。因此,它可以避免使用 do-catch 语句:

if let content = try? readFile(filename: "example.txt") {
    print(content)
} else {
    print("Failed to read the file.")
}

3. try! #

try! 将强制执行可能抛出错误的表达式,并假定不会抛出错误。如果错误确实抛出,程序将崩溃。应谨慎使用:

let content = try! readFile(filename: "example.txt")
print(content)

自定义错误类型 #

可以通过实现 Error 协议来定义自定义的错误类型。以下是一个定义和使用自定义错误类型的示例:

enum DivisionError: Error {
    case dividedByZero
}

func divide(_ num: Int, by divisor: Int) throws -> Int {
    if divisor == 0 {
        throw DivisionError.dividedByZero
    }
    return num / divisor
}

do {
    let result = try divide(10, by: 2)
    print("Result: \(result)")
} catch DivisionError.dividedByZero {
    print("Cannot divide by zero.")
} catch {
    print("An unexpected error occurred: \(error).")
}

guard-else 语句 #

guard 关键字是 Swift 中一个非常有用的控制语句,用于提前退出某个代码块,通常用于验证条件并确保程序流在有效状态下继续执行。如果条件不符合,guard 语句必须执行退出当前作用范围的操作,比如 returnbreakcontinuethrow

guard 的基本语法 #

guard condition else {
    // 如果条件不满足,则执行退出操作
    return  // 或其他退出操作
}

// 如果条件满足,继续执行下面的代码

使用场景 #

guard 关键字常用于以下几种情况:

  1. 条件验证: 用于检查函数参数或外部输入是否满足特定条件。
  2. 强制解包: 用于安全地解包可选值,并在解包失败时提前退出。
  3. 防御性编程: 提前处理错误或不期望的情况,使主代码路径更清晰。

使用示例 #

示例 1: 条件验证

func validateAge(age: Int) {
    guard age >= 18 else {
        print("You must be at least 18 years old.")
        return
    }
    print("Access granted.")
}

validateAge(age: 20)  // 输出: Access granted.
validateAge(age: 16)  // 输出: You must be at least 18 years old.

示例 2: 强制解包

func printUsername(user: [String: String]) {
    guard let username = user["username"] else {
        print("No username found.")
        return
    }
    print("Username is \(username).")
}

let user1 = ["username": "Alice"]
let user2 = ["email": "alice@example.com"]

printUsername(user: user1)  // 输出: Username is Alice.
printUsername(user: user2)  // 输出: No username found.

示例 3: 多个条件验证

func process(order: [String: Any]) {
    guard
        let productID = order["productID"] as? Int,
        let quantity = order["quantity"] as? Int,
        quantity > 0
    else {
        print("Invalid order.")
        return
    }
    print("Processing order for productID \(productID) with quantity \(quantity).")
}

let order1 = ["productID": 123, "quantity": 10]
let order2 = ["productID": "abc", "quantity": 0]

process(order: order1)  // 输出: Processing order for productID 123 with quantity 10.
process(order: order2)  // 输出: Invalid order.

使用注意事项 #

  • guard 必须退出: 在 else 块中必须执行退出操作,否则会引发编译错误。这保证了在条件不满足时程序的正确性。
  • 作用范围: 在 guard 语句中解包的值在验证通过后依然有效。这使得主代码路径更为简洁,更容易阅读和维护。

比较 guardif #

  • if 语句: 用于处理条件为真的情况。代码路径可能有多个分支,可能会导致嵌套和代码不易阅读。
  • guard 语句: 用于处理条件为假的情况,提前退出,有助于减少嵌套,使主代码路径更为直观和清晰。

示例对比

// 使用 if 嵌套
func processIf(order: [String: Any]) {
    if let productID = order["productID"] as? Int {
        if let quantity = order["quantity"] as? Int {
            if quantity > 0 {
                print("Processing order for productID \(productID) with quantity \(quantity).")
            } else {
                print("Invalid quantity.")
            }
        } else {
            print("Invalid quantity.")
        }
    } else {
        print("Invalid productID.")
    }
}

// 使用 guard 提前退出
func processGuard(order: [String: Any]) {
    guard
        let productID = order["productID"] as? Int,
        let quantity = order["quantity"] as? Int,
        quantity > 0
    else {
        print("Invalid order.")
        return
    }
    print("Processing order for productID \(productID) with quantity \(quantity).")
}

总之,guard 关键字的引入是为了提高代码的可读性和可维护性,使得你的代码结构更加清晰,减少不必要的嵌套,处理错误条件更加明确。

枚举 #

枚举(Enumeration)是 Swift 中的一种强大的数据类型,它允许你定义一组相关的值,并且每个值都被认为是同一类型的一部分。枚举可以帮助你组织代码,更好地表示和处理一组相关的值。

枚举的定义和使用 #

一个简单的枚举定义如下:

enum CompassPoint {
    case north
    case south
    case east
    case west
}

可以像这样声明一个枚举变量:

var direction = CompassPoint.north

你可以使用点语法来改变枚举的值:

direction = .east

关联值 #

枚举可以包含关联值,这使得枚举成员可以在其背后存储不同类型的关联值。关联值与枚举成员相关联,每个枚举成员的关联值类型可以各不相同。

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

你可以使用 switch 语句来匹配枚举值并提取关联值:

switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check)")
case .qrCode(let productCode):
    print("QR Code: \(productCode)")
}

原始值 #

枚举成员可以被预填充默认值,称为原始值(raw values)。这些原始值的类型可以是字符串、字符或任何整型或浮点类型,每个原始值在其枚举声明中必须是唯一的。

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

let earthsOrder = Planet.earth.rawValue // earthsOrder是3

你可以使用 rawValue 初始化一个枚举成员:

let possiblePlanet = Planet(rawValue: 3) // possiblePlanet 是 Planet.earth

枚举中的方法 #

枚举可以定义实例方法,方法可以访问枚举成员的值并运行一些逻辑:

enum CompassPoint {
    case north, south, east, west
    
    mutating func next() {
        switch self {
        case .north:
            self = .east
        case .east:
            self = .south
        case .south:
            self = .west
        case .west:
            self = .north
        }
    }
}

var direction = CompassPoint.north
direction.next()  // direction 现在是 .east

在这个例子中,next() 方法修改了 CompassPoint 枚举实例的值,并且因为它会修改实例,所以它被标记为 mutating

递归枚举 #

递归枚举是一种枚举类型,它有一个或多个枚举成员使用该类型的实例作为关联值。为了声明递归枚举,必须在枚举成员前使用 indirect 关键字。

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// 你也可以在枚举前加上 indirect 关键字
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

下面是如何使用递归枚举来定义和计算算术表达式的例子:

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

let result = evaluate(product) // result 是 18

使用注意 #

  1. 一旦确定变量的类型,再次为其赋值可以省略枚举类型名,使用更简短的点语法即可。
  2. 与其他语言的枚举不一样的是,swift 的枚举成员在创建时不会被赋予一个默认的整型值。
  3. switch 时 case 要覆盖全,或者用 default ,不然会报错。

小结 #

  • 定义枚举 使用 enum 关键字。
  • 基础用法:可以定义基本成员,并通过点语法访问和设置。
  • 关联值:允许枚举成员携带不同类型的值,增加灵活性。
  • 原始值:为枚举成员预定义原始值,可以是字符串、整型或浮点数。
  • 枚举方法:枚举可以包含方法,以增加行为逻辑。
  • 递归枚举:允许枚举成员包含自身类型的实例,用于构建复杂的数据结构。

枚举在 Swift 中是非常强大的工具,有助于创建更加严谨和类型安全的代码。

函数 #

在 Swift 中,函数是一等公民。这意味着函数不仅可以用来执行一系列指令,还可以作为参数传递给其他函数,作为返回值从其他函数中返回,甚至可以嵌套和捕获周围作用域中的变量。下面是 Swift 中函数的详细介绍和使用示例。

定义和调用函数 #

基本定义和调用 #

你可以使用关键词 func 来定义一个函数:

func greet(name: String) -> String {
    return "Hello, \(name)!"
}

let greeting = greet(name: "Alice")
print(greeting)  // 输出: "Hello, Alice!"

这里 greet 函数接受一个参数 name,返回一个 String。你可以通过传入实际参数来调用该函数。

多个参数和参数标签 #

函数可以接受多个参数。可以使用参数标签使函数调用时更加清晰:

func sum(a: Int, b: Int) -> Int {
    return a + b
}

let result = sum(a: 5, b: 3)
print(result)  // 输出: 8

还可以定义外部参数标签,使调用更具可读性:

func join(string s1: String, with s2: String, separator: String) -> String {
    return s1 + separator + s2
}

let joinedString = join(string: "Hello", with: "World", separator: ", ")
print(joinedString)  // 输出: "Hello, World"

_ 下划线可以用来省略外部参数名。

# 井号的使用:参数标签省略(不推荐) #

在 Swift 中,函数参数可以使用 # 井号前缀,这种做法称为参数标签省略。在这种情况下,调用函数时会使用参数名作为标签,而无需在调用位置显式地写出参数标签。这种特性使得代码更加简洁和可读。

假设有一个函数需要两个参数,可以将参数标签前缀加 #,如下所示:

func greet(#name: String, #age: Int) {
    print("Hello, \(name)! You are \(age) years old.")
}

greet(name: "Alice", age: 30)
// 输出: Hello, Alice! You are 30 years old.

在这个示例中,greet 函数的参数 nameage 都使用了 # 前缀。这意味着在调用函数时,可以直接使用参数名,使代码更具可读性。我们可以对比没有 # 情况的函数:

func sum(_ a: Int, _ b: Int) -> Int {
    return a + b
}

let result = sum(10, 20)
print(result)  // 输出: 30

在这个没有参数标签的函数调用中,读者无法通过参数本身直接知道每个参数的含义。实际上,Swift 5.3之后,参数标签使用 # 的行为已经发生了变化。参数标签省略和参数标签的默认行为有所不同。以下是 Swift 5.3 和以后版本中,函数参数标签的推荐和默认使用方式:

// 推荐的参数标签和参数名不同使用方式
func greet(person name: String, from hometown: String) {
    print("Hello, \(name)! Glad you could visit from \(hometown).")
}

greet(person: "Alice", from: "New York")

在 Swift 当前版本中,虽然早期版本允许直接使用 # 井号作为参数标签前缀,但在更现代的版本(Swift 5.3 以后),为了避免可能的混淆和清楚起见,通常情况下推荐的做法是显式地定义参数标签和参数名以确保代码的可读性和清晰度。

如果你仍在使用较早版本的 Swift 编译器,你可以继续使用 # 井号前缀,但更新的代码库中推荐使用参数标签。希望这能帮你理解函数参数标签的使用。

默认参数值 #

可以为参数指定默认值,这样调用函数时可以省略这些参数:

func greet(name: String = "Guest") -> String {
    return "Hello, \(name)!"
}

print(greet())  // 输出: "Hello, Guest!"
print(greet(name: "Bob"))  // 输出: "Hello, Bob!"

返回值和多重返回值 #

单一返回值 #

函数可以返回一个值,使用 -> 符号来指定返回类型:

func square(number: Int) -> Int {
    return number * number
}

let squaredValue = square(number: 4)
print(squaredValue)  // 输出: 16

多重返回值 #

可以使用元组返回多个值:

func minMax(array: [Int]) -> (min: Int, max: Int)? {
    guard let minElement = array.min(), let maxElement = array.max() else {
        return nil
    }
    return (minElement, maxElement)
}

if let bounds = minMax(array: [1, 2, 3, 4, 5]) {
    print("min: \(bounds.min), max: \(bounds.max)")
}

函数参数和返回函数 #

参数为函数 #

函数可以接受其他函数作为参数:

func applyOperation(_ a: Int, _ b: Int, operation: (Int, Int) -> Int) -> Int {
    return operation(a, b)
}

let result = applyOperation(4, 2, operation: { $0 * $1 })
print(result)  // 输出: 8

返回函数 #

函数也可以返回其他函数:

func makeIncrementer(amount: Int) -> (Int) -> Int {
    func incrementer(value: Int) -> Int {
        return value + amount
    }
    return incrementer
}

let incrementByFive = makeIncrementer(amount: 5)
let result = incrementByFive(10)
print(result)  // 输出: 15

内嵌函数和闭包 #

内嵌函数 #

内嵌函数可以捕获其外围函数中定义的变量和常量:

func outerFunction() -> () -> Int {
    var total = 0
    func innerFunction() -> Int {
        total += 1
        return total
    }
    return innerFunction
}

let incrementer = outerFunction()
print(incrementer())  // 输出: 1
print(incrementer())  // 输出: 2

闭包 #

闭包是可以在代码中传递和引用的自包含代码块。你可以使用 {} 来定义闭包:

let numbers = [1, 2, 3, 4, 5]
let doubledNumbers = numbers.map { $0 * 2 }
print(doubledNumbers)  // 输出: [2, 4, 6, 8, 10]

可变参数 #

函数可以接受可变数量的参数:

func sum(of numbers: Int...) -> Int {
    return numbers.reduce(0, +)
}

let result = sum(of: 1, 2, 3, 4, 5)
print(result)  // 输出: 15

输入输出参数和关键字 inout #

在 Swift 中,inout 关键字用于函数参数,表示该参数是一个传入和传出参数,也就是说,这个参数允许在函数内部被修改,并且这种修改会在函数返回后保留。换句话说,使用 inout 参数,你可以在函数内部修改传入的变量,并且这些修改会反映到函数的调用者。

func swapValues(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}

var x = 3
var y = 5
swapValues(&x, &y)
print(x, y)  // 输出: 5 3

使用 inout 参数的场景 #

inout 参数最适用于需要在函数内直接修改调用者传入的变量的场景。比如,对数组进行排序或对变量进行交换等操作。

如何使用 inout 参数 #

  1. 使用 inout 关键字标记参数。
  2. 在调用函数时使用 & 操作符传递变量。

下面是一些使用 inout 参数的示例:

示例 1:交换两个变量的值 #
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

var x = 5
var y = 10
print("Before swap: x = \(x), y = \(y)")  // 输出: Before swap: x = 5, y = 10

swapTwoInts(&x, &y)
print("After swap: x = \(x), y = \(y)")  // 输出: After swap: x = 10, y = 5

在这个例子中,swapTwoInts 函数接受两个 inout 参数。由于使用了 inout,函数内部对 ab 的修改会反映到调用者传入的 xy 上。

示例 2:对数组进行排序 #

你可以使用 inout 参数实现一个简单的排序函数(如冒泡排序)。

func bubbleSort(_ array: inout [Int]) {
    let n = array.count
    for i in 0..<n {
        for j in 1..<n-i {
            if array[j-1] > array[j] {
                let temp = array[j-1]
                array[j-1] = array[j]
                array[j] = temp
            }
        }
    }
}

var numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]
print("Before sort: \(numbers)")  // 输出: Before sort: [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]

bubbleSort(&numbers)
print("After sort: \(numbers)")  // 输出: After sort: [1, 1, 2, 3, 3, 4, 5, 5, 5, 6, 9]

在这个例子中,bubbleSort 函数接受一个 inout 数组。数组中的元素在函数内部被排序,并且调用者变量 numbers 内容也会反映排序结果。

注意事项 #

  1. 声明和调用:

    • inout 参数在函数定义时使用 inout 关键字。
    • 在函数调用时,传入的变量前需要加上 & 符号。
  2. 传递变量:

    • 只能传递变量,不能传递常量、字面量或表达式。
    var a = 10
    var b = 20
    swapTwoInts(&a, &b)   // 合法
    // swapTwoInts(&10, &20) // 非法:不能传递字面量
    
  3. 值类型:

    • inout 参数主要用于值类型(如结构体和枚举)。引用类型(如类)的引用本身会传递给函数,因此无需使用 inout 进行修改。
  4. 避免混淆:

    • inout 参数在函数内被强制转为变量(var),意味着它的修改是直接的。例如,不能对一个 inout 变量同时多次传递。
    var value = 100
    
    // 多次传递同一个 `inout` 参数会引起编译错误
    // swapTwoInts(&value, &value) // 非法
    

小结 #

inout 参数用于让函数可以直接修改传入的变量。它通过 & 符号传递变量,并且修改在函数执行结束后会保留。这个特性能简化某些需要在函数内部修改参数的操作,增强代码的可读性和可维护性。在使用时需要注意它的限制和注意事项,以避免引起困扰。

总结 #

Swift 中的函数是功能强大且灵活的。你可以通过各种方式定义、调用和传递函数,还可以利用函数来构建简洁、高效、模块化的代码结构。希望以上内容能够帮助你全面理解并熟练掌握 Swift 中的函数使用。

闭包的函数体部分由关键字 in 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

协议 Protocol #

在 Swift 中,协议(Protocol)是一种为方法、属性和其他需求定义蓝图的方式。协议本身并不实现这些需求,它只是向遵循协议的类型(类、结构体、枚举等)规定这些需求应该存在并实现。

协议用来定义一组接口,这些接口可以被多个不同类型遵循并实现,从而实现多态性和接口分离、灵活扩展的目标。

定义协议 #

你可以用 protocol 关键字来定义协议,协议可以包含方法、属性、下标等需求。

protocol Drawable {
    func draw()
}

protocol Identifiable {
    var id: String { get }
}

在这个例子中,Drawable 协议要求任何遵循它的类型都必须实现一个 draw 方法。Identifiable 协议要求任何遵循它的类型都必须有一个 id 属性。

遵循协议 #

类、结构体或枚举遵循协议时,需要在类型声明中使用 :协议名称 来表明遵循某个协议,并实现协议中定义的所有需求。

struct Circle: Drawable {
    func draw() {
        print("Drawing a circle")
    }
}

class User: Identifiable {
    var id: String

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

在这个例子中,Circle 结构体遵循了 Drawable 协议,实现了 draw 方法。User 类遵循了 Identifiable 协议,实现了 id 属性。

多个协议 #

Swift 允许一个类型同时遵循多个协议,以逗号分隔。

struct UserProfile: Identifiable, Drawable {
    var id: String

    func draw() {
        print("Drawing user profile with id: \(id)")
    }
}

协议的属性要求 #

协议可以要求属性具有特定的 getter 和 setter。如果属性是只读的,只需要指定 { get };如果属性是可读写的,则需要指定 { get set }

protocol FullyNamed {
    var fullName: String { get }
}

protocol Person {
    var name: String { get set }
    var age: Int { get set }
}

协议中的 Mutating 方法 #

当协议中的方法可能改变类型的实例时,需要在方法前加上 mutating 关键字,通常用于结构体和枚举。

protocol Toggleable {
    mutating func toggle()
}

enum LightSwitch: Toggleable {
    case off, on

    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

协议的继承 #

协议能继承一个或多个其他协议,要求实现多个协议的需求。

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

protocol Person: Named, Aged { }

Person 协议继承了 NamedAged 协议,遵循 Person 协议的类型需要同时实现 NamedAged 的需求。

协议组合 #

协议组合允许你将多个协议组合成一个单一的要求,使用特殊的 & 语法。

func describe<T: Named & Aged>(person: T) {
    print("\(person.name) is \(person.age) years old.")
}

协议扩展 #

协议扩展允许你为协议添加默认实现,使得遵循协议的类型可以仅实现一些而非全部的需求。

protocol Greetable {
    func greet()
}

extension Greetable {
    func greet() {
        print("Hello!")
    }
}

struct Person: Greetable {}

let person = Person()
person.greet() // 输出: Hello!

关联类型 #

协议可以包含关联类型要求,通过 associatedtype 关键字定义,用于在协议中使用泛型。

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get }
}

struct Stack<Element>: Container {
    var items: [Element] = []
    
    var count: Int {
        return items.count
    }
    
    subscript(i: Int) -> Element {
        return items[i]
    }
}

在这个例子中,Container 协议包含一个关联类型 ItemStack 结构体遵循了 Container 协议,并将 Item 关联类型具体化为 Element

小结 #

  • 定义协议:使用 protocol 关键字定义包含方法、属性、下标等需求的协议。
  • 遵循协议:类型通过在声明中使用 :协议名称 表明遵循协议,并实现所有的需求。
  • 多个协议:类型可以同时遵循多个协议。
  • 属性要求:协议可以要求属性具有特定的 getter 和 setter。
  • Mutating 方法:用于需要改变自身实例的方法,通常用于结构体和枚举。
  • 协议继承:协议可以继承一个或多个其他协议。
  • 协议组合:使用 & 将多个协议组合成一个单一要求。
  • 协议扩展:为协议提供默认实现。
  • 关联类型:使用 associatedtype 在协议中定义泛型。

协议是 Swift 强大而灵活的类型系统的一部分,广泛用于定义接口并实现多态性、灵活扩展和接口分离。

结构体 #

在 Swift 中,结构体(struct)是一种非常有用的数据类型。它允许你将一组相关的值组合在一起,并定义它们的行为。结构体在 Swift 中是值类型,与类(class)的引用类型有显著的区别。

结构体的定义 #

定义一个结构体使用 struct 关键字,结构体中可以包含属性(存储属性和计算属性)和方法。

struct Resolution {
    var width: Int
    var height: Int
}

在这个例子中,Resolution 结构体包含了两个存储属性 widthheight

创建结构体实例 #

你可以通过成员初始化器来创建结构体实例:

let res = Resolution(width: 1920, height: 1080)

在这个例子中,res 是一个新的 Resolution 实例,其 width 属性为 1920,height 属性为 1080。

访问和修改属性 #

可以通过点语法来访问和修改结构体的属性:

print("The resolution width is \(res.width)")

// 如果需要修改,需要将实例声明为 var
var modifiableRes = res
modifiableRes.width = 1280
print("The new resolution width is \(modifiableRes.width)")

方法 #

结构体可以包含方法,这些方法可以使用和修改结构体的属性。

struct Resolution {
    var width: Int
    var height: Int
    
    func display() {
        print("Resolution: \(width)x\(height)")
    }
    
    mutating func resize(toWidth width: Int, andHeight height: Int) {
        self.width = width
        self.height = height
    }
}

var res = Resolution(width: 1920, height: 1080)
res.display() // 输出: Resolution: 1920x1080
res.resize(toWidth: 1280, andHeight: 720)
res.display() // 输出: Resolution: 1280x720

在这个例子中,display() 方法用来打印分辨率,而 resize(toWidth:andHeight:) 方法是一个 mutating 方法,它可以修改结构体的属性。

构造器 #

当你定义一个结构体时,Swift 会自动提供一个成员初始化方法,你也可以自定义构造器:

struct Resolution {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

值类型 #

结构体是值类型,这意味着结构体的实例在赋值和传递过程中会被拷贝:

let res1 = Resolution(width: 1920, height: 1080)
var res2 = res1
res2.width = 1280

print("res1 width: \(res1.width)") // 输出: res1 width: 1920
print("res2 width: \(res2.width)") // 输出: res2 width: 1280

在这个例子中,res1res2 是两个独立的实例,对 res2 的修改不会影响 res1

结构体与类的对比 #

结构体的特点: #

  1. 值类型:结构体在赋值和传递时总是被拷贝。
  2. 不需要继承:结构体不支持继承。
  3. 自动成员初始化器:Swift 自动为结构体提供成员初始化器。
  4. 更适合表示简单的数据:例如几何图形、坐标、范围等。

类的特点: #

  1. 引用类型:类在赋值和传递时总是引用同一个实例。
  2. 支持继承:类可以继承自其他类,并且可以使用多态。
  3. 需要手动定义初始化方法:类通常需要显式定义初始化方法。
  4. 更适合表示需要共享状态的数据:例如用户对象、单例模式等。

小结 #

结构体在 Swift 中是一种灵活强大的值类型,适用于表示简单的数据结构。它们使用方便,适合那些不需要继承和共享状态的情况。

  1. 定义结构体 通过 struct 关键字。
  2. 存储属性和计算属性:可以包含存储真实数据的属性,也可以包含计算属性。
  3. 方法:可以包含操作和修改结构体属性的方法。
  4. 值类型 赋值和传递时会被拷贝。
  5. 构造器 可以使用默认的成员初始化方法或自定义构造方法。

理解结构体和类之间的区别以及何时使用它们,对于开发高效、健壮的 Swift 应用至关重要。

#

关键字 self #

在 Swift 中,关键字 self 有多种用途,主要用于引用当前实例、属性或方法。下面是 self 的几种常见用途:

1. 引用当前实例 #

  • 在类或结构体的方法中,可以使用 self 来引用当前实例。
class Person {
    var name: String
    
    init(name: String) {
        self.name = name
    }
    
    func greet() {
        print("Hello, my name is \(self.name)")
    }
}

2. 区分属性和参数 #

  • 当实例的属性名与参数名相同时,可以使用 self 来区分它们。
class Point {
    var x: Int
    var y: Int
    
    init(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
    
    func setCoordinates(x: Int, y: Int) {
        self.x = x
        self.y = y
    }
}

3. 避免闭包循环引用 #

  • 在闭包内部引用当前实例时,可以使用 self 来避免循环引用。
class MyClass {
    var closure: (() -> Void)?
    
    func setupClosure() {
        self.closure = {
            self.doSomething()
        }
    }
    
    func doSomething() {
        print("Doing something")
    }
}

4. 在闭包中弱化引用 #

  • 在闭包中捕获 self 并使用弱引用来避免循环引用。
class SomeClass {
    var closure: (() -> Void)?
    
    func setupClosure() {
        self.closure = { [weak self] in
            self?.doSomething()
        }
    }
    
    func doSomething() {
        print("Doing something")
    }
}

总结 #

  • self 是一个特殊的关键字,用于引用当前实例、区分属性和参数、避免循环引用以及在闭包内部使用。
  • 在大多数情况下,Swift 会自动推断 self 的引用,因此在许多情况下,可以省略 self 关键字。

访问控制 #

Access Control

  • open
  • public
  • internal
  • fileprivate
  • private

条件编译 #

通过条件编译来控制哪些代码在那些情况下需要编译,而哪些代码不需要

#if <condition>

	举例:#if os(iOS) || os(watchOS) || os(tvOS)

#elseif <condition>

#else

#endif

其中 #elseif#else 是可选的。

断言 #

assert 使用断言进行调试 #

只在 debug 下生效

precondition 强制先决条件 #

debug 和 release 都生效

泛型 #

泛型(Generics)是 Swift 语言中的强大特性,它允许你编写灵活且可重用的代码。通过使用泛型,你可以让函数、方法、类、结构体和枚举适用于任何类型,而不需要重复编写相同的代码。泛型有助于编写更加抽象和通用的代码,从而提高代码的复用性和类型安全性。

泛型在 Swift 中通过在尖括号 <T> 中声明类型参数来实现,以下是引入泛型的几个基本内容:

泛型函数 #

泛型函数允许你定义一个独立于任何特定类型的函数。例如,下面是一个非泛型的函数:

func swapTwoInts(_ a: inout Int, _ b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}

使用泛型后,可以将函数改写为处理任意类型:

func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

你可以使用这个泛型函数来交换任意类型的两个值:

var int1 = 1
var int2 = 2
swapTwoValues(&int1, &int2) // int1 现在是 2,int2 是 1

var str1 = "Hello"
var str2 = "World"
swapTwoValues(&str1, &str2) // str1 现在是 "World",str2 是 "Hello"

泛型类型 #

除了泛型函数,你还可以定义泛型类型,包括类、结构体和枚举。以下是一个使用泛型的栈(Stack)结构体示例:

struct Stack<Element> {
    private var items: [Element] = []

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element? {
        return items.popLast()
    }

    func top() -> Element? {
        return items.last
    }

    var isEmpty: Bool {
        return items.isEmpty
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.top()!) // 输出 2

var stringStack = Stack<String>()
stringStack.push("Hello")
stringStack.push("World")
print(stringStack.top()!) // 输出 "World"

泛型约束 #

你可以在泛型类型或函数上添加约束,要求类型参数必须符合特定的协议或父类。例如,下面是一个要求类型参数遵循 Equatable 协议的函数:

func findIndex<T: Equatable>(of valueToFind: T, in array: [T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

let numbers = [1, 2, 3, 4, 5]
if let index = findIndex(of: 3, in: numbers) {
    print("Index of 3 in numbers is \(index)") // 输出 "Index of 3 in numbers is 2"
}

如果要为泛型添加多个约束,可以使用 where 关键字:

func someFunction<T, U>(param1: T, param2: U) where T: Equatable, U: Comparable {  
    // 在这个函数中,T 需要遵循 Equatable 协议,U 需要遵循 Comparable 协议  
}

关联类型 #

协议可以有一个或多个关联类型作为其定义的一部分。关联类型是在协议内部定义的占位符名称,表示协议中使用到的某种类型,直到被采纳协议的类型具体化。例如:

protocol Container {
    associatedtype ItemType
    mutating func append(_ item: ItemType)
    var count: Int { get }
    subscript(i: Int) -> ItemType { get }
}

struct IntStack: Container {
    // existing implementation of IntStack
    var items = [Int]()
    mutating func push(_ item: Int) {
        items.append(item)
    }
    mutating func pop() -> Int {
        return items.removeLast()
    }

    // conformance to the Container protocol
    typealias ItemType = Int

    mutating func append(_ item: Int) {
        self.push(item)
    }

    var count: Int {
        return items.count
    }

    subscript(i: Int) -> Int {
        return items[i]
    }
}

总结 #

Swift 的泛型提供了一种强大且灵活的方式来编写可重用和类型安全的代码。通过泛型,你可以定义独立于任何具体类型的函数、方法、类、结构体和枚举,并在需要时进行类型约束,以确保代码的准确性和健壮性。理解和掌握泛型是编写高效、通用、高度可重用代码的关键。

宏(Macro) #

宏在编译时执行,可以在代码中生成或变换其他代码。宏在 Swift 语言中的应用能够帮助开发者在编译时进行一些高级的代码操作,从而使开发流程更加高效和简洁。

Preview #

在 Xcode 15 和 Swift 5.9 中,Apple 引入了这一宏语法,以简化视图预览的配置,让开发者能够以更直观和便捷的方式设置 SwiftUI 视图的预览。通过使用 #Preview 宏,开发者可以直接用这种方式创建视图的预览,而不需要像以前那样遵循更复杂的预览提供器(@PreviewProvider) 规范。这让代码更为简洁,同时提高了在 Xcode 中设计和调试用户界面的效率。

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, world!")
            .padding()
    }
}

#Preview {
    ContentView()
}

这个示例代码定义了一个简单的包含 “Hello, world!” 文本的视图,并使用 #Preview 来告诉 Xcode 在预览区域中展示这个视图。通过这种方式,开发者可以立即看到视图的外观并进行相应的调整,而不需编译和运行整个应用程序。

available #

if #available(iOS 14, *) {
    // 代码只在 iOS 14 及以上版本运行
} else {
    // 其他版本的替代实现代码
}

属性 Properties #

getset #

在 Swift 中,getset 是用于定义计算属性的访问器。计算属性是一种特殊的属性,它没有直接存储值,而是通过一个方法来计算其值。

get 访问器:

  • 用于获取计算属性的值。
  • 当你访问计算属性时,get 访问器会被调用。
  • get 访问器必须返回一个与属性类型匹配的值。

set 访问器:

  • 用于设置计算属性的值。
  • 当你给计算属性赋值时,set 访问器会被调用。
  • set 访问器接收一个新值作为参数,并将其用于更新计算属性的内部状态。

示例:

class Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        get {
            return width * height
        }
        set {
            width = sqrt(newValue)
            height = sqrt(newValue)
        }
    }

    init(width: Double, height: Double) {
        self.width = width
        self.height = height
    }
}

let rectangle = Rectangle(width: 4.0, height: 3.0)

print(rectangle.area) // 输出:12.0

rectangle.area = 25.0 // 设置 area 为 25.0

print(rectangle.width) // 输出:5.0
print(rectangle.height) // 输出:5.0

在这个示例中,area 是一个计算属性,它没有直接存储值,而是通过 get 访问器来计算其值。当我们访问 rectangle.area 时,get 访问器会被调用,并返回 width * height 的结果。

set 访问器用于设置 area 的值。当我们给 rectangle.area 赋值时,set 访问器会被调用,并接收一个新值作为参数。在这个示例中,我们使用新值来更新 widthheight,以确保 area 的值始终等于 width * height

注意:

  • 你可以省略 get 访问器的关键字,因为它默认存在。
  • set 访问器必须有一个名为 newValue 的参数,用于接收新值。
  • 计算属性不能直接存储值,它们的值始终是通过 get 访问器计算出来的。

你还可以为 set 访问器中的参数选择其他名称,但需要遵循以下步骤:

示例:

class Circle {
    var radius: Double

    var area: Double {
        get {
            return Double.pi * radius * radius
        }
        set(newAreaValue) { // 使用 newAreaValue 作为参数名
            radius = sqrt(newAreaValue / Double.pi) // 使用 newAreaValue 访问参数值
        }
    }

    init(radius: Double) {
        self.radius = radius
    }
}

let circle = Circle(radius: 2.0)
circle.area = 12.56 // 设置 area 为 12.56
print(circle.radius) // 输出:2.0

在这个例子中,我们使用 newAreaValue 作为 set 访问器中的参数名,并在访问器内部使用它来计算新的 radius 值。

注意:

  • 虽然你可以使用其他参数名,但 newValue 是最常用的,因为它清晰地表明了这个参数代表的是要设置的新值。
  • 如果你选择使用其他参数名,请确保它易于理解和维护。

表达式 #

key-path 表达式 #

Key-Path Expression

键路径(Key Path)是 Swift 语言中的一种强大特性,它提供了一种类型安全的方式来引用类型(如结构体或类)中的属性。键路径使得我们能够以一种间接的、动态的、且类型安全的方式访问和修改属性。

基本概念 #

键路径基本上是属性的引用,允许你间接地访问和修改属性,而不需要直接写出属性名。它们使用反斜杠(\) 开头作为标识符。

使用示例 #

定义和使用键路径 #

考虑一个简单的结构体 Person

struct Person {
    var name: String
    var age: Int
}

你可以定义一个指向 Person 结构体的 nameage 属性的键路径:

let nameKeyPath = \Person.name
let ageKeyPath = \Person.age

这两个键路径分别指向 Personnameage 属性。

通过键路径访问和修改属性 #

一旦你定义了键路径,就可以用它们来访问和修改属性值了:

var person = Person(name: "Alice", age: 30)

let personName = person[keyPath: nameKeyPath]  // "Alice"
let personAge = person[keyPath: ageKeyPath]    // 30

person[keyPath: nameKeyPath] = "Bob"
person[keyPath: ageKeyPath] = 35

print(person)  // 输出: Person(name: "Bob", age: 35)

在这个例子中,键路径 nameKeyPathageKeyPath 被用来读取和修改 Person 实例的 nameage 属性。

键路径的层级访问 #

键路径不仅可以访问类型的直接属性,还可以访问嵌套属性。例如:

struct Address {
    var city: String
    var zipcode: String
}

struct Person {
    var name: String
    var age: Int
    var address: Address
}

let addressKeyPath = \Person.address
let cityKeyPath = \Person.address.city

let person = Person(name: "Alice", age: 30, address: Address(city: "New York", zipcode: "10001"))

let personAddress = person[keyPath: addressKeyPath]  // Address(city: "New York", zipcode: "10001")
let personCity = person[keyPath: cityKeyPath]        // "New York"

在这个例子中,addressKeyPath 指向 Person 类型的 address 属性,而 cityKeyPath 则进一步引用了 address 属性中的 city 属性。

类型安全 #

键路径是严格类型安全的。如果你尝试使用不兼容的键路径,编译器将会报错:

let nameKeyPath = \Person.name
let person = Person(name: "Alice", age: 30)

// 错误:类型不匹配
let city = person[keyPath: nameKeyPath] // 编译错误

编译器会检查键路径和实例的类型是否匹配,不匹配则报错。

WritableKeyPath 和 KeyPath #

Swift 中有两种主要的键路径类型:

  • KeyPath:只读键路径,可以用于访问属性,但不能修改属性。
  • WritableKeyPath:可写键路径,可以用于访问和修改属性。

还有其他更为细分的键路径类型,比如:

  • PartialKeyPath<Root>:可以指向某个具体类型中的某个属性,但不指定属性的类型。
  • ReferenceWritableKeyPath<Root, Value>:适用于类实例的可写键路径。

扩展应用 #

键路径在 Swift 的其他特性中也有广泛应用,如:

  • KVO(键值观察,Key-Value Observing):在观察某些属性的变化时使用键路径。
  • SwiftUI:键路径在声明式 UI 结构中广泛使用,如 @Binding@StateObject 等属性包装器。

小结 #

键路径是 Swift 中一种极其灵活且类型安全的机制,提供了间接访问和修改属性的能力。它在 SwiftUI 和其他 Swift 特性中有广泛应用。通过理解和熟练使用键路径,可以编写出更加灵活和可扩展的代码。希望这个解释能帮助你更好地理解和运用 Swift 中的键路径。

关键字 #

mutating #

在 Swift 中,mutating 关键字用于标记能改变结构体(struct)或枚举(enum)实例中属性的方法。Swift 中的结构体和枚举是值类型,而类(class)是引用类型。值类型的实例方法默认情况下不能修改实例的属性,因为它们是被拷贝的。为了允许修改,需要在方法前添加 mutating 关键字。

结构体中的 mutating 方法 #

当你在结构体中定义一个方法,并且该方法需要修改结构体的属性时,就需要将该方法标记为 mutating。例如:

struct Point {
    var x: Int
    var y: Int

    mutating func moveBy(dx: Int, dy: Int) {
        x += dx
        y += dy
    }
}

var point = Point(x: 0, y: 0)
point.moveBy(dx: 5, dy: 3)
print("Point is now at (\(point.x), \(point.y))") // 输出: Point is now at (5, 3)

在这个例子中,moveBy(dx:dy:) 方法修改 Point 结构体的 xy 属性。由于这是一个 mutating 方法,因此我们可以在方法中修改这些属性。

枚举中的 mutating 方法 #

同样的,枚举中的方法如果修改实例(比如切换到另一个枚举值),也必须标记为 mutating

enum LightSwitch {
    case off, on

    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}

var lightSwitch = LightSwitch.off
lightSwitch.toggle() // 使用 mutating 方法来切换状态
print(lightSwitch) // 输出: on

在这个例子中,toggle 方法改变了 LightSwitch 实例的状态,因为它是一个 mutating 方法。

为什么需要 mutating #

mutating 关键字存在的原因在于结构体和枚举是值类型。在 Swift 中,当你改变一个值类型的属性时,本质上是在改变值的副本。为了使得方法可以合法地改变实例内部的状态,必须显式地标记这个方法为 mutating。这能够明确方法可能会改变实例的自身属性,有助于代码的可读性和安全性。

小结 #

  • mutating 关键字 用于标记那些修改结构体或枚举实例中属性的方法。
  • 值类型 结构体和枚举是值类型,因此默认情况下,其方法不能改变实例自身的状态。
  • 修改属性 标记为 mutating 的方法可以修改实例的属性或改变实例本身(例如枚举切换值)。

例子总结 #

定义一个可以修改内部属性的方法:

struct MyStruct {
    var property: Int

    mutating func changeProperty(newValue: Int) {
        property = newValue
    }
}

enum MyEnum {
    case firstCase, secondCase

    mutating func toggleCase() {
        switch self {
        case .firstCase:
            self = .secondCase
        case .secondCase:
            self = .firstCase
        }
    }
}

通过使用 mutating 关键字,结构体和枚举中的方法可以成功修改实例的内部状态。

some #

在 Swift 中,some 关键字用于声明一个“存在类型”(existential type),这是一种抽象类型,可以封装任意类型的值,并保证它符合某个特定的协议。具体来说,some 关键字通常与协议类型一起使用,用于定义一种限制,表示返回的类型实现了某个协议,但具体是什么类型不重要。最常见的场景是在 SwiftUI 中声明视图的返回类型。在 Swift 5.1 引入的功能中,它被称为“Opaque Return Types”(不透明返回类型)。

下面是一些使用 some 关键字的例子和解释:

基本用法 #

假设你有一个协议 Shape

protocol Shape {
    func area() -> Double
}

然后你有两个遵循 Shape 协议的结构体:

struct Circle: Shape {
    let radius: Double
    
    func area() -> Double {
        return .pi * radius * radius
    }
}

struct Rectangle: Shape {
    let width: Double
    let height: Double
    
    func area() -> Double {
        return width * height
    }
}

现在,你可以使用 some 关键字定义一个函数,该函数返回实现了 Shape 协议的某种类型:

func createShape() -> some Shape {
    return Circle(radius: 5)
}

这种方式声明的函数返回的具体类型是模糊的,但可以保证返回的类型遵循 Shape 协议。这让编译器在编译时可以进行更多优化,同时让你的 API 保持简洁和抽象。

在 SwiftUI 中的用法 #

在 SwiftUI 中,你会经常看到 some View,用于定义视图的返回类型:

import SwiftUI

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
    }
}

这里的 some View 表明 body 属性返回的是某种类型,它实现了 View 协议,但具体是什么类型并不重要。因为 SwiftUI 中的视图构建方式通常会涉及到大量的泛型和类型推断,使用 some 关键字可以简化类型声明,同时又能保证代码的类型安全性和性能。

使用限制 #

  1. 一致的返回类型:使用 some 声明的函数,必须在所有代码路径中返回相同的具体类型。例如,下面的代码是无效的:

    func createShape(basedOn value: Int) -> some Shape {
        if value > 0 {
            return Circle(radius: 5)
        } else {
            return Rectangle(width: 3, height: 4)
        }
    }
    

    编译器会报错,因为 createShape 在不同的条件下返回了不同的类型。要解决这个问题,必须确保返回的类型保持一致。

  2. 单一实现类型some 只能用于返回单一具体类型,不能返回协议类型的集合。例如,不能返回 [some Shape]

总结 #

some 关键字提供了一种非常强大和类型安全的方法来隐藏 API 的具体实现细节,对外暴露一个符合协议的抽象接口。这使得 API 更加灵活和易于扩展,同时提高了编译器的优化能力。在 SwiftUI 中的广泛使用也使其成为日常 Swift 开发中一个非常重要的概念。

as #

在 Swift 中,as 关键字用于类型转换,它有几种不同的用法,根据具体情况可以转换值的类型或检查类型的兼容性。以下是 as 关键字的几种常见用法:

1. 向下类型转换(Downcasting) #

as?as!

  • 可选的向下类型转换 as?as? 尝试将对象转换为某个特定类型,如果转换失败,则返回 nil。这种方式通常用于安全类型转换。

    let someObject: Any = "Hello, Swift"
    if let string = someObject as? String {
        print("Conversion successful: \(string)")
    } else {
        print("Conversion failed")
    }
    
  • 强制的向下类型转换 as!as! 强制将对象转换为某个特定类型,如果转换失败会导致运行时错误(crash)。这种方式适用于你明确知道转换一定会成功的情况。

    let someObject: Any = "Hello, Swift"
    let string = someObject as! String
    print("Conversion successful: \(string)")
    

2. 向上类型转换(Upcasting) #

在 Swift 中,向上类型转换是隐式的,无需使用 as 关键字。向上类型转换是将某个特定类型的对象转换为其父类或协议类型。这种转换总是安全的。

3. 协议类型转换 #

当将对象转换为某个协议类型时,可以使用 as 关键字:

如果类型转换一定会成功(例如接口实现转换),可以直接使用 as,不需要可选或强制类型转换。

protocol Greetable {
    func greet()
}

class Person: Greetable {
    func greet() {
        print("Hello!")
    }
}

let person = Person()
let greetable: Greetable = person as Greetable
greetable.greet()

4. 通用类型转换 #

在某些情况下,需要将一个常量或变量转换为某个类型,但是确定转换一定成功时,可以直接使用 as

let value: Int = 42
let doubleValue = value as Double  // 编译错误:无法直接转换

上面的例子会编译错误,因为直接将 Int 转换为 Double 是不允许的,需要显式进行类型转换。

例如:

let intValue: Int = 42
let doubleValue = Double(intValue)
print(doubleValue)

这种方式下,类型转换是显式的且安全。

5. 对类型注解的使用 #

as 还可以用于类型注解,在初始化变量或常量时,明确指定它们的类型:

let numberAsString = "123"
let number = Int(numberAsString) as Int?

通过这些示例和解释,我们可以清楚地看到 as 关键字在 Swift 中的各种用途和作用。它在类型系统中起到了转换和兼容检查的重要作用。

associatedtype #

在 Swift 中,associatedtype 关键字用于定义协议中的关联类型。关联类型为协议声明了一个占位符类型,该具体类型在实现协议时由具体类型来确定。这使得协议更加通用和灵活,因为它们能够处理未指定的类型。

通过使用 associatedtype,你可以在协议中定义类型参数,而无需在协议声明时提前决定这些类型具体是什么。这样可以使协议更适应于不同的类型组合,从而提供强大的抽象能力。

关联类型的定义和使用 #

以下是一些使用 associatedtype 关键字的具体示例和解释:

1. 定义一个协议,其中包含一个关联类型 #
protocol Container {
    associatedtype Item
    func addItem(_ item: Item)
    func getItem(at index: Int) -> Item
}

在这个例子中,我们定义了一个 Container 协议,它具有一个关联类型 Item。这个协议声明了两个方法:

  • addItem(_:):接受一个 Item 类型的参数。
  • getItem(at:):返回一个 Item 类型的值。
2. 实现协议并指定关联类型 #

实体类型在实现协议时需要声明 associatedtype 的具体类型:

struct IntContainer: Container {
    typealias Item = Int
    private var items: [Int] = []
    
    func addItem(_ item: Int) {
        items.append(item)
    }
    
    func getItem(at index: Int) -> Int {
        return items[index]
    }
}

IntContainer 中,我们将 Container 协议的 Item 关联类型指定为 Int。 我们还实现了协议中定义的方法来添加和获取 Int 类型的元素。

3. 使用泛型和关联类型 #

可以使用泛型来使实现更加通用:

struct GenericContainer<T>: Container {
    typealias Item = T
    private var items: [T] = []
    
    func addItem(_ item: T) {
        items.append(item)
    }
    
    func getItem(at index: Int) -> T {
        return items[index]
    }
}

在这个例子中,GenericContainer 是一个泛型结构体,它可以存储任何类型的元素。通过将 associatedtype 设为泛型参数 T,返回和接受的类型是由使用 GenericContainer 时决定的。

关联类型的默认实现 #

虽然 associatedtype 不能具体地提供默认实现,但可以通过扩展提供协议方法的默认行为:

extension Container where Item == String {
    func displayAllItems() {
        // 只在 Item 是 String 时可用的方法
        for i in 0..<items.count {
            print("Item \(i): \(getItem(at: i))")
        }
    }
}

这个例子提供了一个基于条件关联类型(即 ItemString 类型时的扩展)中加入了一个新的方法。

结合使用协议和关联类型 #

示例:定义一个可迭代的容器

protocol IterableContainer {
    associatedtype Item
    func makeIterator() -> AnyIterator<Item>
}

struct StringContainer: IterableContainer {
    typealias Item = String
    private var items: [String] = ["one", "two", "three"]
    
    func makeIterator() -> AnyIterator<String> {
        var index = 0
        return AnyIterator {
            if index < self.items.count {
                let item = self.items[index]
                index += 1
                return item
            } else {
                return nil
            }
        }
    }
}

let sc = StringContainer()
var iterator = sc.makeIterator()

while let item = iterator.next() {
    print(item)
}

在这个示例中,IterableContainer 协议定义了一个关联类型 Item 以及一个生成迭代器的方法。StringContainer 实现了该协议,并在返回的迭代器中使用了 Item 类型。

通过这些示例,你应该能够理解 associatedtype 的基本概念和用法。它在 Swift 的协议中提供了强大的抽象能力,使得协议可以处理泛型和不同类型的组合,而无需预先指定具体类型。

typealias #

typealias 是 Swift 语言中的一个关键字,用于为已有类型提供一个新的名字。它可以使代码更简洁、更具可读性,并有助于重用复杂类型定义。使用 typealias 你可以为任何类型(包括基本数据类型、集合类型、函数类型、泛型类型等)创建自定义名称。

基本用法 #

为现有类型创建别名 #

假设你有一个复杂的类型,你可以为它创建一个简短的别名:

typealias Coordinates = (x: Double, y: Double, z: Double)

let point: Coordinates = (x: 3.0, y: 4.0, z: 5.0)
print(point)  // 输出: (x: 3.0, y: 4.0, z: 5.0)

在这个例子中,我们为一个三维坐标类型创建了别名 Coordinates,使得代码更易读和易维护。

为函数类型创建别名 #

函数类型有时候可能很复杂,为它们创建别名可以简化函数声明:

typealias CompletionHandler = (Bool, String) -> Void

func loadData(completion: CompletionHandler) {
    // 执行一些操作
    completion(true, "Data loaded")
}

loadData { success, message in
    print("Success: \(success), Message: \(message)")
}

在这个例子中,我们创建了一个叫做 CompletionHandler 的别名,使得函数 loadData 的定义更简洁。

为集合类型创建别名 #

你可以为各种集合类型创建别名,特别是当这些类型很复杂时:

typealias StringDictionary = [String: String]

var capitalCities: StringDictionary = ["France": "Paris", "Japan": "Tokyo"]
print(capitalCities)  // 输出: ["France": "Paris", "Japan": "Tokyo"]

使用 typealias 简化泛型类型 #

尤其是在处理复杂的泛型类型时,typealias 可以显著简化代码:

typealias StringIntDictionary = Dictionary<String, Int>

var scores: StringIntDictionary = ["Alice": 90, "Bob": 85]
print(scores)  // 输出: ["Alice": 90, "Bob": 85]

作为协议中的关联类型 #

在前一段讨论使用 associatedtype 与协议中的关联类型时,我们提到可以使用 typealias 为具体类型命名:

protocol Container {
    associatedtype Item
    func addItem(_ item: Item)
    func getItem(at index: Int) -> Item
}

struct IntContainer: Container {
    typealias Item = Int
    private var items = [Int]()
    
    mutating func addItem(_ item: Int) {
        items.append(item)
    }
    
    func getItem(at index: Int) -> Int {
        return items[index]
    }
}

其他场景 #

为特定类型的元组创建别名 #
typealias NameAndAge = (name: String, age: Int)

let person: NameAndAge = (name: "John", age: 30)
print(person)  // 输出: (name: "John", age: 30)
为闭包类型创建别名 #
typealias ActionHandler = (String) -> Void

func performAction(handler: ActionHandler) {
    handler("Action performed")
}

performAction { message in
    print(message)
}
与泛型一起使用 #
struct Stack<Element> {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element? {
        return items.popLast()
    }
}

typealias IntStack = Stack<Int>

var stack = IntStack()
stack.push(3)
stack.push(5)
print(stack.pop()!)  // 输出: 5

总结 #

typealias 是一个非常有用的工具,使得代码更加简洁和易读。以下是一些常见的应用场景:

  1. 简化复杂类型声明:尤其是函数类型和泛型类型。
  2. 改进代码可读性:使用语义化的别名。
  3. 支持协议和关联类型:在协议中对关联类型进行重命名。

通过了解 typealias 的使用,你可以编写更具可读性、更易维护的 Swift 代码。

where #

在Swift中,where 关键词主要用于添加约束条件或过滤条件。它可以在多种情况下使用,以下是一些常见的用法:

1. 在泛型约束中: #

func someFunction<T>(param: T) where T: Comparable {
    // 只有当 T 遵循 Comparable 协议时,这个函数才能被调用
}

2. 在协议扩展中: #

extension Collection where Element: Equatable {
    // 这个扩展只适用于元素类型遵循 Equatable 协议的集合
}

3. 在 switch 语句的 case 中: #

switch someValue {
case let x where x > 0:
    print("Positive")
case let x where x < 0:
    print("Negative")
default:
    print("Zero")
}

4. 在 for-in 循环中: #

for i in 1...100 where i % 2 == 0 {
    print(i) // 只打印偶数
}

5. 在可选绑定中: #

if let value = optionalValue where value > 10 {
    // 只有当 optionalValue 不为 nil 且大于 10 时执行
}

6. 在关联类型声明中: #

protocol Container {
    associatedtype Item where Item: Equatable
}

小结 #

总的来说,where 关键词允许你在各种上下文中添加额外的条件或约束,使得代码更加灵活和精确。它帮助你更好地控制类型、值的范围,以及在何种条件下执行特定的代码。

extension #

在 Swift 中,extension 关键词用于为已有的类、结构体、枚举和协议类型添加新功能。这使得你可以在不访问原始源码的情况下扩展类型的行为。Extensions 非常强大和灵活,它们可以:

  • 为现有类型添加计算属性和计算静态属性
  • 定义实例方法和类型方法
  • 提供新的构造器
  • 添加下标
  • 定义和使用新的嵌套类型
  • 使现有类型遵循协议

下面是关于扩展(extension)的一些详细示例和解释:

基本用法 #

1. 添加计算属性 #

你可以通过扩展添加计算属性,但无法添加存储属性:

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}

let distance = 5.0.km
print(distance)  // 输出: 5000.0
2. 添加实例方法和类型方法 #

可以通过扩展添加新的实例方法和类型方法:

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

3.repetitions {
    print("Hello!")
}
// 输出:
// Hello!
// Hello!
// Hello!
3. 提供新的构造器 #

扩展可以为现有类型添加新的构造器:

struct Point {
    var x = 0.0, y = 0.0
}

extension Point {
    init(value: Double) {
        self.x = value
        self.y = value
    }
}

let point = Point(value: 3.0)
print(point)  // 输出: Point(x: 3.0, y: 3.0)
4. 添加下标 #

可以为已有类型添加新的下标功能:

extension Array {
    subscript(safe index: Int) -> Element? {
        return indices.contains(index) ? self[index] : nil
    }
}

let array = [1, 2, 3]
print(array[safe: 1])  // 输出: Optional(2)
print(array[safe: 10]) // 输出: nil

使现有类型遵循协议 #

你可以通过扩展让现有类型遵循一个或多个协议:

protocol Printable {
    func printDescription()
}

extension Int: Printable {
    func printDescription() {
        print("Integer: \(self)")
    }
}

let num = 42
num.printDescription()  // 输出: Integer: 42

添加嵌套类型 #

可以在扩展中为已有类型添加新的嵌套类型:

extension Int {
    enum Parity {
        case even, odd
    }
    
    var parity: Parity {
        return self % 2 == 0 ? .even : .odd
    }
}

let number = 3
print(number.parity)  // 输出: odd

泛型扩展 #

你可以对泛型类型进行扩展,而不需要在扩展声明中再次指定泛型参数:

struct Stack<Element> {
    var items = [Element]()
    
    mutating func push(_ item: Element) {
        items.append(item)
    }
    
    mutating func pop() -> Element {
        return items.removeLast()
    }
}

extension Stack {
    var topElement: Element? {
        return items.last
    }
}

var intStack = Stack<Int>()
intStack.push(1)
intStack.push(2)
print(intStack.topElement)  // 输出: Optional(2)

条件扩展 #

你可以对泛型类型进行条件扩展,限制扩展只能应用于满足某些条件的泛型类型:

extension Array where Element: Equatable {
    func isAllEqual() -> Bool {
        guard let first = self.first else { return true }
        return self.dropFirst().allSatisfy { $0 == first }
    }
}

let equalArray = [1, 1, 1, 1]
let nonEqualArray = [1, 2, 1, 1]

print(equalArray.isAllEqual())    // 输出: true
print(nonEqualArray.isAllEqual()) // 输出: false

总结 #

extension 是 Swift 中一个非常强大的工具,允许你为已有类型添加新的功能,而无需访问源代码。它们在打造模块化、可组合和易扩展的代码方面起着关键作用。

@ 符号的使用 #

当作特性 Attribute #

Attributes

在 Swift 中,@main 和 @available 是属性(Attributes),用于修饰类、函数或其他代码元素,以提供特定的功能或条件。

@objc #

将 Swift 声明暴露给 Objective-C。

@main #

@main 用于标记 Swift 应用程序的入口点。用这个属性标记的类型会成为程序的启动点,相当于传统的 C 语言或者其他编程语言中的 main() 函数。在 Swift 5.3 中引入了 @main属性,主要用于简化应用程序的启动配置。

一个典型的示例是 SwiftUI 应用程序的入口点:

import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

在这个示例中,MyApp 结构体被标记为应用程序的入口点。Swift 程序在启动时会执行这个定义在 MyApp 结构体中的代码。

@available #

@available 用于声明代码的可用性,可以指定代码在不同操作系统、平台和版本中的可用性。从而使代码在编译和运行时提供相应的警告或错误提示,有助于保持代码的兼容性和稳定性。语法如下:

@available(platform version constraints, *)

其中:

  • platform 可以是 iOSmacOSwatchOStvOS 等。
  • version constraints 指定代码在特定平台的版本要求。
  • * 表示不指定其他平台。

常见的使用示例如下:

@available(iOS 14, macOS 11, *)
func newFeature() {
    // 只在 iOS 14 和 macOS 11 及其之后的版本中可用的代码
}

// 或者用于类和结构体
@available(iOS 13.0, *)
class MyViewController: UIViewController {
    // 类中的代码
}

Type Attributes #

Type Attributes

在 Swift 中,@escaping 关键字并不是一个属性包装器(Property Wrapper),而是用于闭包的一个属性修饰符。理解 @escaping 以及它的用途,对于掌握 Swift 闭包的生命周期和使用场景至关重要。

@escaping 的解释 #

当闭包作为函数参数传递时,默认情况下闭包是在函数体中执行的。换句话说,闭包的生命周期不会超出函数调用的生命周期。这种情况下,闭包被称为 “非逃逸”(non-escaping)。

但是,有些情况下,你可能需要在函数执行完毕之后,仍然保留并使用这个闭包。例如,当你将闭包作为回调存储并将在将来某个时间点调用时,你需要明确指出这个闭包是 “逃逸”(escaping)的,这样 Swift 编译器才能正确管理其生命周期。

语法 #

可以通过在闭包类型参数前面加上 @escaping 关键字,来标记这个闭包将会逃逸:

func someFunctionWithEscapingClosure(completion: @escaping () -> Void) {
    // 存储闭包以便后续调用
    someStoreClosure = completion
}

func someFunction(){
    someFunctionWithEscapingClosure {
        print("This closure is escaping")
    }
}
使用场景 #
  • 异步操作: 通常与异步操作如网络请求、延迟执行等场景相关联,这些操作会在函数返回之后才执行任务。
func performAsyncOperation(completion: @escaping () -> Void) {
    DispatchQueue.global().async {
        // 异步操作
        completion()
    }
}
  • 存储属性: 将闭包存储为对象的属性,这些闭包会在函数返回之后才可能执行。
var completionHandler: (() -> Void)?

func someFunctionWithEscapingClosure(completion: @escaping () -> Void) {
    completionHandler = completion
}
注意事项 #
  • 在标记为 @escaping 的闭包中,如果访问 self 的成员变量或者调用方法,需要使用 self 来显示捕获以避免循环引用。这是因为逃逸闭包可能会在 self 不再存在时仍然尝试访问它。
class SomeClass {
    var someProperty: String = "Hello"
    
    func doSomethingWithEscapingClosure(completion: @escaping () -> Void) {
        DispatchQueue.global().async {
            print(self.someProperty)  // 需要使用 self
            completion()
        }
    }
}

总之,@escaping 关键字用于标记将会在函数返回之后仍然存在和使用的闭包。理解并正确使用 @escaping 关键字有利于更好地进行异步操作处理和闭包的生命周期管理。

当作属性包装器(property wrapper) #

1. @State #

https://developer.apple.com/documentation/swiftui/state

@State 是 SwiftUI 中用于声明本地状态变量的属性包装器。任何使用到 @State 变量的视图,当该变量变化时,都会重新渲染。

import SwiftUI

struct ContentView: View {
    @State private var counter = 0
    
    var body: some View {
        VStack {
            Text("Counter: \(counter)")
            Button(action: {
                counter += 1
            }) {
                Text("Increment Counter")
            }
        }
    }
}

2. @Binding #

@Binding 用于在不同视图之间传递和共享 @State 数据,使得一个视图可以绑定到另一个视图的状态。

import SwiftUI  

struct ParentView: View {  
    @State private var isPresented: Bool = false  

    var body: some View {  
        VStack {  
            Button(action: {  
                isPresented.toggle()  
            }) {  
                Text("Toggle Child View")  
            }  
            ChildView(isPresented: $isPresented)  
        }  
    }  
}  

struct ChildView: View {  
    @Binding var isPresented: Bool  

    var body: some View {  
        Text(isPresented ? "Presented!" : "Not Presented")  
    }  
}

3. @ObservedObject #

@ObservedObject 用于将一个符合 ObservableObject 协议的对象实例引入到视图中。当被观察对象的 @Published 属性发生变更时,系统会触发视图更新。

import SwiftUI
import Combine

class UserModel: ObservableObject {
    @Published var name: String = "John"
}

struct ContentView: View {
    @ObservedObject var user = UserModel()

    var body: some View {
        VStack {
            Text("Name: \(user.name)")
            Button(action: {
                self.user.name = "Alice"
            }) {
                Text("Change Name")
            }
        }
    }
}

4. @StateObject@EnvironmentObject #

@ObservedObject@StateObject@EnvironmentObject 是 SwiftUI 中用于处理和传递数据的属性包装器,它们之间的主要区别在于它们的作用范围和生命周期管理。

  1. @ObservedObject
  • 作用@ObservedObject 用于在视图内部观察和响应来自其他对象的状态变化。
  • 用法:通常用于在视图间共享数据,当被观察对象的属性发生变化时,相应的视图会自动更新。
  1. @StateObject
  • 作用@StateObject 用于在视图内部创建和管理持久化对象或模型。
  • 用法:通常用于在视图内部创建对象,并确保对象在视图销毁和重新加载时保持持久性,不会丢失。
  1. @EnvironmentObject
  • 作用@EnvironmentObject 用于在整个视图层次结构中共享数据模型,并将数据模型注入到子孙视图中。
  • 用法:通常用于全局共享的数据模型,可在任何子孙视图中直接访问和更新该数据。

区别总结:

  • @ObservedObject 用于在视图内部观察和响应来自其他对象的状态变化。
  • @StateObject 用于在视图内部创建和管理持久化对象或模型。
  • @EnvironmentObject 用于在整个视图层次结构中共享数据模型。

示例代码:

import SwiftUI
import Combine

class UserData: ObservableObject {
    @Published var name: String = "John"
}

struct ContentView: View {
    @ObservedObject var observedUser = UserData()
    @StateObject var stateUser = UserData()
    @EnvironmentObject var environmentUser: UserData

    var body: some View {
        VStack {
            Text("Observed: \(observedUser.name)")
            Text("State: \(stateUser.name)")
            Text("Environment: \(environmentUser.name)")
        }
    }
}

在上面的示例中,ContentView 中使用了 @ObservedObject@StateObject@EnvironmentObject 分别观察、创建和共享 UserData 对象。每个属性包装器都有不同的作用和用法,用于处理不同的数据传递和管理需求。

5. @Published #

@Published 属性包装器用于在 ObservableObject 中声明一个可发布的属性。当属性变化时,SwiftUI 会自动通知相关的视图进行更新。

import Combine

class UserModel: ObservableObject {
    @Published var name: String = "John"
}

6. @Bindable #

小结 #

@State@Binding 是用于在单个视图或视图之间传递数据和管理状态的属性包装器,而 @Published@ObservedObject 则更多用于跨视图层次结构共享数据和实现数据的响应性更新。以下是它们之间的主要区别和适用场景:

1. @State@Binding #
  • @State:用于在单个视图内部管理可变状态。当状态发生变化时,视图会自动重新渲染。
  • @Binding:用于在不同视图间传递数据,并创建对其他视图状态的引用。可以将 @State 数据绑定到其他视图的 @Binding 属性上。
2. @Published@ObservedObject #
  • @Published:用于在 ObservableObject 中标记需要被观察的属性,当被标记的属性发生变化时发送更新通知。
  • @ObservedObject:用于在视图间观察遵循 ObservableObject 协议的对象的状态。当被观察对象的 @Published 属性发生变化时,相应视图会自动更新。
场景和用途 #
  • 如果只需要在单个视图内部管理状态或者在不同视图之间直接传递数据,可以使用 @State@Binding
  • 当需要在整个应用程序或多个视图之间共享数据,让数据具有响应性更新,并且希望利用 SwiftUI 的自动更新功能时,应使用 @Published@ObservedObject
适用性举例 #
  • 当您想要在两个子视图之间共享数据并同步更改时使用 @State@Binding
  • 当您需要在整个应用程序中共享一个数据模型或实现数据响应性更新时使用 @Published@ObservedObject

综上所述,@State@Binding 主要用于局部状态管理和视图间直接数据传递,而 @Published@ObservedObject 更适用于整个应用程序范围内共享数据和实现数据响应性更新。根据具体的需求和场景,选择合适的属性包装器来实现数据管理和传递是很重要的。

@ViewBuilder #

@ViewBuilder 是 SwiftUI 框架中的一个属性包装器,用于将包含多个视图的声明式代码块转换为单个聚合视图。这个功能使开发者可以方便地创建复杂的视图层次结构,同时保持代码的简洁和可读性。当您在 SwiftUI 中定义视图的初始化或布局时,常常会使用 @ViewBuilder

使用 @ViewBuilder 最直接的方法是定义一个自定义的 SwiftUI 视图。这里有一个简单的示例,展示如何在 SwiftUI 中使用 @ViewBuilder

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello")
            Text("World")
        }
    }
}

在这个示例中,VStack 使用了 @ViewBuilder 来接受多个视图。不过,@ViewBuilder 也可以被自定义视图使用。要理解和使用 @ViewBuilder,我们可以创建一个自定义的容器视图,并在其中使用 @ViewBuilder。以下是一个展示如何创建自定义视图并使用 @ViewBuilder 的详细示例:

import SwiftUI

struct CustomContainer<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack {
            content
        }
    }
}

struct ContentView: View {
    var body: some View {
        CustomContainer {
            Text("Hello")
            Text("World")
        }
    }
}

详细说明:

  1. 定义和初始化自定义视图

    • CustomContainer 是一个通用的容器视图,接收一个内容视图 Content,该内容视图实现 View 协议。
    • init 方法中使用 @ViewBuilder 属性包装器来定义参数 content,这个参数是一个返回 Content 类型的闭包。
  2. 构建视图层次结构

    • body 属性中使用 VStack 作为容器,将内容闭包的返回视图包装起来。
  3. 使用自定义视图

    • ContentView 中,可以看到我们使用 CustomContainer 并传递两个文本视图:Text("Hello")Text("World")。这些视图将会被 VStack 包装,并显示在界面上。

@ViewBuilder 的优势

  • 简洁和清晰:能够直观地组合多个视图,而看起来不会让代码变得冗长。
  • 灵活性:适用于在不同层次结构中构建复杂的视图布局。
  • 类型安全:通过 Swift 的类型系统,确保组合的视图都是有效的 SwiftUI 视图。

@ViewBuilder 是 SwiftUI 框架中一个强大的工具,帮助开发者以声明式的方式创建复杂的视图层次结构。借助 @ViewBuilder,您可以轻松地定义自定义视图,并通过属性包装器将多个视图组合成一个整体,使您的 SwiftUI 代码更简洁、更优雅。

@Environment #

在 SwiftUI 中,@EnvironmentObject@Environment 是用于在视图层次结构中共享和传递数据的属性包装器和环境变量。@EnvironmentObject 用于共享特定数据模型,而 @Environment 则用于读取系统级别的环境变量。下面是关于如何使用 @Environment 的简单示例:

1. 读取系统环境变量 #
  • @Environment 可以用于读取系统级别的环境变量,例如颜色方案、设备类型等。

示例代码

import SwiftUI

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme

    var body: some View {
        VStack {
            Text("Color Scheme: \(colorScheme)")
        }
    }
}

在上面的示例中,通过 @Environment(\.colorScheme) 可以获取当前的颜色方案,然后在视图中显示当前的颜色方案。

2. 自定义环境变量 #
  • 您还可以自定义环境变量,并在整个视图层次结构中共享和访问它们。

示例代码

import SwiftUI

struct CustomEnvironmentKey: EnvironmentKey {
    static var defaultValue: String = "Default Value"
}

extension EnvironmentValues {
    var customValue: String {
        get { self[CustomEnvironmentKey.self] }
        set { self[CustomEnvironmentKey.self] = newValue }
    }
}

struct ContentView: View {
    @Environment(\.customValue) var customValue

    var body: some View {
        Text("Custom Value: \(customValue)")
    }
}

在上面的示例中,定义了一个自定义的环境键 CustomEnvironmentKey 和一个扩展 EnvironmentValues,用于设置和访问自定义值。通过 @Environment(\.customValue) 可以在视图中访问自定义的环境变量。

结论 #

使用 @Environment 可以方便地读取系统级别的环境变量或自定义环境变量,并在整个视图层次结构中共享这些数据。这可以帮助您实现更灵活、高效的界面设计和数据传递。希望这个简单示例能帮助您理解如何使用 @Environment。如有任何进一步问题,请随时提出!

$ 的使用 #

在 Swift 中,$ 符号有几个不同的用途,主要包括适配 SwiftUI 和 Combine 框架中的绑定和发布者,以及 Swift 闭包的简写,它们在不同场景中有不同的含义。

1. SwiftUI 中的绑定 #

在 SwiftUI 中,$ 符号通常用于绑定到某个 @State@ObservedObject 属性。在 SwiftUI 中,绑定(Binding)允许你在视图和数据源之间建立双向数据绑定,从而保持数据的一致性。

以下是一些常见的用法:

绑定到 @State 属性 #

import SwiftUI

struct ContentView: View {
    @State private var isOn: Bool = false

    var body: some View {
        Toggle(isOn: $isOn) {  // 使用 $ 符号绑定到 @State 属性
            Text("Switch")
        }
    }
}

在这个示例中,$isOnToggle 的值绑定到 isOn 状态属性。这样,当 Toggle 的值改变时,相应的状态属性也会更新,反之亦然。

绑定到 @ObservedObject 属性 #

import SwiftUI
import Combine

// 定义一个遵循 ObservableObject 协议的类
class Settings: ObservableObject {
    @Published var isEnabled: Bool = false
}

struct ContentView: View {
    @ObservedObject var settings = Settings()

    var body: some View {
        Toggle(isOn: $settings.isEnabled) {  // 使用 $ 符号绑定到 @ObservedObject 的 @Published 属性
            Text("Enable feature")
        }
    }
}

在这个示例中,$settings.isEnabledToggle 的值绑定到 Settings 对象的 isEnabled 属性中。当 Toggle 的状态更新时,isEnabled 属性也会同步更新,反之亦然。

2. Combine 框架中的发布者 #

在 Combine 框架中,$ 符号通常用于访问 @Published 属性的发布者。当你定义一个 @Published 属性时,可以通过 $属性名 来获取对应的发布者,用于订阅和响应数据的变化。

import Combine

class MyObject: ObservableObject {
    @Published var someValue: Int = 0  // 定义一个 @Published 属性
    
    init() {
        $someValue  // 使用 $ 符号获取相应的发布者
            .sink { newValue in
                print("Value changed to \(newValue)")
            }
            .store(in: &cancellables)
    }
    
    private var cancellables = Set<AnyCancellable>()
}

let myObject = MyObject()
myObject.someValue = 42  // 这将触发发布者并打印 "Value changed to 42"

在这段代码中,$someValue 获取了 someValue 属性的发布者。使用 sink 来订阅这个发布者,以便响应 someValue 值的变化。

3. 闭包中的参数简写 #

Swift 中的$符号还可以用于闭包参数的简写。在某些简写场景下,Swift 提供了默认的内联参数名 $0, $1, $2 等,这些参数是从 0 开始索引的。

let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 }  // 使用 $0 作为第一个参数的简写
print(doubled)  // 输出 [2, 4, 6, 8, 10]

在这个示例中,$0 表示闭包的第一个参数。这样写更简洁,不过仅适用于简单闭包,复杂闭包建议显式声明参数名以提高代码可读性。

总结 #

  • SwiftUI 绑定: 在 SwiftUI 中使用 $ 符号实现 @State@ObservedObject 属性与视图组件之间的数据绑定。
  • Combine 发布者: 在 Combine 框架中,使用 $ 符号获取 @Published 属性对应的发布者,以订阅和响应数据变化。
  • 闭包参数简写: 使用 $0, $1 等符号作为闭包参数的简写,简化闭包的写法。

这些用法在特定场景下非常有用,能够显著简化代码并提高可读性。

本文共 24918 字,上次修改于 Jul 17, 2024
相关标签: Swift, Xcode, ByAI