普通视图

发现新文章,点击刷新页面。
昨天 — 2025年8月27日首页

重学仓颉-2基础编程概念详解

作者 unravel2025
2025年8月27日 18:27

引言

仓颉语言(Cangjie Language)作为一门新兴的编程语言,具有强类型、函数式编程特性,并且支持现代化的编程范式。本文将深入探讨仓颉语言的基础编程概念,包括程序结构、标识符、函数定义和表达式系统。每个概念都将配有详细的代码示例,确保语法正确且可编译通过。

1. 程序结构(Program Structure)

1.1 基本概念

仓颉程序通常保存在扩展名为 .cj 的文本文件中。在顶层作用域中,可以定义全局变量、全局函数和自定义类型(如 structclassenuminterface)。程序入口是 main 函数,它可以有 Array<String> 类型的参数(用于接收命令行参数),也可以没有参数。

1.2 程序入口示例

package cangjie_blog
// 全局变量,最好有类型标注
let globalVariable: String = "Hello, Cangjie!"

// 全局函数
func globalFunction() {
    println("This is a global function")
}

// 自定义类型
struct MyStruct {
    MyStruct(let data: Int64){}
}

class MyClass {
    // 主构造函数,如果参数前面有修饰符,会自动生成同名的成员变量
     MyClass(let name: String){}
}

enum MyEnum {
    // 枚举值,用 | 分隔。
    // 第一个枚举值前面的|有可无
    | Case1 
    | Case2
    // 最后一个枚举值后面还可以有可选的 ...
    | ...
}

interface MyInterface {
    // 接口方法,不需要实现,只需要声明。必须明确参数和返回值类型
    func method(): Unit 
}

// 程序入口 - 不需要 func 修饰符
main() {
    println(globalVariable)
    globalFunction()
    
    let structInstance = MyStruct(42)
    let classInstance = MyClass("Cangjie")
    
    println("Struct data: ${structInstance.data}")
    println("Class name: ${classInstance.name}")
}

1.3 带命令行参数的程序入口

// args.cj
main(args: Array<String>) {
    println("程序名称: ${args[0]}")
    for (i in 1..args.size) {
        println("参数 ${i}: ${args[i]}")
    }
}

2. 标识符(Identifier)

2.1 标识符规则

仓颉语言的标识符分为普通标识符和原始标识符两类:

  • 普通标识符:不能和仓颉关键字相同,必须以 XID_Start 字符开头,后接任意长度的 XID_Continue 字符,或者以一个 _ 开头,后接至少一个 XID_Continue 字符。
  • 原始标识符:在普通标识符或仓颉关键字的首尾加上一对反引号,主要用于将仓颉关键字作为标识符。

2.2 合法标识符示例

// valid_identifiers.cj
main() {
    // 普通标识符示例
    let abc = "普通英文标识符"
    let _abc = "下划线开头的标识符"
    let abc_ = "下划线结尾的标识符"
    let a1b2c3 = "包含数字的标识符"
    let a_b_c = "包含下划线的标识符"
    let 仓颉 = "中文字符标识符"
    let __こんにちは = "日文字符标识符"
    
    // 原始标识符示例
    let `abc1` = "原始标识符"
    let `if` = "关键字作为标识符"
    let `while` = "另一个关键字作为标识符"
    
    println(abc)
    println(_abc)
    println(abc_)
    println(a1b2c3)
    println(a_b_c)
    println(仓颉)
    println(__こんにちは)
    // 这里使用反引号引用abc这个变量。和直接使用abc是一样的
    println(`abc`)
    println(`abc1`)
    println(`if`)
    println(`while`)
}

2.3 非法标识符示例

// invalid_identifiers.cj
main() {
    // 以下代码会编译错误,仅用于演示非法标识符
    
    // let ab&c = "包含非法字符"  // 错误:& 不是 XID_Continue 字符
    // let 3abc = "数字开头"     // 错误:数字不能作为起始字符
    // let _ = "单独的下划线"    // 不会报错,但是也无法引用这个变量,相当于丢弃这个值。这里实际上是模式匹配语法
    // let while = "关键字"      // 错误:普通标识符不能使用关键字
    
    println("这些标识符都是非法的")
}

3. 函数(Function)

3.1 函数定义语法

仓颉语言使用关键字 func 来定义函数,语法格式为:

func 函数名(参数列表): 返回值类型 {
    函数体
}

3.2 基本函数示例

// functions.cj

// 无参数无返回值的函数
func greet() {
    println("Hello, Cangjie!")
}

// 有参数无返回值的函数
func greetPerson(name: String) {
    println("Hello, ${name}!")
}

// 有参数有返回值的函数
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

// 有多个返回值的函数(使用元组)
func divideAndRemainder(a: Int64, b: Int64): (Int64, Int64) {
    return (a / b, a % b)
}

// 递归函数示例
func factorial(n: Int64): Int64 {
    if (n <= 1) {
        return 1
    } else {
        return n * factorial(n - 1)
    }
}

main() {
    greet()
    greetPerson("张三")
    
    let sum = add(10, 20)
    println("10 + 20 = ${sum}")
    
    let (quotient, remainder) = divideAndRemainder(17, 5)
    println("17 ÷ 5 = ${quotient} 余 ${remainder}")
    
    let fact = factorial(5)
    println("5! = ${fact}")
}

3.3 函数重载示例

// function_overloading.cj

// 函数重载 - 不同参数类型
func add(a: Int64, b: Int64): Int64 {
    return a + b
}

func add(a: Float64, b: Float64): Float64 {
    return a + b
}

func add(a: String, b: String): String {
    return a + b
}

// 函数重载 - 不同参数数量
func multiply(a: Int64, b: Int64): Int64 {
    return a * b
}

func multiply(a: Int64, b: Int64, c: Int64): Int64 {
    return a * b * c
}

main() {
    println("整数相加: ${add(5, 3)}")
    println("浮点数相加: ${add(5.5, 3.2)}")
    println("字符串连接: ${add("Hello", " Cangjie")}")
    
    println("两个数相乘: ${multiply(4, 5)}")
    println("三个数相乘: ${multiply(2, 3, 4)}")
}

4. 变量(Variable)

4.1 变量定义

仓颉语言中的变量由变量名、数据类型、初始值和修饰符构成。主要修饰符包括:

  • let:不可变变量
  • var:可变变量
  • const:常量
  • private/public:可见性修饰符
  • static:静态性修饰符

4.2 基本变量示例

// variables.cj

// 全局变量
let globalConstant: String = "全局常量"
var globalVariable: String = "全局变量"

main() {
    // 局部变量
    let localConstant: Int64 = 42
    var localVariable: String = "局部变量"
    
    // 类型推断
    let inferredInt = 100
    let inferredString = "类型推断"
    let inferredFloat = 3.14
    
    // const 变量
    const PI = 3.14159265359
    const GRAVITY = 9.8
    
    println("全局常量: ${globalConstant}")
    println("全局变量: ${globalVariable}")
    println("局部常量: ${localConstant}")
    println("局部变量: ${localVariable}")
    println("推断整数: ${inferredInt}")
    println("推断字符串: ${inferredString}")
    println("推断浮点数: ${inferredFloat}")
    println("圆周率: ${PI}")
    println("重力加速度: ${GRAVITY}")
    
    // 修改变量值
    globalVariable = "修改后的全局变量"
    localVariable = "修改后的局部变量"
    
    println("修改后 - 全局变量: ${globalVariable}")
    println("修改后 - 局部变量: ${localVariable}")
}

4.3 延迟初始化示例

// delayed_initialization.cj

main() {
    // 延迟初始化 - 先声明后赋值
    let message: String
    let number: Int64
    
    // 在引用前必须初始化
    message = "Hello, Cangjie!"
    number = 42
    
    println("消息: ${message}")
    println("数字: ${number}")
    
    // 条件初始化
    let condition = true
    let conditionalValue: String
    
    if (condition) {
        conditionalValue = "条件为真"
    } else {
        conditionalValue = "条件为假"
    }
    
    println("条件值: ${conditionalValue}")
}

4.4 值类型和引用类型示例

// value_reference_types.cj

// 值类型 - struct
struct Point {
    Point(let x: Int64, let y: Int64) {}
}

// 引用类型 - class
class Person {
    var name: String
    var age: Int64

    public init(name: String, age: Int64) {
        this.name = name
        this.age = age
    }
}

main() {
    // 值类型示例
    let point1 = Point(10, 20)
    let point2 = point1 // 创建副本
    println("Point1: (${point1.x}, ${point1.y})")
    println("Point2: (${point2.x}, ${point2.y})")

    // 引用类型示例
    let person1 = Person("张三", 25)
    let person2 = person1 // 创建引用
    println("Person1: ${person1.name}, ${person1.age}")
    println("Person2: ${person2.name}, ${person2.age}")

    // 修改引用类型会影响所有引用
    person2.age = 30
    println("修改后 - Person1: ${person1.name}, ${person1.age}")
    println("修改后 - Person2: ${person2.name}, ${person2.age}")
}

5. 表达式(Expression)

5.1 代码块

仓颉中没有代码块,但是可以使用闭包代替

// code_blocks.cj
main() {
    let blockValue = {
        =>
            let a = 10
            let b = 20
            a + b
    }
    println("执行闭包,返回值: ${blockValue()}")
}

5.2 if 表达式

5.2.1 基本 if 表达式

// if_expressions.cj

main() {
    let number = 15
    
    // 基本 if-else
    if (number > 10) {
        println("数字大于10")
    } else {
        println("数字小于等于10")
    }
    
    // 多级 if-else
    if (number > 20) {
        println("数字大于20")
    } else if (number > 10) {
        println("数字大于10但小于等于20")
    } else {
        println("数字小于等于10")
    }
    
    // if 表达式作为值
    let result = if (number % 2 == 0) {
        "偶数"
    } else {
        "奇数"
    }
    println("${number} 是 ${result}")
    
    // 条件表达式
    let score = 85
    let grade = if (score >= 90) {
        "A"
    } else if (score >= 80) {
        "B"
    } else if (score >= 70) {
        "C"
    } else if (score >= 60) {
        "D"
    } else {
        "F"
    }
    println("分数 ${score} 对应等级: ${grade}")
}

5.2.2 涉及 let pattern 的 if 表达式

// let_pattern_if.cj

main() {
    // Option 类型示例
    let someValue = Some(42)
    let noneValue: Option<Int64> = None
    
    // 使用 let pattern 进行模式匹配
    if (let Some(value) <- someValue) {
        println("someValue 包含值: ${value}")
    } else {
        println("someValue 是 None")
    }
    
    if (let Some(value) <- noneValue) {
        println("noneValue 包含值: ${value}")
    } else {
        println("noneValue 是 None")
    }
    
    // 组合条件
    let anotherSome = Some(100)
    if (let Some(x) <- someValue && let Some(y) <- anotherSome) {
        println("两个值都存在: ${x} 和 ${y}")
    }
    
    // 逻辑或
    if (let Some(_) <- someValue || let Some(_) <- noneValue) {
        println("至少有一个是 Some")
    } else {
        println("都是 None")
    }
}

