hiccLoghicc log by wccHipo日志

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#

参考#