hiccLoghicc log by wccHipo Log

Swift 5.2到5.4新特性整理

深圳东西冲

Swift 5.4#

👉Swift 5.4 需要Xcode 12.5以上

改善隐式成员语法#

SE-0287提案改进了Swift使用隐式成员表达式的能力。Swift 5.4之后不但可以使用单个 使用,而且可以链起来使用。

例如我们使用隐式成员:

struct ContentViewView1: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(.red)
    }
}

我们如果要使用颜色的函数,我们只能用回Color.red.opacity(0.5)

struct ContentViewView2: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(Color.red.opacity(0.5))
    }
}

这点很违反直觉,Swift 5.4之后就不会报错了“Cannot infer contextual base in reference to member 'red'”。可以更简易使用了:

struct ContentViewView3: View {
    var body: some View {
        Text("You're not my supervisor!")
            .foregroundColor(.red.opacity(0.5))
    }
}

函数支持多个可变参数(variadic parameter)#

SE-0284能够让函数,下标和初始化器能够支持多个可变参数(需要带上参数label)。在此之前只支持一个。

func sumarizeGoals(times: Int..., players: String...) {
    let joinedNames = ListFormatter.localizedString(byJoining: players)
    let joinedTimes = ListFormatter.localizedString(byJoining: times.map(String.init))
    
    print("\(times.count) goals where scord by \(joinedNames) at the follow minutes:\(joinedTimes)")
}

调用很方便

sumarizeGoals(times: 18, 33, 55, 90, players: "Dani", "jamie", "Roy")
// 4 goals where scord by Dani, jamie, and Roy at the follow minutes:18, 33, 55, and 90

Result Builders#

Swift 5.1中非正式的引入了Function Builders。Swift 5.4中SE-0289提案把它升级为了Result Builders。

简单来说,Result Builders最重要功能是可以将我们所需的一系列值一步一步变成一个新值。这个能力也是SwiftUI view创建系统的核心驱动,例如在VStack有一批子view,Swift会在背后将这些view组合成一个内部的Tupleview,这样才会被VStack真正使用。——Result Builder把一系列view变成了一个view。

举例来说,首先我们有个函数返回一个字符串:

func makeSenence1() -> String {
    "Why settle for a Duke when you can have a Prince?"
}

print(makeSenence1())
// Why settle for a Duke when you can have a Prince?

这点没问题,但是我们如果需要将多个字符串join在一起呢?我们能像SwifUI那么写么?

很明显会编译错误,Swift 5.4之后,我们可以创建一个result builder来告诉Swift如何去转换。

@resultBuilder
struct SimplestringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
}

几句代码,可能不好理解:

  • @resultBuilder属性告知SwiftUI,所装饰的类型应该被当作一个result builder。之前类似的想法是@_functionBuilder。但是有下划线,以为着设计来不为常规使用。
  • 每个result builder,都必须至少提供一个静态的buildBlock()。一般这个函数接受一批数据然后做一些转换。上面例子中,接受0到多个字符串,通过回车符来合并成一个。
  • 最后,我们创建的SimpleStringBuilder结构体变成了一个result builder。意味着,我们可以在任何字符串join的地方来使用@simpleStringBuilder

可以直接使用SimpleStringBuilder.buildBlock():

let joined = SimplestringBuilder.buildBlock(
    "Why settle for a Duke",
    "when you can have",
    "a Prince?"
)

print(joined)
// Why settle for a Duke
// when you can have
// a Prince?

当然,我们使用了@resultBuilder来装饰了我们的SimpleStringBuilder。我们可以如下使用:

@SimplestringBuilder
func makesentence3() -> String {
    "Why settle for a Duke"
    "when you can have"
    "a Prince?"
}

print(makesentence3())
// Why settle for a Duke
// when you can have
// a Prince?
👉注意: 我们不再需要在每个字符串结尾使用逗号

@resultBuilder自动将makeSentence()中的表达式通过SimpleStringBuilder来转换成一个字符串。

result builder还可以支持if/else, for循环等表达方式。

@resultBuilder
struct ConditionalStringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
    
    static func buildEither(first component: String) -> String {
        return component
    }
    static func buildEither(second component: String) -> String {
        return component
    }
}

可以直接在函数体内使用 if else

@ConditionalStringBuilder
func makesentence4() -> String {
    "Why settle for a Duke"
    "when you can have"
    if Bool.random() {
        "a Prince?"
    } else {
        "a King?"
    }
}