5.3 循环表达式

5.3.1 while 表达式

// while_expressions.cj

main() {
    // 基本 while 循环
    var counter = 0
    while (counter < 5) {
        println("计数器: ${counter}")
        counter++
    }
    
    // 计算阶乘
    var n = 5
    var factorial = 1
    while (n > 0) {
        factorial *= n
        n--
    }
    println("5! = ${factorial}")
    
    // 查找数组中的元素
    let numbers = [10, 20, 30, 40, 50]
    var index = 0
    var found = false
    
    while (index < numbers.size && !found) {
        if (numbers[index] == 30) {
            println("找到30,索引为: ${index}")
            found = true
        }
        index++
    }
    
    if (!found) {
        println("未找到30")
    }
}

5.3.2 do-while 表达式

// do_while_expressions.cj

main() {
    // 基本 do-while 循环
    var counter = 0
    do {
        println("do-while 计数器: ${counter}")
        counter++
    } while (counter < 3)
    
    // 至少执行一次的循环
    var number = 0
    do {
        println("当前数字: ${number}")
        number++
    } while (number <= 5)
    
    // 密码验证示例
    var attempts = 0
    let correctPassword = "cangjie123"
    var inputPassword = "wrong"
    
    do {
        attempts++
        println("尝试次数: ${attempts}")
        // 模拟密码验证
        if (attempts >= 3) {
            inputPassword = correctPassword
        }
    } while (inputPassword != correctPassword && attempts < 5)
    
    if (inputPassword == correctPassword) {
        println("密码验证成功!")
    } else {
        println("密码验证失败!")
    }
}

5.3.3 for-in 表达式

// for_in_expressions.cj

main() {
    // 遍历数组
    let fruits = ["苹果", "香蕉", "橙子", "葡萄"]
    println("水果列表:")
    for (fruit in fruits) {
        println("- ${fruit}")
    }
    
    // 遍历区间
    println("1到5的数字:")
    for (i in 1..=5) {
        println("数字: ${i}")
    }
    
    // 遍历区间(不包含结束值)
    println("0到4的数字:")
    for (i in 0..5) {
        println("数字: ${i}")
    }
    
    // 遍历元组数组
    let coordinates = [(1, 2), (3, 4), (5, 6)]
    println("坐标点:")
    for ((x, y) in coordinates) {
        println("(${x}, ${y})")
    }
    
    // 使用通配符
    var sum = 0
    for (_ in 0..5) {
        sum += 10
    }
    println("累加结果: ${sum}")
    
    // 使用 where 条件
    println("1到10中的偶数:")
    for (i in 1..=10 where i % 2 == 0) {
        println("偶数: ${i}")
    }
    
    // 嵌套循环
    println("乘法表 (1-3):")
    for (i in 1..=3) {
        for (j in 1..=3) {
            print("${i * j} ")
        }
        println()
    }
}

5.4 控制转移表达式

5.4.1 break 表达式

// break_expressions.cj

main() {
    // 在 for 循环中使用 break
    let numbers = [1, 3, 5, 7, 9, 2, 4, 6, 8, 10]
    
    println("查找第一个偶数:")
    for (number in numbers) {
        if (number % 2 == 0) {
            println("找到第一个偶数: ${number}")
            break
        }
    }
    
    // 在 while 循环中使用 break
    var counter = 0
    while (true) {
        counter++
        if (counter > 10) {
            break
        }
        if (counter % 2 == 0) {
            continue
        }
        println("奇数: ${counter}")
    }
    
    // 嵌套循环中的 break
    println("嵌套循环示例:")
    for (i in 1..=3) {
        for (j in 1..=3) {
            if (i == 2 && j == 2) {
                println("在 (2,2) 处跳出内层循环")
                break
            }
            println("(${i}, ${j})")
        }
    }
}

5.4.2 continue 表达式

// continue_expressions.cj
main() {
    // 在 for 循环中使用 continue
    println("打印1到10中的奇数:")
    for (i in 1..=10) {
        if (i % 2 == 0) {
            continue  // 跳过偶数
        }
        println("奇数: ${i}")
    }
    
    // 在 while 循环中使用 continue
    var number = 0
    println("打印小于10的3的倍数:")
    while (number < 10) {
        number++
        if (number % 3 != 0) {
            continue  // 跳过不是3的倍数的数
        }
        println("3的倍数: ${number}")
    }
    
    // 跳过特定值
    let scores = [85, 92, 78, 95, 88, 100, 76, 89]
    println("打印90分以上的成绩:")
    for (score in scores) {
        if (score < 90) {
            continue  // 跳过90分以下的成绩
        }
        println("优秀成绩: ${score}")
    }
    
    // 处理数组中的有效数据
    let data = [Some(1), Option<Int>.None, Some(3), Option<Int>.None, Some(5)]
    println("处理有效数据:")
    for (item in data) {
        if (let None <- item) {
            continue  // 跳过 None 值
        }
        if (let Some(value) <- item) {
            println("有效数据: ${value}")
        }
    }
}

6. 作用域(Scope)

6.1 作用域规则

仓颉语言中的作用域由大括号 {} 定义,遵循以下规则:

  1. 当前作用域中定义的程序元素在当前作用域和其内层作用域中有效
  2. 内层作用域中定义的程序元素在外层作用域中无效
  3. 内层作用域可以使用外层作用域中的名字重新定义绑定关系(变量遮蔽)

6.2 作用域示例

// scope_examples.cj
// 全局作用域
let globalVar: String = "全局变量"

func outerFunction() {
    let outerVar = "外层函数变量"
    println("外层函数: ${globalVar}")
    println("外层函数: ${outerVar}")

    func innerFunction() {
        let innerVar = "内层函数变量"
        println("内层函数: ${globalVar}")
        println("内层函数: ${outerVar}")
        println("内层函数: ${innerVar}")

        // 变量遮蔽
        let outerVar = "遮蔽的外层变量"
        println("内层函数(遮蔽后): ${outerVar}")
    }

    innerFunction()
    let globalVar: String = "遮蔽的全局变量"
    println("外层函数(遮蔽后): ${globalVar}")
}

main() {
    println("主函数: ${globalVar}")

    // 局部作用域
    do {
        let localVar = "局部作用域变量"
        println("局部作用域: ${localVar}")
        println("局部作用域: ${globalVar}")
    } while (false)

    // 条件作用域
    if (true) {
        let conditionalVar = "条件作用域变量"
        println("条件作用域: ${conditionalVar}")
    }

    // 循环作用域
    for (i in 1..=3) {
        let loopVar = "循环变量 ${i}"
        println("循环作用域: ${loopVar}")
    }

    outerFunction()
}

7. 综合示例

7.1 简单计算器

// calculator.cj
func add(a: Float64, b: Float64): Float64 {
    return a + b
}

func subtract(a: Float64, b: Float64): Float64 {
    return a - b
}

func multiply(a: Float64, b: Float64): Float64 {
    return a * b
}

func divide(a: Float64, b: Float64): Option<Float64> {
    if (b == 0.0) {
        return None
    } else {
        return Some(a / b)
    }
}

func calculate(operation: String, a: Float64, b: Float64): Option<Float64> {
    if (operation == "+") {
        return Some(add(a, b))
    } else if (operation == "-") {
        return Some(subtract(a, b))
    } else if (operation == "*") {
        return Some(multiply(a, b))
    } else if (operation == "/") {
        return divide(a, b)
    } else {
        return None
    }
}

main() {
    let operations = ["+", "-", "*", "/"]
    let numbers = [(10.0, 5.0), (15.0, 3.0), (8.0, 0.0)]
    
    println("简单计算器演示:")
    println("==================")
    
    for ((a, b) in numbers) {
        println("计算 ${a} 和 ${b}:")
        
        for (op in operations) {
            if (let Some(result) <- calculate(op, a, b)) {
                println("  ${a} ${op} ${b} = ${result}")
            } else {
                println("  ${a} ${op} ${b} = 错误(除零或无效操作)")
            }
        }
        println()
    }
}

7.2 学生成绩管理系统

// student_grade_system.cj
import std.collection.ArrayList

struct Student {
    let name: String
    let id: String
    var grades: ArrayList<Float64>
    
    public init(name: String, id: String) {
        this.name = name
        this.id = id
        this.grades = ArrayList<Float64>()
    }
    // 如果是public修饰的函数,必须显示标注返回值类型
    public func addGrade(grade: Float64): Unit {
        if (grade >= 0.0 && grade <= 100.0) {
            this.grades.add(grade)
        } else {
            println("无效成绩: ${grade}")
        }
    }
    
    public func getAverage(): Option<Float64> {
        if (this.grades.size == 0) {
            return None
        }
        
        var sum = 0.0
        for (grade in this.grades) {
            sum += grade
        }
        return Some(sum / Float64(this.grades.size))
    }
    
    public func getGradeLevel(): String {
        if (let Some(average) <- this.getAverage()) {
            if (average >= 90.0) {
                return "A"
            } else if (average >= 80.0) {
                return "B"
            } else if (average >= 70.0) {
                return "C"
            } else if (average >= 60.0) {
                return "D"
            } else {
                return "F"
            }
        } else {
            return "无成绩"
        }
    }
}

main() {
    // 创建学生
    let student1 = Student("张三", "2024001")
    let student2 = Student("李四", "2024002")
    let student3 = Student("王五", "2024003")
    
    // 添加成绩
    student1.addGrade(85.0)
    student1.addGrade(92.0)
    student1.addGrade(78.0)
    
    student2.addGrade(95.0)
    student2.addGrade(88.0)
    student2.addGrade(91.0)
    
    student3.addGrade(72.0)
    student3.addGrade(68.0)
    student3.addGrade(75.0)
    
    // 显示学生信息
    let students = [student1, student2, student3]
    
    println("学生成绩管理系统")
    println("==================")
    
    for (student in students) {
        println("学生: ${student.name} (ID: ${student.id})")
        
        if (let Some(average) <- student.getAverage()) {
            println("  平均分: ${average}")
            println("  等级: ${student.getGradeLevel()}")
        } else {
            println("  无成绩记录")
        }
        
        println("  成绩列表:")
        for (i in 0..student.grades.size) {
            println("    第${i + 1}次: ${student.grades[i]}")
        }
        println()
    }
    
    // 统计信息
    var totalStudents = 0
    var passedStudents = 0
    
    for (student in students) {
        totalStudents++
        if (let Some(average) <- student.getAverage()) {
            if (average >= 60.0) {
                passedStudents++
            }
        }
    }
    
    println("统计信息:")
    println("  总学生数: ${totalStudents}")
    println("  及格学生数: ${passedStudents}")
    println("  及格率: ${Float64(passedStudents) / Float64(totalStudents) * 100.0}%")
}

参考资料

昨天以前首页

Swift 数据容器全景手册:Sequence、Collection、Set、Dictionary 一次掌握

作者 unravel2025
2025年8月26日 14:24

从协议层设计到实战选型,帮你彻底厘清「什么时候用 Array,什么时候用 Set」。

两个顶层协议:Sequence vs Collection

特性 Sequence Collection
顺序访问 ✅ 单向迭代 ✅ 双向/随机访问
可遍历多次 ❌ 不一定 ✅ 总是
下标访问 ❌ 无 ✅ 有
count 复杂度 O(n) O(1) 或 O(n)

一句话:所有 Collection 都是 Sequence,反之则不成立。

Sequence:只关心「能不能 for-in」

最小实现

// 斐波那契作为无限序列
struct Fibonacci: Sequence {
    func makeIterator() -> some IteratorProtocol {
        return FibIterator()
    }
}

struct FibIterator: IteratorProtocol {
    private var a = 0, b = 1
    mutating func next() -> Int? {
        let value = a
        (a, b) = (b, a + b)
        return value
    }
}

// 使用
for (i, v) in Fibonacci().prefix(10).enumerated() {
    print("F[\(i)] = \(v)")
}

延迟特性(Lazy)

// 不会立即分配 1_000_000 个元素
let huge = (0..<1_000_000).lazy.map { $0 * $0 }

Collection:在 Sequence 上加「下标」

核心层级

Sequence
 └── Collection
      ├── MutableCollection
      ├── BidirectionalCollection
      └── RandomAccessCollection

自定义 Collection 示例

// 只读字符串切片集合
struct SubstringCollection: Collection {
    let base: String
    let substrings: [Substring]

    var startIndex: Int { 0 }
    var endIndex:   Int { substrings.count }
    func index(after i: Int) -> Int { i + 1 }
    subscript(position: Int) -> Substring { substrings[position] }
}

四大常用容器对比

容器 有序? 唯一? 读写 典型场景
Array 随机读写 列表、栈、队列
Set Hash 表 去重、交集、差集
Dictionary Key 唯一 键值映射 缓存、配置
NSOrderedSet 索引+唯一 有序去重(需 Foundation)

Array 深度指南

值语义与写时复制 (CoW)

var a = [1, 2, 3]
var b = a           // 共享同一块内存
b.append(4)         // 触发 CoW,复制一份

性能陷阱:大数组拷贝

// 避免在 for 循环中直接 append
let squared = largeArray.map { $0 * $0 }   // 一次分配

Set:哈希与唯一性的艺术

基本操作

let odd: Set = [1, 3, 5]
let even: Set = [2, 4, 6]
print(odd.union(even))            // [1, 2, 3, 4, 5, 6]
print(odd.intersection(even))     // []

自定义可哈希类型

struct Point: Hashable {
    let x, y: Int
    // 自动合成 Hashable
}

let points: Set<Point> = [Point(x: 1, y: 2), Point(x: 3, y: 4)]

Dictionary:字典的正确打开方式

分组 & 计数

let words = ["apple", "banana", "apricot"]
let grouped = Dictionary(grouping: words, by: { $0.first! })
// ["a": ["apple", "apricot"], "b": ["banana"]]

let counts = Dictionary(words.map { ($0, 1) }, uniquingKeysWith: +)

嵌套字典优雅读写

extension Dictionary where Key == String, Value == Any {
    subscript(path path: String) -> Any? {
        get {
            var keys = path.split(separator: ".").map(String.init)
            return keys.reduce(self) { ($0 as? [String: Any])?[$1] }
        }
    }
}

let dict = ["user": ["profile": ["name": "Ada"]]]
print(dict[path: "user.profile.name"] ?? "N/A") // "Ada"

选型决策树

需要下标随机访问?
├─ 是 → Collection
│   ├─ 需要唯一性 → Set / Dictionary
│   └─ 需要顺序   → Array
└─ 否 → Sequence(延迟计算,节省内存)

性能速查表

操作 Array Set Dictionary
append/insert O(1) amortized O(1) O(1)
contains O(n) O(1) O(1)
remove O(n) O(1) O(1)
sort O(n log n) ❌ 无序 ❌ 无序

实战 Checklist

  1. 列表渲染 → Array
  2. 搜索去重 → Set
  3. 键值缓存 → Dictionary
  4. 大数据链式处理 → .lazy Sequence
  5. 需要有序去重 → NSOrderedSet(Foundation)

一句话总结

顺序访问用 Array,唯一性用 Set,键值映射用 Dictionary,延迟计算用 Sequence。

牢记这句口诀,Swift 数据结构选型不再纠结!

深入理解 SOLID 原则:用 Swift 编写优雅、可维护的代码

作者 unravel2025
2025年8月26日 14:13

从理论到实战,逐条拆解 + 代码示例 + 重构案例,让你一次掌握五大设计原则。

什么是 SOLID?

SOLID 是面向对象设计的五大原则,帮助开发者写出高内聚、低耦合、易测试、可扩展的代码。

  • Single Responsibility Principle(单一职责)
  • Open/Closed Principle(开闭原则)
  • Liskov Substitution Principle(里氏替换)
  • Interface Segregation Principle(接口隔离)
  • Dependency Inversion Principle(依赖反转)

SRP:单一职责原则

一个类/模块只有一个理由去改变

把“因为需求 A 修改”和“因为需求 B 修改”拆成两个类。

❌ 反面示例

class DataManager {
    func fetchData() { /* 网络请求 */ }
    func parseData(_ data: Data) { /* JSON 解析 */ }
    func saveData(_ parsed: ParsedData) { /* 写入磁盘 */ }
    func showAlert() { /* UI 弹窗 */ }   // 又管 UI?
}

✅ 重构后:职责分离

// 网络
class NetworkService {
    func fetchData() async throws -> Data { /* ... */ }
}

// 解析
class JSONParser {
    func parse<T: Decodable>(_ data: Data) throws -> T { /* ... */ }
}

// 存储
class CacheService {
    func save<T: Encodable>(_ object: T, key: String) throws { /* ... */ }
}
  • 好处:修改网络层时不会波及缓存逻辑,单元测试也更容易 mock。

OCP:开闭原则

对扩展开放,对修改关闭

加功能时尽量不改动旧代码,用协议 + 多态解决。

❌ 反面:每加一个形状就改 switch

enum ShapeType { case circle, square }
func render(_ type: ShapeType) {
    switch type {
    case .circle: /* 画圆 */
    case .square: /* 画方 */
    }
}

✅ 重构:面向协议扩展

protocol Renderer {
    func render()
}

struct CircleRenderer: Renderer {
    func render() { /* 画圆 */ }
}

struct SquareRenderer: Renderer {
    func render() { /* 画方 */ }
}

// 新增三角形:零改动旧代码
struct TriangleRenderer: Renderer {
    func render() { /* 画三角形 */ }
}
  • 好处:新增形状只需再写一个 Renderer,老代码稳如老狗。

LSP:里氏替换原则

子类必须能透明替换父类,不能破坏父类约定。

❌ 反面:正方形继承矩形导致异常

class Rectangle {
    var width: Double = 0
    var height: Double = 0
    func area() -> Double { width * height }
}

class Square: Rectangle {
    override var width: Double {
        didSet { height = width }   // 破坏父类行为
    }
}

func printArea(_ r: Rectangle) {
    r.width = 5
    r.height = 4
    print(r.area()) // 期望 20,Square 却得到 16
}

✅ 重构:协议抽象

protocol Shape {
    func area() -> Double
}

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

struct Square: Shape {
    let side: Double
    func area() -> Double { side * side }
}
  • 好处:Square 无需伪装成 Rectangle,行为自然正确。

ISP:接口隔离原则

客户端不应被迫依赖它不需要的接口

把胖协议拆成小协议,按需组合。

❌ 反面:胖协议

protocol Database {
    func save()
    func fetch()
    func delete()
    func backup()
}

class TinyCache: Database {
    func save() { /* 只用到 save */ }
    func fetch() { /* 空实现 */ }
    func delete() { /* 空实现 */ }
    func backup() { /* 空实现 */ } // 被迫实现
}

✅ 重构:小协议 + 组合

protocol Savable {
    func save()
}

protocol Fetchable {
    func fetch() -> Data
}

class TinyCache: Savable {
    func save() { /* 只需实现 save */ }
}
  • 好处:实现类只关心自己需要的接口,避免“大而全”。

DIP:依赖反转原则

高层模块不依赖低层细节,两者都依赖抽象

通过构造函数/属性注入解耦。

❌ 反面:硬编码依赖

class DataManager {
    private let api = RemoteAPI() // 写死
    func load() {
        let data = api.fetch()
    }
}
  • 测试时想换成 MockAPI 很难。

✅ 重构:依赖注入

protocol DataProvider {
    func fetch() async throws -> Data
}

struct RemoteAPI: DataProvider { /* ... */ }
struct MockAPI: DataProvider { /* ... */ }

class DataManager {
    private let provider: DataProvider
    init(provider: DataProvider) {
        self.provider = provider
    }
    func load() async throws {
        let data = try await provider.fetch()
    }
}

// 使用
let manager = DataManager(provider: RemoteAPI())
// 测试
let testManager = DataManager(provider: MockAPI())
  • 好处:随时替换实现,单元测试秒变 mock。

综合案例:重构一个“用户服务”

需求

  • 登录后获取用户信息
  • 缓存到本地
  • 支持远程 & 本地两种数据源

初始代码(违反 SRP+DIP)

class UserService {
    func login() {
        // 网络请求
        // JSON 解析
        // 写磁盘
        // 通知 UI
    }
}

✅ SOLID 重构后

// 1. 抽象数据源
protocol UserDataSource {
    func fetchUser(id: String) async throws -> User
}

// 2. 实现
struct RemoteUserSource: UserDataSource { /* 网络 */ }
struct LocalUserSource: UserDataSource { /* 缓存 */ }

// 3. 解析器
struct UserParser {
    func parse(_ data: Data) throws -> User { /* ... */ }
}

// 4. 仓库(高层模块)
class UserRepository {
    private let remote: UserDataSource
    private let local: UserDataSource
    private let parser: UserParser
    
    init(remote: UserDataSource, local: UserDataSource, parser: UserParser) {
        self.remote = remote
        self.local = local
        self.parser = parser
    }
    
    func user(id: String) async throws -> User {
        if let cached = try await local.fetchUser(id: id) { return cached }
        let data = try await remote.fetchUser(id: id)
        return try parser.parse(data)
    }
}
  • 单一职责:每个类只做一件事
  • 开闭原则:新增 GraphQLUserSource 不改动旧代码
  • 依赖反转:UserRepository 只依赖 UserDataSource 协议

总结速记表

原则 一句话 Swift 技巧
SRP 一个类做一件事 拆小类、用协议
OCP 加功能不改动旧代码 协议 + 多态
LSP 子类必须可替换父类 用协议代替继承
ISP 接口要小而专 小协议组合
DIP 依赖抽象不依赖细节 构造函数注入

Swift 并发全景指南:Thread、Concurrency、Parallelism 一次搞懂