// Why settle for a Duke
// when you can have
// a King?

同样,可以使用buildArray()来支持for循环:

@resultBuilder
struct ComplexStringBuilder {
    static func buildBlock(_ parts: String...) -> String {
        parts.joined(separator: "\n")
    }
    
    static func buildEither(first component: String) -> String {
        return component
    }
    static func buildEither(second component: String) -> String {
        return component
    }
    
    static func buildArray(_ components: [String]) -> String {
        components.joined(separator: "\n")
    }
}

使用起来很酷

@ComplexStringBuilder
func countDown() -> String {
    for i in (0...10).reversed() {
        "\(i)..."
    }
    
    "Lift off!"
}

print(countDown())
// 10...
// 9...
// 8...
// 7...
// 6...
// 5...
// 4...
// 3...
// 2...
// 1...
// 0...
// Lift off!
Swift result builder 支持不少的炫酷语法

还值得一提的是,Swift 5.4中result builder也支持作用在属性上, 它会自动让结构体struct的初始化函数应用result builder。

struct CustomVStack<Content: View>: View {
    @ViewBuilder let content:Content
    
    var body: some View {
        VStack {
            // custom functionality here
            content
        }
    }
}

嵌套函数支持重载#

SE-10069提案,可以让Swift支持嵌套函数中重载。

struct Butter { }
struct Flour { }
struct Sugar { }

func makeCookies() {
    func add(item: Butter) {
        print("Adding butter…")
    }

    func add(item: Flour) {
        print("Adding flour…")
    }

    func add(item: Sugar) {
        print("Adding sugar…")
    }

    add(item: Butter())
    add(item: Flour())
    add(item: Sugar())
}

在Swift 5.4之前,add()方法只有不再makeCookies()中才支持重载。

Property wrapper支持函数内变量#

Property wrapper 从Swift 5.1引入,用来装饰属性,复用代码,在Swift 5.4中也支持函数内变量装饰Property wrapper了。

@propertyWrapper struct NonNegative<T: Numeric & Comparable> {
    var value: T

    var wrappedValue: T {
        get { value }

        set {
            if newValue < 0 {
                value = 0
            } else {
                value = newValue
            }
        }
    }

    init(wrappedValue: T) {
        if wrappedValue < 0 {
            self.value = 0
        } else {
            self.value = wrappedValue
        }
    }
}

在Swift 5.4中函数内使用,就可以避免score不会为负数。

func playGame() {
    @NonNegative var score = 0

    // player was correct
    score += 4

    // player was correct again
    score += 8

    // player got one wrong
    score -= 15

    // player got another one wrong
    score -= 16

    print(score)
}

Swift Package支持声明可执行目标#

SE-0294提案可以支持声明Swift Package声明可执行的target。

这点对想使用@main属性的情况很有用,因为目前Swift Package包管理会自动寻找main.swift文件,有了这个能力的支持,我们在Package.swift中指定//swift-tools-version:5.4就可以移除main.swift文件,使用@main的方式了。

Swift 5.3#

多模式错误捕捉#

SE-0276提案(Multi-pattern catch clauses),能够让我们在单个catch块中,捕获多个错误,以此来减少重复代码。

例如,我们有下面的错误码。

enum Temperatureerror: Error {
    case tooCold, tooHot
}

如下的业务代码

func getReactortemperature() -> Int {
    100
}

func checkreactorOperational() throws -> String {
    let temp = getReactortemperature()
    
    if temp < 10 {
        throw Temperatureerror.tooCold
    } else if temp > 90 {
        throw Temperatureerror.tooHot
    } else {
        return "ok"
    }
}

Swift 5.2之后,你可以使用逗号,同时处理tooHot和tooCold。

do {
    let result = try checkreactorOperational()
    print("Result: \(result)")
} catch Temperatureerror.tooHot, Temperatureerror.tooCold {
    print("Shut down the reactor!")
} catch {
    print("An unknown error occurred.")
}

多重尾随闭包#

SE-0279提案引入了多重尾随闭包,能够让我们更简单的调用有多个闭包参数的函数。

之前我们在SwiftUI中,经常会有如下代码

struct OldContentView: View {
    @State private var showOptions = false
    
    var body: some View {
        Button(action: {
            self.showOptions.toggle()
        }) {
            Image(systemName: "gear")
        }

    }
}

现在可以更简化:

struct OldContentView: View {
    @State private var showOptions = false
    