作者 unravel2025
2025年8月26日 13:30

从底层线程到高层 Swift Concurrency,用代码带你吃透所有概念

为什么要关心这些概念?

  • 响应式 UI:主线程阻塞 = 卡死界面。
  • 高性能:多核 CPU 不并行 = 浪费算力。
  • 正确性:数据竞争 = 闪退或脏数据。

Thread:程序的最小执行单元

什么是 Thread

一条独立的执行路径,拥有独立的 栈 和 程序计数器,但与其他线程共享进程的内存空间。

手动创建 Thread(仅教学用,生产请用 GCD/Task)

import Foundation

func threadFunction() {
    for i in 1...3 {
        print("👾 Thread \(Thread.current) count \(i)")
        Thread.sleep(forTimeInterval: 0.3)
    }
}

let t = Thread {
    threadFunction()
}
t.start()          // 手动启动线程

⚠️ 直接使用 Thread 成本高、易出错;日常开发请使用 GCD 或 Swift Concurrency。

Concurrency vs. Parallelism:一对容易混淆的孪生兄弟

维度 Concurrency(并发) Parallelism(并行)
定义 交替推进多个任务 同时执行多个任务
CPU 核数 1 核即可 ≥2 核
目的 提高响应能力 提高吞吐量

并发示例(单核交替)

let queue = DispatchQueue(label: "concurrent", attributes: .concurrent)
queue.async { print("🍎 Task A") }
queue.async { print("🍏 Task B") }

单核 CPU 通过时间片轮转交替打印 A/B。

并行示例(多核同时)

DispatchQueue.concurrentPerform(iterations: 4) { i in
    print("🔥 Parallel \(i) on \(Thread.current)")
}

4 核机器会真正同时跑 4 条线程。

Swift 中的线程种类

线程 用途 注意
Main Thread UI 更新 & 用户交互 禁止长时间阻塞
Global Queue 默认后台线程池 QoS 分级(utility、background …)

主线程 & 后台线程实战

// 1. 回到主线程刷新 UI
DispatchQueue.main.async {
    label.text = "Loaded"
}

// 2. 后台线程做重活
DispatchQueue.global(qos: .userInitiated).async {
    let img = self.resize(image: bigImage)
    DispatchQueue.main.async { imageView.image = img }
}

Thread Safety:别让数据“赛车”

问题示例:数据竞争

class UnsafeCounter {
    var value = 0
    func increment() { value += 1 }
}

在多线程环境下 value += 1 可能丢失更新(读-改-写非原子)。

解决方案速查表

技术 适用场景 示例
NSLock 低层临界区 见下方代码
串行 DispatchQueue 顺序执行任务 DispatchQueue(label: "serial")
Actor(Swift 5.5+) 高层、零锁代码 actor Counter { ... }

NSLock 示例

class SafeCounter {
    private var value = 0
    private let lock = NSLock()
    
    func increment() {
        lock.lock()
        defer { lock.unlock() }
        value += 1
    }
    
    func get() -> Int {
        lock.lock()
        defer { lock.unlock() }
        return value
    }
}

Actor 示例(推荐)

actor CounterActor {
    private var value = 0
    func increment() { value += 1 }
    func get() -> Int { value }
}

// 使用
let counter = CounterActor()
Task {
    await counter.increment()
    print(await counter.get())
}

真实世界场景演练

场景 并发模型 关键代码片段
Web 服务器 GCD + 并行队列 DispatchQueue.global().async { handle(request) }
图片滤镜 concurrentPerform DispatchQueue.concurrentPerform(iterations: count) { applyFilter($0) }
游戏引擎 多线程渲染 渲染线程 + 逻辑线程 + Actor 共享状态

思维导图:如何选择工具

需求: 线程安全
├─ 只读数据 → 无需同步
├─ 低频写   → NSLock / 串行 queue
├─ 高频写   → Actor (零锁、可组合)
└─ 复杂依赖 → Task + Actor + AsyncSequence

常见问题 FAQ

问题 回答
何时用 Task vs DispatchQueue? 新项目优先 Task,老代码逐步迁移。
Actor 会降低性能吗? 轻微调度开销,远低于锁竞争。
MainActor 是什么? 系统预定义的全局 Actor,强制代码跑在主线程。

一句话总结

线程是地基,并发是设计思想,并行是多核福利;用 GCD/Task 管理线程,用 Actor/锁 保证安全,你的 Swift App 就能既快又稳。

Swift 并发模型深度解析:Singleton 与 Global Actor 如何抉择?

作者 unravel2025
2025年8月26日 13:17

在 Swift 的世界里,Singleton(单例模式) 是我们最熟悉的老朋友,而 Global Actor(全局 Actor) 则是 Swift 5.5 引入并发模型后的新伙伴。两者都能帮我们管理全局共享资源,但谁才是现代 Swift 项目的最佳选择?本文将带你深入理解两者的核心差异、适用场景,并结合代码示例给出实战建议。

为什么需要 Singleton?

Singleton 的核心价值

  • 唯一实例:确保类只有一个实例(如网络管理器、数据库连接)。
  • 全局访问:通过 shared 属性在任何地方访问。

传统 Singleton 的实现

// 传统单例:非线程安全
class NetworkManager {
    static let shared = NetworkManager()
    private init() {} // 防止外部实例化
    
    func fetchData() {
        // 可能因多线程访问导致数据竞争
    }
}

线程安全的 Singleton

// 线程安全版本(手动同步)
class NetworkManager {
    static let shared = NetworkManager()
    private let queue = DispatchQueue(label: "com.app.network", qos: .utility)
    private init() {}
    
    func fetchData(completion: @escaping (Data) -> Void) {
        queue.async {
            // 模拟网络请求
            completion(Data())
        }
    }
}

痛点:需要手动管理锁或队列,代码冗长且易出错。

什么是 Global Actor?

Global Actor 的核心价值

  • 自动线程安全:通过 @globalActor 标记,Swift 自动序列化代码执行。
  • 无缝集成 async/await:与 Swift 并发模型完美契合。

定义 Global Actor

// 1. 定义全局 Actor
@globalActor
actor NetworkActor {
    static let shared = NetworkActor()
}

// 2. 使用 Global Actor 的单例
@NetworkActor
class NetworkManager {
    static let shared = NetworkManager()
    private init() {}
    
    func fetchData() async -> Data {
        // 自动线程安全,无需手动锁
        return Data()
    }
}

调用 Global Actor 单例

Task {
    let data = await NetworkManager.shared.fetchData()
    print("数据大小: \(data.count)")
}

优势:无需 DispatchQueue,代码更简洁,且天然支持 async/await

深度对比:Singleton vs Global Actor

维度 传统 Singleton Global Actor Singleton
线程安全 需手动实现(锁/DispatchQueue) 自动序列化访问
代码复杂度 高(需处理同步逻辑) 低(标记 @ActorName即可)
async/await 支持 不直接支持(需回调或手动转换) 完全支持(原生 async/await)
灵活性 可精细控制并发粒度(如部分方法异步) 整个类强制序列化(可能过度保护)
性能 无额外开销(但锁竞争可能影响性能) 轻微调度开销(由 Swift 运行时优化)
适用场景 简单共享资源、遗留代码 高并发读写、现代 Swift 并发模型

何时选择传统 Singleton?

适用场景

  • 资源只读:如配置管理器,无并发修改风险。
  • 遗留项目:未迁移到 Swift 并发模型。
  • 精细控制:需自定义并发策略(如读写锁)。

示例:只读配置管理器

class AppConfig {
    static let shared = AppConfig()
    let apiBaseURL = "https://api.example.com"
    let timeout: TimeInterval = 30
    private init() {} // 只读无需线程安全
}

何时选择 Global Actor?

适用场景

  • 共享可变状态:如缓存、数据库写入。
  • 现代并发模型:已广泛使用 async/await
  • 减少心智负担:避免手动同步。

示例:线程安全的数据库

@globalActor
actor DatabaseActor {
    static let shared = DatabaseActor()
}

@DatabaseActor
class DatabaseManager {
    static let shared = DatabaseManager()
    private var cache: [String: Data] = [:]
    
    func save(key: String, value: Data) async {
        cache[key] = value
    }
    
    func fetch(key: String) async -> Data? {
        return cache[key]
    }
}

混合模式:全局访问 + 线程安全

场景:需要全局访问且高并发

// 定义 Actor
@globalActor
actor AnalyticsActor {
    static let shared = AnalyticsActor()
}

// 混合单例
@AnalyticsActor
class AnalyticsManager {
    static let shared = AnalyticsManager()
    private var events: [String] = []
    
    func track(event: String) async {
        events.append(event)
    }
    
    func getEvents() async -> [String] {
        return events
    }
}

调用示例

Task {
    await AnalyticsManager.shared.track(event: "user_login")
    let events = await AnalyticsManager.shared.getEvents()
}

性能对比与最佳实践

性能测试(简化版)

// 传统单例(DispatchQueue)
class TraditionalManager {
    static let shared = TraditionalManager()
    private let queue = DispatchQueue(label: "test")
    var counter = 0
    
    func increment() {
        queue.async {
            self.counter += 1
        }
    }
}

// Global Actor 单例
@globalActor
actor PerformanceActor {
    static let shared = PerformanceActor()
}

@PerformanceActor
class ActorManager {
    static let shared = ActorManager()
    var counter = 0
    
    func increment() async {
        counter += 1
    }
}

结果:Global Actor 在 高并发写入 时性能更稳定(无锁竞争)。

最佳实践总结

  • 新项目:优先 Global Actor(除非性能极端敏感)。
  • 旧项目:逐步迁移,可保留传统 Singleton。
  • 混合场景:Actor 管理可变状态,传统单例管理只读数据。

结论:现代 Swift 的正确姿势

选择 结论
传统 Singleton 仅适用于只读资源或遗留代码。
Global Actor 现代 Swift 的首选,线程安全且代码简洁。

一句话总结:

除非有明确理由(如遗留代码或极端性能需求),Global Actor 是管理共享可变资源的现代解决方案。它让开发者专注于业务逻辑,而非线程安全细节。

附录:迁移指南(传统 Singleton → Global Actor)

步骤 1:标记类为 Actor

// 旧代码
class OldManager {
    static let shared = OldManager()
    private init() {}
}

// 新代码
@globalActor
actor NewManager {
    static let shared = NewManager()
    private init() {}
}

步骤 2:调整调用方式

// 旧调用
OldManager.shared.doSomething()

// 新调用(需 await)
await NewManager.shared.doSomething()

步骤 3:处理回调转 async

// 旧回调方式
OldManager.shared.fetchData { result in
    // 处理结果
}

// 新 async 方式
let result = await NewManager.shared.fetchData()

Swift Global Actor 完全指南

作者 unravel2025
2025年8月26日 09:46

原文:Global actor in Swift Concurrency explained with code examples – SwiftLee

什么是 Global Actor?

概念 一句话解释
Actor 一种引用类型,串行化地执行对其状态的访问,天然线程安全。
Global Actor 全局唯一的 Actor,可被标注到 任意函数 / 属性 / 类型,让所有访问都在同一条串行队列上执行。

一句话:把「线程同步」这件事从手动加锁升级为「编译器帮你保证」。

系统自带:@MainActor

最常见、也是 iOS 开发者天天打交道的全局 Actor。

// 1. 标注函数 → 保证主线程
@MainActor
func updateUI() {
    label.text = "Done"      // 永远在主线程执行
}

// 2. 标注属性 → 所有读写都在主线程
@MainActor
var titles: [String] = []

// 3. 标注整个类 → 类内所有成员默认主线程
@MainActor
final class ContentViewModel {
    var titles: [String] = []
}

凡是 UI 更新、KVO、NotificationCenter 的 post/observe,都建议加上 @MainActor,省去手动 DispatchQueue.main.async

自定义 Global Actor:三步走

当你需要把 一类业务逻辑 放到同一条串行执行队列时(如图像处理、磁盘缓存、音频渲染…),自建全局 Actor 是利器。

Step 1:声明 Actor + @globalActor

@globalActor
actor ImageProcessing {
    static let shared = ImageProcessing()   // 必须实现
    private init() {}                      // ✅ 建议私有,防止乱 new
}
  • @globalActor 告诉编译器:这是一个「全局单例」Actor。
  • 必须提供 static let shared,否则会编译错误。
  • init() 设为 private,避免外部 ImageProcessing() 创建新实例。

Step 2:贴标签即可用

// 1. 标注类 → 类内所有成员自动走 ImageProcessing 队列
@ImageProcessing
final class ImageCache {
    private var store: [URL: Data] = [:]

    func store(_ data: Data, for url: URL) {
        store[url] = data          // 自动线程安全
    }
}

// 2. 标注单个函数
@ImageProcessing
func applyFilter(_ image: UIImage) -> UIImage {
    // 耗时滤镜计算
    return image
}

Step 3:跨 Actor 调用

@MainActor
class EditorViewModel {
    private let cache = ImageCache()   // 也在 ImageProcessing 上

    func saveThumbnail(for url: URL) async {
        let image = UIImage(data: try! Data(contentsOf: url))!
        let filtered = await applyFilter(image)   // ✅ 跨 Actor,用 await
        await cache.store(filtered.pngData()!, for: url)
    }
}
  • 任何跨 Actor 访问都要 await,编译器会强制你遵守。
  • 不再需要手动 DispatchQueue / NSLock,彻底告别数据竞争。

常见疑问 & 最佳实践

问题 解答
可以用在 struct / enum 吗? ✅ 可以,只要挂上 @YourGlobalActor
能继承吗? ❌ Actor 是引用类型,不能继承;但你可以把 Actor 包装在类里。

Swift 计算属性(Computed Property)详解:原理、性能与实战

作者 unravel2025
2025年8月26日 09:37

原文:What is a Computed Property in Swift? – SwiftLee

什么是计算属性?

Swift 中的属性分为两大族谱:

类型 描述 存储值
Stored Property(存储属性) 保存一个固定的值,最常见
Computed Property(计算属性) 每次被访问时实时计算出一个值,不占用额外存储

计算属性的核心特征:“算完即走,不落痕迹”。

它通过 getter(必含)和可选的 setter 来间接读取或修改其他属性。

只读计算属性:最常见形态

典型场景:基于已有属性生成新值

struct Content {
    var name: String
    let fileExtension: String

    // 计算属性:拼接文件名
    var filename: String {
        name + "." + fileExtension   // 单行可省略 return
    }
}

let content = Content(name: "swiftlee-banner", fileExtension: "png")
print(content.filename)  // swiftlee-banner.png
  • filename 是 只读 的,无法赋值:content.filename = "new.png" 会编译错误。
  • 若显式写 get,代码更冗余,不推荐:
var filename: String {
    get { name + "." + fileExtension }
}

可读可写计算属性:暴露私有模型的接口

有时我们想把复杂的模型隐藏在内部,只暴露一个“代理”属性供外部读写——计算属性就能优雅完成。

struct ContentViewModel {
    private var content: Content   // 真正的数据模型对外不可见

    init(_ content: Content) {
        self.content = content
    }

    // 计算属性:既读又写,内部转发到 content.name
    var name: String {
        get { content.name }
        set { content.name = newValue }   // newValue 是 Swift 的默认形参
    }
}

var content = Content(name: "swiftlee-banner", fileExtension: "png")
var viewModel = ContentViewModel(content)
viewModel.name = "SwiftLee Post"
print(viewModel.name)  // SwiftLee Post

效果:调用者只知道 name,却不知道内部还有一个复杂的 Content 对象。

在 Extension 中使用计算属性:无痛加功能

计算属性可以写在 extension 里,为现有类型(尤其是系统类型)增加无痛扩展。

import UIKit

extension UIView {
    // 快速访问 frame 尺寸
    var width: CGFloat {
        frame.size.width
    }

    var height: CGFloat {
        frame.size.height
    }
}

let view = UIView(frame: CGRect(x: 0, y: 0, width: 320, height: 480))
print(view.width)  // 320

优点:无需继承,即刻生效。

在子类中重写计算属性

计算属性还可以 被 override,常用于定制 UIKit 行为。

最简单:直接硬编码

final class HomeViewController: UIViewController {
    override var prefersStatusBarHidden: Bool { true }
}

进阶:由内部存储属性驱动

final class HomeViewController: UIViewController {
    private var shouldHideStatusBar: Bool = true {
        didSet {
            // 状态改变后刷新系统样式
            setNeedsStatusBarAppearanceUpdate()
        }
    }

    override var prefersStatusBarHidden: Bool { shouldHideStatusBar }
}

何时使用计算属性?官方推荐 3 个条件

条件 示例场景
值依赖其他属性 上文的 filename
在 extension 中定义 给 UIView加 width/height
作为内部对象的受控访问点 ContentViewModel.name

个人补充:若计算逻辑 纯静态、且 无状态依赖,考虑直接声明为 static let,避免每次调用重新计算。

性能陷阱:每次访问都会重新计算

计算属性 不会缓存结果,高频访问 + 重计算 = 性能灾难。

反面教材:每次都排序

struct PeopleViewModel {
    let people: [Person]

    var oldest: Person? {
        people.sorted { $0.age > $1.age }.first   // O(n log n) 每次都要跑
    }
}

优化:移入初始化器,只算一次

struct PeopleViewModel {
    let people: [Person]
    let oldest: Person?

    init(people: [Person]) {
        self.people = people
        oldest = people.max(by: { $0.age < $1.age })   // 或者自己实现一次遍历找最大值
    }
}

经验法则:

  • 数据量小 or 变化频繁 → 计算属性
  • 数据量大 or 代价高昂 → 存储属性 + 预计算

计算属性 VS 方法:如何抉择?

维度 计算属性 方法 (func)
参数 可接受参数
可读性暗示 “轻量级值” “可能耗时”
测试/模拟 不方便 mock 容易 stub/mock
适用场景 简单、无参数、依赖内部状态 复杂、需参数、可能异步或耗时

一句话:重逻辑用方法,轻数据用属性。

总结 & 扩展场景

核心结论

  1. 计算属性 = 无存储 + 实时计算 + 可选 setter。
  2. 带来 语义化 API 与 封装性,但 不缓存。
  3. 在 extension、子类 override、MVVM 视图模型中大放异彩。

扩展实战场景

场景 代码示例 & 注释
格式化输出 var displayPrice: String { "(price)$" }
链式依赖 var isAdult: Bool { age >= 18 }→ var canDrink: Bool { isAdult }
Core Data 轻量级封装 在 NSManagedObject 的 extension 中,把 primitiveValue包装成计算属性,隐藏 KVC 细节
SwiftUI 绑定 在 ObservableObject 中,用计算属性把 @Published的私有变量暴露为 public 接口
缓存友好型计算属性 结合 lazy或自定义缓存字典,实现 “第一次算,之后读” 的懒加载计算属性

Swift Property Wrapper:优雅地消除样板代码

作者 unravel2025
2025年8月26日 09:22

原文链接:www.avanderlee.com/swift/prope…

为什么会出现 Property Wrapper?

在业务代码里,我们经常写出大量 重复的模式:

var username: String {
    get { UserDefaults.standard.string(forKey: "username") ?? "guest" }
    set { UserDefaults.standard.set(newValue, forKey: "username") }
}

当属性越来越多时,样板代码 呈指数级增长。

Apple 在 WWDC 2019 引入 Property Wrapper(SE-0258),把“如何存取值”这一横切关注点抽象出来,封装成可复用的 包装类型。

什么是 Property Wrapper?

Property Wrapper 是一个带 @propertyWrapper 标注的结构体/类,它决定了被包装属性的存储与读取方式。

核心必须实现:

@propertyWrapper
struct Wrapper<T> {
    var wrappedValue: T   // 真正读写的值
    // 可选:var projectedValue: SomeType  // 投影值,用于暴露更多能力
}

实战:UserDefaults 的 Property Wrapper

传统写法(痛点)

extension UserDefaults {
    enum Keys {
        static let hasSeenAppIntroduction = "has_seen_app_introduction"
    }

    var hasSeenAppIntroduction: Bool {
        get { bool(forKey: Keys.hasSeenAppIntroduction) }
        set { set(newValue, forKey: Keys.hasSeenAppIntroduction) }
    }
}

缺点

  • 每个属性都要写一遍 get / set
  • Key 字符串散落各处,易错

封装成 @UserDefault

@propertyWrapper
struct UserDefault<Value> {
    let key: String
    let defaultValue: Value
    var container: UserDefaults = .standard

    var wrappedValue: Value {
        get {
            container.object(forKey: key) as? Value ?? defaultValue
        }
        set {
            container.set(newValue, forKey: key)
        }
    }
}

使用:

extension UserDefaults {
    @UserDefault(key: "has_seen_app_introduction", defaultValue: false)
    static var hasSeenAppIntroduction: Bool

    @UserDefault(key: "username", defaultValue: "Antoine")
    static var username: String
}

一行即可声明,零样板!

进阶:支持可选值 & 移除 Key

Swift 的泛型不支持“可选与非可选”同时满足,需要一点技巧:

public protocol AnyOptional {
    var isNil: Bool { get }
}
extension Optional: AnyOptional {
    public var isNil: Bool { self == nil }
}

改造 setter:

var wrappedValue: Value {
    get { ... }
    set {
        if let optional = newValue as? AnyOptional, optional.isNil {
            container.removeObject(forKey: key)   // 置 nil 时删除
        } else {
            container.set(newValue, forKey: key)
        }
    }
}

于是支持:

@UserDefault(key: "year_of_birth")
static var yearOfBirth: Int?

UserDefaults.yearOfBirth = nil   // 自动删除 key

Projected Value:把属性变成 Publisher

有时我们想 监听 变化,Combine 友好:

import Combine