    var body: some View {
        Button {
            self.showOptions.toggle()
        } label: {
            Image(systemName: "gear")
        }

    }
}

可比较的枚举#

SE-0266提案可以让我们给非关联枚举和遵循comparable协议的关联值使用comparable协议,也就是可以使用<,>或类似的操作。

enum Size: Comparable {
    case small
    case medium
    case large
    case extraLarge
}

if Size.small < Size.large {
    print("That shirt is too small")
}
// That shirt is too small

对于关联值也是支持的:

enum WorldCupResult: Comparable {
    case neverWon
    case winner(stars: Int)
}

let americanMen = WorldCupResult.neverWon
let americanWomen = WorldCupResult.winner(stars: 4)
let japaneseMen = WorldCupResult.neverWon
let japaneseWomen = WorldCupResult.winner(stars: 1)

let teams = [americanMen, americanWomen, japaneseMen, japaneseWomen]
let sortedByWins = teams.sorted()
print(sortedByWins)

注意这里面的排序默认是根据case的顺序,以及关联值的值大小,上述,winner的会高于neverWon,而winner(stars: 4) 大于winner(stars: 1)

当然你也可以自定义比较函数:

enum Size2: Comparable {
    
    case medium
    case large
    case small
    case extraLarge
    
    static func < (lhs: Self, rhs: Self) -> Bool {
        print(lhs)
        print(lhs.hashValue)
        print(rhs.hashValue)
        return lhs.hashValue > rhs.hashValue
    }
}

if Size2.small < Size2.large {
    print("That shirt is too small")
}
// That shirt is too small

self在很多地方不再必须#

SE-0269提案允许我们在很多不需要的地方停止使用self。在此之前,我们需要在任何引用self的地方写上self.。这样我们就把我们的捕获语义显示化了。然而经常出现的情况是,我们的闭包不会导致引用循环,也就意味着self是多余的。

例如在之前

struct OldContentView: View {
    var body: some View {
        List(1..<5) { number in
            self.cell(for: number)
        }
    }
    
    func cell(for number: Int) -> some View {
        Text("Cell \(number)")
    }
}

因为是在struct中调用self.cell(for:),不会导致循环引用。在Swift 5.3中,我们可以这样写:

struct OldContentView: View {
    var body: some View {
        List(1..<5) { number in
            cell(for: number)
        }
    }
    
    func cell(for number: Int) -> some View {
        Text("Cell \(number)")
    }
}

基于类型的程序入口#

SE-0281提案引入了@main的属性来声明程序的入口。

在此之前,我们需要在main.swfit文件中,如此启动程序

struct OldApp {
    func run() {
        print("Running!")
    }
}

let app = OldApp()
app.run()

现在我们可以简单如此书写:

@main
struct NewApp {
    static func main() {
        print("Running!")
    }
}

需要注意的是:

  • 如果你已经使用了main.swift的方式,你就不能再使用@main
  • 只能有一个@main
  • @main只能用在基类中,它不能被任何子类继承。

上下文泛型声明中支持where限制#

SE-0280提案允许在泛型类型和extension的函数中使用where限制。

例如我们有Stack的结构体。

struct Stack<Element> {
    private var array = [Element]()
    
    mutating func push(_ obj: Element) {
        array.append(obj)
    }
    
    mutating func pop() -> Element? {
        array.popLast()
    }
}

Swift 5.3之后,我们可以给stack添加一个sorted()方法,仅仅element遵循Comparable协议。

可以直接在struct中

struct Stack<Element> {
    func sorted() -> [Element] where Element: Comparable {
        array.sorted()
    }
}

也可以在extendion中。

extension Stack {
    func sorted() -> [Element] where Element: Comparable {
        array.sorted()
    }
}

Enum cases as protocol witnesses#

SE-0280提案,允许枚举case 更好的匹配协议protocol。

譬如你会写如下的协议和实现来表达默认值。

protocol Defaultable {
    static var defaultValue: Self { get }
}

// make integers have a default value of 0
extension Int: Defaultable {
    static var defaultValue: Int { 0 }
}

// make arrays have a default of an empty array
extension Array: Defaultable {
    static var defaultValue: Array { [] }
}

// make dictionaries have a default of an empty dictionary
extension Dictionary: Defaultable {
    static var defaultValue: Dictionary { [:] }
}

现在同样的事情,也可以用在枚举上。