@propertyWrapper
struct UserDefault<Value> {
    ...
    private let publisher = PassthroughSubject<Value, Never>()

    var wrappedValue: Value { ... publisher.send(newValue) }

    var projectedValue: AnyPublisher<Value, Never> {
        publisher.eraseToAnyPublisher()
    }
}

订阅:

let cancellable = UserDefaults.$username.sink {
    print("用户名变为:\($0)")
}
UserDefaults.username = "新名字"
// 控制台:用户名变为:新名字

访问包装器本体

Swift 预留了两个魔法前缀:

前缀 说明 示例
_ 直接访问包装器实例 _username.key
$ 访问 projectedValue $username(即 AnyPublisher)
extension UserDefaults {
    static func debugKeys() {
        print(_username.key)   // "username"
        print($username)       // AnyPublisher
    }
}

在函数参数里用 Property Wrapper

@propertyWrapper
struct Debuggable<Value> {
    init(wrappedValue: Value, description: String = "") { ... }
    var wrappedValue: Value { ... }
}

func animate(@Debuggable(description: "动画时长") duration: Double) {
    UIView.animate(withDuration: duration) { ... }
}

animate(withDuration: 2.0)
// 控制台:
// Initialized '动画时长' with value 2.0
// Accessing '动画时长', returning: 2.0

调试神器!

更多灵感

  • @UsesAutoLayout var label = UILabel()

    自动把 translatesAutoresizingMaskIntoConstraints = false

  • @SampleFile(fileName: "avatar.jpg") var avatarURL: URL

    一键获取 Bundle 内资源 URL

小结

能力 传统写法 Property Wrapper
去除重复
类型安全
可测试性
组合能力(Combine、Debug...)

一句话总结:

只要发现属性读写有重复模式,就考虑封装成 Property Wrapper,让 Swift 帮你写样板代码!

Swift Concurrency:彻底告别“线程思维”,拥抱 Task 的世界

作者 unravel2025
2025年8月19日 09:40

原文:Threads vs. Tasks in Swift Concurrency 链接:www.avanderlee.com/concurrency…

前言:别再问“它跑在哪个线程?”

在 GCD 时代,我们习惯用 DispatchQueue.global(qos: .background).async { ... }DispatchQueue.main.async { ... } 来显式地把任务丢到指定线程。久而久之,形成了一种“线程思维”:

“这段代码很重,我要放到子线程。”

“这行 UI 代码必须回到主线程。”

Swift Concurrency(async/await + Task)出现以后,这套思维需要升级——系统帮你决定“跑在哪个线程”。我们只需关心“任务(Task)”本身。

线程(Thread)到底是什么?

  • 系统级资源:由操作系统调度,创建、销毁、切换开销大。
  • 并发手段:多线程可以让多条指令流同时跑。
  • 痛点:数量一多,内存占用高、上下文切换频繁、优先级反转。

Swift Concurrency 的目标就是让我们 不再直接面对线程。

Task:比线程更高级的抽象

  • 一个 Task = 一段异步工作单元。
  • 不绑定线程:Task 被放进 合作线程池(cooperative thread pool),由运行时动态分配到“刚好够用”的线程上。
  • 运行机制:
    1. 线程数量 ≈ CPU 核心数。
    2. 遇到 await(挂起点)时,当前线程被释放,可立即执行其他 Task。
    3. 挂起的 Task 稍后可能在另一条线程恢复。

代码示范:Task 与线程的“若即若离”

struct ThreadingDemonstrator {
    private func firstTask() async throws {
        print("Task 1 started on thread: \(Thread.current)")
        try await Task.sleep(for: .seconds(2))   // 挂起点
        print("Task 1 resumed on thread: \(Thread.current)")
    }

    private func secondTask() async {
        print("Task 2 started on thread: \(Thread.current)")
    }

    func demonstrate() {
        Task {
            try await firstTask()
        }
        Task {
            await secondTask()
        }
    }
}

典型输出(每次都可能不同):

Task 1 started on thread: <NSThread: 0x600001752200>{number = 3, name = (null)}
Task 2 started on thread: <NSThread: 0x6000017b03c0>{number = 8, name = (null)}
Task 1 resumed on thread: <NSThread: 0x60000176ecc0>{number = 7, name = (null)}

解读:

  • Task 1 在 await 时释放了线程 3;
  • Task 2 趁机用到了线程 8;
  • Task 1 恢复时,被安排到线程 7——前后线程可以不同。

线程爆炸(Thread Explosion)还会发生吗?

场景 GCD Swift Concurrency
同时发起 1000 个网络请求 可能创建 1000 条线程 → 内存暴涨、调度爆炸 最多 CPU 核心数条线程,其余任务挂起 → 无爆炸
阻塞线程 线程真被 block,CPU 空转 用 continuation 挂起,线程立刻服务别的任务

因此,线程爆炸在 Swift Concurrency 中几乎不存在。

线程更少,性能反而更好?

  • GCD 误区:线程越多,并发越高。
  • 真相:线程 > CPU 核心时,上下文切换成本激增。
  • Swift Concurrency 做法
    • 线程数 = 核心数;
    • 用挂起/恢复代替阻塞;
    • CPU 始终在跑有效指令,切换开销极低。

实测常见场景(CPU-bound & I/O-bound)下,Swift Concurrency 往往优于 GCD。

三个常见误区

误区 正解
每个 Task 会新开一条线程 Task 与线程是多对一,由调度器动态复用
await会阻塞当前线程 await会挂起任务并释放线程
Task 一定按创建顺序执行 执行顺序不保证,取决于挂起点与调度策略

思维升级:从“线程思维”到“任务思维”

线程思维 任务思维
“这段代码要在子线程跑” “这段代码是异步任务,系统会调度”
“回到主线程刷新 UI” “用 @MainActor或 MainActor.run标记主界面任务”
“我怕线程太多” “线程数系统自动管理,我专注业务逻辑”

小结

  1. 线程是低层、昂贵的系统资源。
  2. Task 是高层、轻量的异步工作单元。
  3. Swift Concurrency 通过合作线程池 + 挂起/恢复机制,让线程数始终保持在“刚好够用”,既避免线程爆炸,又提升性能。
  4. 开发者应把注意力从“线程”转向“任务”与“挂起点”。

当你下次再想问“这段代码跑在哪个线程?”时,提醒自己:

“别管线程,写正确的 Task 就行。”

深入理解 Swift 中的 async/await:告别回调地狱,拥抱结构化并发

作者 unravel2025
2025年8月19日 09:32

原文:Async await in Swift explained with code examples

Swift 5.5 在 WWDC 2021 中引入了 async/await,随后在 Swift 6 中进一步完善,成为现代 iOS 开发中处理并发的核心工具。它不仅让异步代码更易读写,还彻底改变了我们组织并发任务的方式。

什么是 async?

async 是一个方法修饰符,表示该方法是异步执行的,即不会阻塞当前线程,而是挂起等待结果。

✅ 示例:定义一个 async 方法

func fetchImages() async throws -> [UIImage] {
    // 模拟网络请求
    let data = try await URLSession.shared.data(from: URL(string: "https://example.com/images")!).0
    return try JSONDecoder().decode([UIImage].self, from: data)
}
  • async 表示异步执行;
  • throws 表示可能抛出错误;
  • 返回值是 [UIImage]
  • 调用时需要用 await 等待结果。

什么是 await?

await 是调用 async 方法时必须使用的关键字,表示“等待异步结果”。

✅ 示例:使用 await 调用 async 方法

do {
    let images = try await fetchImages()
    print("成功获取 \(images.count) 张图片")
} catch {
    print("获取图片失败:\(error)")
}
  • 使用 try await 等待异步结果;
  • 错误用 catch 捕获;
  • 代码顺序执行,逻辑清晰。

async/await 如何替代回调地狱?

在 async/await 出现之前,异步操作通常使用回调闭包,这会导致回调地狱(Callback Hell):

❌ 旧写法:嵌套回调

fetchImages { result in
    switch result {
    case .success(let images):
        resizeImages(images) { result in
            switch result {
            case .success(let resized):
                print("处理完成:\(resized.count) 张图片")
            case .failure(let error):
                print("处理失败:\(error)")
            }
        }
    case .failure(let error):
        print("获取失败:\(error)")
    }
}

✅ 新写法:线性结构

do {
    let images = try await fetchImages()
    let resizedImages = try await resizeImages(images)
    print("处理完成:\(resizedImages.count) 张图片")
} catch {
    print("处理失败:\(error)")
}
  • 没有嵌套;
  • 顺序清晰;
  • 更易于维护和测试。

在非并发环境中调用 async 方法

如果你尝试在同步函数中直接调用 async 方法,会报错:

'async' call in a function that does not support concurrency

✅ 解决方案:使用 Task

final class ContentViewModel: ObservableObject {
    @Published var images: [UIImage] = []

    func fetchData() {
        Task { @MainActor in
            do {
                self.images = try await fetchImages()
            } catch {
                print("获取失败:\(error)")
            }
        }
    }
}
  • Task {} 创建一个新的异步上下文;
  • @MainActor 保证 UI 更新在主线程;
  • 适用于 SwiftUI 或 UIKit。

如何在旧项目中逐步迁移?

Xcode 提供了三种自动重构方式,帮助你从旧回调方式迁移到 async/await:

✅ 方式一:Convert Function to Async

直接替换旧方法,不保留旧实现:

// 旧
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void)

// 新
func fetchImages() async throws -> [UIImage]

✅ 方式二:Add Async Alternative

保留旧方法,并添加新 async 方法,使用 @available 标记:

@available(*, deprecated, renamed: "fetchImages()")
func fetchImages(completion: @escaping (Result<[UIImage], Error>) -> Void) {
    Task {
        do {
            let result = try await fetchImages()
            completion(.success(result))
        } catch {
            completion(.failure(error))
        }
    }
}

func fetchImages() async throws -> [UIImage] {
    // 新实现
}
  • 旧方法调用会提示警告;
  • 支持逐步迁移;
  • 不破坏现有代码。

✅ 方式三:Add Async Wrapper

使用 withCheckedThrowingContinuation 包装旧方法:

func fetchImages() async throws -> [UIImage] {
    try await withCheckedThrowingContinuation { continuation in
        fetchImages { result in
            continuation.resume(with: result)
        }
    }
}
  • 无需改动旧实现;
  • 适合第三方库或无法修改的代码。

async/await 会取代 Result 枚举吗?

虽然 async/await 让 Result 枚举看起来不再必要,但它不会立即消失。很多老代码和第三方库仍在使用 Result,但未来可能会逐步弃用。

迁移建议:先 async,再 Swift 6

  • Swift 6 引入了更强的并发安全检查;
  • 建议先迁移到 async/await,再升级到 Swift 6;
  • 使用 @preconcurrency@Sendable 等工具逐步迁移。

总结:async/await 带来的改变

特性 回调方式 async/await
可读性 差(嵌套) 好(线性)
错误处理 手动 Result try/catch
并发控制 手动管理 结构化
测试难度
与 SwiftUI 集成 复杂 自然

深入理解 SwiftUI 的 ViewBuilder:从隐式语法到自定义容器

作者 unravel2025
2025年8月19日 08:40

SwiftUI 的声明式语法之所以优雅,一大功臣是隐藏在幕后的 ViewBuilder。它让我们可以在 bodyHStackVStack 等容器的闭包里随意组合多个视图,而无需手动把它们包进 GroupTupleView

ViewBuilder 是什么?

ViewBuilder 是一个 结果构建器(Result Builder),负责把 DSL(领域特定语言)中的多条表达式“构建”成单个视图。它最常出现的场景:

VStack {
    Image(systemName: "star")
    Text("Hello, world!")
}

我们并没有显式写 ViewBuilder.buildBlock(...),却能在 VStack 的尾随闭包里放两个视图,这就是 @ViewBuilder 的魔力。

实际上,View 协议已经把 body 标记成了 @ViewBuilder

@ViewBuilder var body: Self.Body { get }

所以下面这样写也完全合法:

var body: some View {
    if user != nil {
        HomeView(user: user!)
    } else {
        LoginView(user: $user)
    }
}

即使 if 的两个分支返回不同类型,ViewBuilder 也能通过 buildEither 等内部方法把它们擦除为 AnyView_ConditionalContent,最终呈现出单一根视图。

给自己的 API 加上 @ViewBuilder:自定义容器

想让自定义容器也支持 DSL 语法?只需在属性或闭包参数前加 @ViewBuilder

基本用法:把属性变成视图构建闭包

struct Container<Header: View, Content: View>: View {
    @ViewBuilder var header: Header
    @ViewBuilder var content: Content

    var body: some View {
        VStack(spacing: 0) {
            header
                .frame(maxWidth: .infinity)
                .padding()
                .foregroundStyle(.white)
                .background(.blue)

            ScrollView { content.padding() }
        }
    }
}

调用方式立即变得“SwiftUI 味儿”:

Container(header: {
    Text("Welcome")
}, content: {
    if let user {
        HomeView(user: user)
    } else {
        LoginView(user: $user)
    }
})

让 header 可选:两种做法

做法 A:带约束的扩展

extension Container where Header == EmptyView {
    init(@ViewBuilder content: () -> Content) {
        self.init(header: EmptyView.init, content: content)
    }
}

现在可以这样写:

Container {
    LoginView(user: $user)
}

做法 B:默认参数 + 手动调用闭包

struct Container<Header: View, Content: View>: View {
    private let header: Header
    private let content: Content

    init(@ViewBuilder header: () -> Header = EmptyView.init,
         @ViewBuilder content: () -> Content) {
        self.header = header()
        self.content = content()
    }

    var body: some View { ... }
}

优点:

  • 不需要额外扩展;
  • 可以在未来继续添加默认参数;
  • 闭包在 init 就被执行,避免 body 反复求值带来的性能损耗。

多条表达式与隐式 Group

当闭包里出现多条顶层表达式时,ViewBuilder 会把它们当成 Group 的子视图。例如:

Container(header: {
    Text("Welcome")
    NavigationLink("Info") { InfoView() }
}, content: { ... })

实际上得到的是两个独立的 header 视图,各自撑满宽度,而不是一个整体。解决方式:

  1. 在容器内部用显式 VStack 再包一层:
VStack(spacing: 0) {
    VStack { header }   // 👈 统一布局
        .frame(maxWidth: .infinity)
        .padding()
        .background(.blue)

    ScrollView {
        VStack { content }.padding()
    }
}
  1. 或者在调用方显式组合:
private extension RootView {
    func header() -> some View {
        VStack(spacing: 20) {
            Text("Welcome")
            NavigationLink("Info") {
                InfoView()
            }
        }
    }
}

小建议:

如果函数/计算属性返回“一个整体”视图,最好显式用 VStackHStack 等包装,而不是依赖 @ViewBuilder 隐式 Group。语义更清晰,布局也更稳定。

把 ViewBuilder 当“代码组织工具”

body 越来越复杂时,可以把子区域拆成私有的 @ViewBuilder 方法:

struct RootView: View {
    @State private var user: User?

    var body: some View {
        Container(header: header, content: content)
    }
}

private extension RootView {
    @ViewBuilder
    func content() -> some View {
        if let user {
            HomeView(user: user)
        } else {
            LoginView(user: $user)
        }
    }
}

注意:如果 header() 需要返回多个兄弟视图,则推荐返回显式容器,而不是 @ViewBuilder

func header() -> some View {
    VStack(spacing: 20) { ... }
}

要点回顾

场景技巧 让属性支持 DSL在属性前加 @ViewBuilder 让参数支持 DSL在闭包参数前加 @ViewBuilder,并在 init 内手动执行 可选组件使用 EmptyView.init 作为默认值或约束扩展 多条表达式记住隐式 Group 行为,必要时显式包一层容器 代码组织用 @ViewBuilder 拆分 body,但根视图最好显式容器

结语

ViewBuilder 把 SwiftUI 的声明式语法推向了“像写普通 Swift 代码一样自然”的高度。当我们为自定义容器、可复用组件也加上 @ViewBuilder 时,API 就能与系统控件保持一致的体验,既易读又易维护。

下次写 SwiftUI 时,不妨问问自己:“这段代码能不能也让调用者用 ViewBuilder 的语法糖?” 如果答案是肯定的,就把 @ViewBuilder 加上去吧!

在 async/throwing 场景下优雅地使用 Swift 的 defer 关键字

作者 unravel2025
2025年8月19日 08:31

原文:Using Swift’s defer keyword within async and throwing contexts – Swift by Sundell

在日常 Swift 开发中,我们经常需要在多出口的函数里做清理工作:关闭文件句柄、归还数据库连接、把布尔值复原……如果每个出口都手写一遍,既啰嗦又容易遗漏。

Swift 提供了 defer 关键字,让我们可以把“善后逻辑”一次性声明在当前作用域顶部,却延迟到作用域退出时才执行。

本文将结合错误抛出(throwing)与并发(async/await)两个典型场景,带你彻底吃透 defer 的用法与注意点。

defer 基础回顾

defer { ... } 中的代码,会等到当前作用域(函数、闭包、do 块……)即将退出时执行,无论退出路径是 return、throw 还是 break。

最小示例:

func demo() {
    defer { print("最后才打印") }
    print("先打印")
    // 函数返回前,defer 里的内容一定执行
}

同步 + throwing 场景:避免重复清理

想象一个 SearchService,它通过 Database API 查询条目,必须先 open、后 close:

❌ 传统写法:分支重复

actor SearchService {
    private let database: Database

    func loadItems(matching searchString: String) throws -> [Item] {
        let connection = database.connect()

        do {
            let items: [Item] = try connection.runQuery(
                .entries(matching: searchString)
            )
            connection.close()          // 成功路径
            return items
        } catch {
            connection.close()          // 失败路径
            throw error
        }
    }
}

问题:

  • 两处 close(),容易漏写。
  • 如果再加 returnguard,分支会更多。

✅ 利用 defer:把 close 写在 open 旁边

func loadItems(matching searchString: String) throws -> [Item] {
    let connection = database.connect()
    defer { connection.close() }          // 一次声明,处处生效

    return try connection.runQuery(.entries(matching: searchString))
}

优点:

  • 逻辑集中,一眼可见“成对动作”。
  • 任意新增提前退出(guardthrow)都不用再管 close()

⚠️ 注意执行顺序:

connect()runQuery() → 作用域结束 → close()

“延迟”并不代表“立刻”,代码阅读时需要适应这种跳跃。

async/await 场景:状态复原与去重

并发代码里,defer 更显价值——异步函数可能在任意 await 点挂起并抛错,手动追踪所有出口几乎不现实。

复原布尔 flag

场景:防止重复加载,用 isLoading 标记。

❌ 传统写法:catch 里回写 flag

actor ItemListService {
    private let networking: NetworkingService
    private var isLoading = false

    func loadItems(after lastItem: Item) async throws -> [Item] {
        guard !isLoading else { throw Error.alreadyLoading }
        isLoading = true

        do {
            let request  = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            let items    = try response.decoded() as [Item]

            isLoading = false        // 成功路径
            return items
        } catch {
            isLoading = false        // 失败路径
            throw error
        }
    }
}

✅ defer 写法:一行搞定

func loadItems(after lastItem: Item) async throws -> [Item] {
    guard !isLoading else { throw LoadingError.alreadyLoading }
    isLoading = true
    defer { isLoading = false }       // 无论成功/失败都会执行

    let request  = requestForLoadingItems(after: lastItem)
    let response = try await networking.performRequest(request)
    return try response.decoded()
}

利用 Task + defer 做“去重”

需求:如果同一 lastItem.id 的加载任务已在进行中,直接等待现有任务,而不是重新发起。

actor ItemListService {
    private let networking: NetworkingService
    private var activeTasks: [Item.ID: Task<[Item], Error>] = [:]

    func loadItems(after lastItem: Item) async throws -> [Item] {
        // 1. 已有任务则等待
        if let existing = activeTasks[lastItem.id] {
            return try await existing.value
        }

        // 2. 创建新任务
        let task = Task {
            // 任务结束前一定清理字典
            defer { activeTasks[lastItem.id] = nil }

            let request  = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            return try response.decoded() as [Item]
        }

        // 3. 登记
        activeTasks[lastItem.id] = task
        return try await task.value
    }
}

要点:

  • 一旦 Task 结束(无论正常返回还是抛错),defer 把字典条目删掉,防止内存泄漏。
  • 因为 actor 的重入性(reentrancy),await 期间仍可接受新调用;通过字典+Task 实现“幂等”效果。

何时不要使用 defer

  • 逻辑需要严格顺序时:defer 会在作用域最后执行,若必须与中间语句保持先后关系,则不适合。
  • 过度嵌套:多层 defer 会让执行顺序难以一眼看出,阅读负担大。
  • 性能极端敏感:defer 本质是隐藏的 try/finally,有微小开销,但通常可以忽略。

小结 checklist

场景是否推荐 defer 单一出口函数❌ 没必要 多出口、需清理资源✅ 强烈推荐 async/await 中状态复原✅ 强烈推荐 需要精确控制顺序❌ 慎用

一句话:defer 是“善后”利器,不是“流程”利器。

只要牢记“无论怎么退出,这段代码一定跑”,就能把它用得恰到好处。

当Swift Codable遇到缺失字段:优雅解决数据解码难题

作者 unravel2025
2025年8月18日 20:48

在Swift开发中,我们经常使用Codable协议轻松实现JSON数据与模型对象的自动转换。

但实际开发中常会遇到这种棘手问题:需要解码的模型中包含某些字段,但这些关键数据却不在当前接收到的JSON中。

本文将通过具体案例,深入探讨三种优雅解决方案及其适用场景。

问题的本质

假设我们有如下User模型:

struct User: Identifiable {
   let id: UUID
   var name: String
   var membershipPoints: Int
   var favorites: Favorites
}