enum Padding: Defaultable {
    case pixels(Int)
    case cm(Int)
    case defaultValue
}

重新定义didSet#

SE-0268提案为更好的效率,调整了didSet属性监听的工作方式。简单来说:

  • 如果didSet中没有引用oldValue,那么就会跳过获取oldValue,叫做“simple” didSet
  • 如果已经是“simple” didSet也没有willSet。则直接在赋值的时候直接改值。

当然如果要依赖老方式,可以这么写

didSet {
    _ = oldValue
}

新的Float16类型#

SE-0277提案引入了,新的数据类型Float16。这种类型被广泛使用在图形编程和机器学习中。

let first: Float16 = 5
let second: Float32 = 11
let third: Float64 = 7
let fourth: Float80 = 13

Swift 包管理改进#

这里不再细列,可以参考SE-0271SE-0278 SE-0272SE-0273等。

Swift 5.2#

Key Path表达式用作函数#

SE-0249提案的实现,Key Path表达式作为函数(Key Path Expressions as Functions)。可以将函数(Root) -> Value 简写为\Root.value

例如,我们定义User类型

struct User {
    let name: String
    let age: Int
    let bestFriend: String?
    
    var cnaVote: Bool {
        age >= 18
    }
}

创建几个实例,并且放入数组

let eric = User(name: "Eric Effiong", age: 18, bestFriend: "Otis Milburn")
let maeve = User(name: "Maeve Wiley", age: 19, bestFriend: nil)
let otis = User(name: "Otis Milburn", age: 17, bestFriend: "Eric Effiong")
let users = [eric, maeve, otis]

Swift 5.2之后,你可以像下面一样使用key path

let userNames = users.map(\.name)
print(userNames)

而在之前稍微啰嗦一点

let oldUserNames = users.map { $0.name }

当然,你也可以如下使用

let kp: (User) -> String? = \User.bestFriend
let bestFriends = users.compactMap(kp)
print(bestFriends)

注意kp需要显示的写明类型,不然默认是KeyPath类型

可调用的值#

提案SE-0253为Swift带来可调用的值(Callable values of user-defined nominal types)。具体来说,如果类型实现了名为的callAsFunction()的方法,其类型的实例就能直接调用。

struct Dice {
    var lowerBound: Int
    var upperBound: Int
    
    func callAsFunction() -> Int {
        (lowerBound...upperBound).randomElement()!
    }
}

let d6 = Dice(lowerBound: 1, upperBound: 6)
let roll1 = d6()
print(roll1)

你可以正常定义callAsFunction()方法,支持throwsrethrows,可以使用mutating

struct StepCounter {
    var steps = 0
    
    mutating func callAsFunction(count: Int) -> Bool {
        steps += count
        print(steps)
        return steps > 10_100
    }
}

var steps = StepCounter()

let targetReached = steps(count: 10)

这么甜的语法糖是为了什么?

  • 更清晰的语法
  • 能更有好的开发机器学习(提议的原始动机之一)。

下标可声明默认参数#

Swift 5.2 之后,当你使用自定义下标时候,你可以给参数声明默认值了。

struct PoliceForce {
    var officers: [String]

    subscript(index: Int, default default: String = "Unknown") -> String {
        if index >= 0 && index < officers.count {
            return officers[index]
        } else {
            return `default`
        }
    }
}
//Amy
//Unknown

现在可以像如下自定义值

print(force[-1, default: "The Vulture"])

当然自定义下标,你也可以不给参数家label

struct Multiplier {
  subscript(x: Int, y: Int = 1) -> Int {
    x * y
  }
}

let multiplier = Multiplier()

multiplier[2, 3]
multiplier[4]

Lazy filter 顺序反转#

let people = ["Arya", "Cersei", "Samwell", "Stannis"]
    .lazy
    .filter { $0.hasPrefix("S") }
    .filter { print($0); return true }
_ = people.count
//Samwell
//Stannis

Swift 5.2 之前使用lazy,会返回所有,这很违反直觉,因此Swift 5.2 修复了这个问题。

支持使用外作用域值作为默认值#

func outer(x: Int) -> (Int, Int) {
    func inner(y: Int = x) -> Int {
        return y
    }
    return (inner(), inner(y: 0))
}

Swift 5.2 之后上述可以编译通过。

更好的错误诊断#

Swift 5.2之后,改善了,Swift和SwiftUI的错误提示。

Swift 3 到Swift 5.1#

参考#