struct Favorites: Codable { 
    var genre: String 
    var directorName: String 
    var movieIDs: [String] 
}

服务器返回的JSON数据只包含基础信息:

{
   "id": "7CBE0CC1-7779-42E9-AAF1-C4B145F3CAE9",
   "name": "John Appleseed",
   "membershipPoints": 192
}

而Favorites数据需要单独请求获取:

{
   "genre": "action",
   "directorName": "Christopher Nolan",
   "movieIDs": [
       "F028CAB5-74D7-4B86-8450-D0046C32DFA0",
       "D2657C95-1A35-446C-97D4-FAAA4783F2AA"
   ]
}

这时候直接使用Codable会出现什么问题?尝试解码时会因为缺少favorites字段导致崩溃。

方案一:可选属性(权宜之计)

最简单的解决办法是将favorites设为可选类型:

var favorites: Favorites?

优点

  • 实现简单,无需额外代码
  • 编译器不会报错

缺点

  • 模型变得脆弱,容易产生未初始化状态
  • 使用时必须频繁解包(user.favorites?.genre ?? "未知"
  • 无法保证数据完整性,可能导致后续逻辑错误

方案二:中间模型+数据合并(折中方案)

定义一个仅包含公共字段的Partial模型:

extension User {
   struct Partial: Decodable {
       let id: UUID
       var name: String
       var membershipPoints: Int
   }
}

网络请求时同时获取两部分数据:

func loadUser(id: UUID) async throws -> User {
   let (partialData, favoritesData) = try await Task.group {
       URLSession.shared.data(from: userURL(id))
       URLSession.shared.data(from: favoritesURL(id))
   }
   
   let partial = try JSONDecoder().decode(User.Partial.self, from: partialData)
   let favorites = try JSONDecoder().decode(Favorites.self, from: favoritesData)
   
   return User(
       id: partial.id,
       name: partial.name,
       membershipPoints: partial.membershipPoints,
       favorites: favorites
   )
}

优点

  • 保持原有模型完整性
  • 明确区分不同来源的数据

缺点

  • 需要维护额外的中间模型
  • 代码量增加约30%
  • 异步合并逻辑稍显复杂

方案三:CodableWithConfiguration(完美方案)

利用Swift 5.7引入的CodableWithConfiguration特性:

extension User: DecodableWithConfiguration {
    // 告诉编译器:我需要一个 Favorites 作为解码配置
    typealias DecodingConfiguration = Favorites
    
    enum CodingKeys: CodingKey {
        case id, name, membershipPoints
    }
    
    init(from decoder: Decoder, configuration: Favorites) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        name = try container.decode(String.self, forKey: .name)
        membershipPoints = try container.decode(Int.self, forKey: .membershipPoints)
        favorites = configuration
    }
}

向下兼容:iOS 15/16 也能用 自定义JSONDecoder扩展以支持配置传递:

extension JSONDecoder {
    private struct Wrapper<T: DecodableWithConfiguration>: Decodable {
        let value: T
        init(from decoder: Decoder) throws {
            let config = decoder.userInfo[.configKey] as! T.DecodingConfiguration
            value = try T(from: decoder, configuration: config)
        }
    }

    func decode<T: DecodableWithConfiguration>(
        _ type: T.Type,
        from data: Data,
        configuration: T.DecodingConfiguration
    ) throws -> T {
        userInfo[.configKey] = configuration
        return try decode(Wrapper<T>.self, from: data).value
    }
}

private extension CodingUserInfoKey {
    static let configKey = CodingUserInfoKey(rawValue: "configuration")!
}

使用时只需一行代码即可完成配置注入:

func loadUser() throws -> User {
    let favoriteData = """
    {
      "genre": "action",
      "directorName": "Christopher Nolan",
      "movieIDs": ["7CBE0CC1-7779-42E9-AAF1-C4B145F3CAE9"]
    }
""".data(using: .utf8)!
    let favorites: Favorites = try JSONDecoder().decode(Favorites.self, from: favoriteData)
    // ↓ 直接把 favorites 当 configuration 传进去
    let userData = """
        {
          "id": "7CBE0CC1-7779-42E9-AAF1-C4B145F3CAE9",
          "name": "John Appleseed",
          "membershipPoints": 192
        }
""".data(using: .utf8)!
    return try JSONDecoder().decode(
        User.self,
        from: userData,
        configuration: favorites
    )
}

do {
    let u = try loadUser()
    print(u)
}

技术对比与选择建议

特性 可选属性 中间模型 CodableWithConfiguration
实现复杂度 ★☆☆☆☆ ★★☆☆☆ ★★★★☆
代码侵入性
运行时安全性 ⚠️潜在风险 ✅安全可靠 ✅绝对安全
类型系统支持 部分 完整
iOS版本要求 全平台支持 全平台支持 iOS 17+/Swift 5.7+

推荐使用场景

  • 紧急修复:可选属性适合快速验证原型
  • 团队协作:中间模型适合多人协作项目
  • 生产环境:CodableWithConfiguration适合追求代码质量的长期项目

通过合理选择技术方案,我们可以在保证代码质量的同时,优雅地解决这类数据解码难题。每种方案都有其适用场景,关键是根据项目实际情况做出最佳权衡。

用 SwiftUI 打造“会长大”的组件 —— 从一次性 Alert 到可扩展设计系统

作者 unravel2025
2025年8月12日 10:43

原文链接

为什么旧写法撑不过三次迭代?

先来看一个“经典”写法

Alert(
    title: "Title",
    message: "Description",
    type: .info,
    showBorder: true,
    isDisabled: false,
    primaryButtonTitle: "OK",
    secondaryButtonTitle: "Cancel",
    primaryAction: { /* ... */ },
    secondaryAction: { /* ... */ }
)

痛点一句话总结:初始化即地狱。

• 参数爆炸,阅读困难

• 布局/样式/行为耦合,一改全改

• 无法注入自定义内容,复用性 ≈ 0

目标:像原生一样的 SwiftUI 组件

我们想要的最终形态:

AlertView(title: "...", message: "...") {
    AnyViewBuilder Content
}
.showBorder(true)
.disabled(isLoading)

为此,需要遵循 4 个关键词:

  1. Familiar APIs – 看起来像 SwiftUI 自带的
  2. Composability – 任意组合内容
  3. Scalability – 业务扩张不炸窝
  4. Accessibility – 无障碍不打补丁

三步重构法

Step 1:只保留「必须参数」

public struct AlertView: View {
    private let title: String
    private let message: String
    
    public init(title: String, message: String) {
        self.title = title
        self.message = message
    }
    
    public var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(title).font(.headline)
            Text(message).font(.subheadline)
        }
        .padding()
    }
}

经验:先把最常用、不可省略的参数放进 init,其余全部踢出去。这一步就能干掉 70% 的参数。

Step 2:用 @ViewBuilder 把“内容”交出去

public struct AlertView<Footer: View>: View {
    private let title: String
    private let message: String
    private let footer: Footer
    
    public init(
        title: String,
        message: String,
        @ViewBuilder footer: () -> Footer
    ) {
        self.title = title
        self.message = message
        self.footer = footer()
    }
    
    public var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(title).font(.headline)
            Text(message).font(.subheadline)
            footer.padding(.top, 25)
        }
        .padding()
    }
}

使用:

AlertView(title: "提示", message: "确定删除吗?") {
    HStack {
        Button("取消", role: .cancel) {}
        Button("删除", role: .destructive) {}
    }
}

Step 3:样式/行为用 环境值 + 自定义修饰符

我们想让边框可开关,但又不想回到“参数爆炸”。

struct ShowBorderKey: EnvironmentKey {
    static let defaultValue = false
}

extension EnvironmentValues {
    var showBorder: Bool {
        get { self[ShowBorderKey.self] }
        set { self[ShowBorderKey.self] = newValue }
    }
}

extension View {
    public func showBorder(_ value: Bool) -> some View {
        environment(\.showBorder, value)
    }
}

在 AlertView 内部读取

@Environment(\.showBorder) private var showBorder

// …
.overlay(
    RoundedRectangle(cornerRadius: 12)
        .stroke(Color.accentColor, lineWidth: showBorder ? 1 : 0)
)

至此,API 回归简洁:

AlertView(...) { ... }
    .showBorder(true)

进阶:用 @resultBuilder 做「有约束的自由」

当设计规范新增“免责声明 + 倒计时”组合时,与其疯狂加 init,不如定义一个 InfoSectionBuilder:

@resultBuilder
public struct InfoSectionBuilder {
    public static func buildBlock(_ disclaimer: Text) -> some View {
        disclaimer.disclaimerStyle()
    }
    public static func buildBlock(_ timer: TimerView) -> some View {
        timer
    }
    public static func buildBlock(
        _ disclaimer: Text,
        _ timer: TimerView
    ) -> some View {
        VStack(alignment: .leading, spacing: 12) {
            disclaimer.disclaimerStyle()
            timer
        }
    }
}

把 AlertView 再升级一次:

public struct AlertView<Info: View, Footer: View>: View {
    private let title, message: String
    private let infoSection: Info
    private let footer: Footer
    
    public init(
        title: String,
        message: String,
        @InfoSectionBuilder infoSection: () -> Info,
        @ViewBuilder footer: () -> Footer
    ) {
        self.title = title
        self.message = message
        self.infoSection = infoSection()
        self.footer = footer()
    }
    
    public var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Text(title).font(.headline)
            Text(message).font(.subheadline)
            infoSection.padding(.top, 16)
            footer.padding(.top, 25)
        }
        .padding()
    }
}

用法:

AlertView(
    title: "删除账户",
    message: "此操作不可撤销",
    infoSection: {
        Text("余额将在 24 小时内退回")
        TimerView(targetDate: .now + 100)
    },
    footer: {
        Button("确认删除", role: .destructive) {}
    }
)

无障碍:组件方 + 使用方 共同责任

组件内部负责结构级:

.accessibilityElement(children: .combine)
.accessibilityLabel("\(type.rawValue) alert: \(title). \(message)")
.accessibilityAddTraits(.isModal)

使用方负责内容级:

Button("延长会话") {}
    .accessibilityHint("延长 30 分钟")
    .accessibilityAction(named: "延长会话") { // 实际逻辑 }

写在最后的 checklist

维度 ✅ 自检问题
初始化 是否只有“最少必要参数”?
可组合 是否使用 @ViewBuilder / @resultBuilder
样式扩展 是否通过 EnvironmentKey + 自定义修饰符?
无障碍 结构 + 内容 是否都提供了 label / hint / action?
向后兼容 新增需求是否只“加 Builder 方法”而不是“改 init”?

源码仓库

所有示例已整理到 GitHub(非官方镜像,可直接跑 playground): github.com/muhammadosa…

当你用 .disabled(true) 把一整块区域关掉,子组件自动变灰、按钮自动失效 —— 这种「像原生」的体验,正是可扩展设计系统给人的最大安全感。

❌
❌