-
Swift 的新功能
和我们一起了解 Swift 的更新。我们将向你展示 API 如何通过参数包和宏等功能变得更具扩展性和表现力。我们还将带你了解互操作性的改进,并分享我们如何将 Swift 的性能和安全优势扩展到从 Foundation 到服务器上的大型分布式程序等各个地方。
Chapitres
- 0:39 - Swift project update
- 2:44 - Using if/else and switch statements as expressions
- 3:52 - Result builders
- 4:53 - type parameter packs
- 9:34 - Swift macros
- 19:47 - Swift foundation
- 23:25 - Ownership
- 27:59 - C++ interoperability
- 32:41 - What's new in Swift Concurrency
- 38:20 - FoundationDB: A case study
Ressources
Vidéos connexes
WWDC23
- Beyond the basics of structured concurrency
- Expand on Swift macros
- Generalize APIs with parameter packs
- Meet SwiftData
- Mix Swift and C++
- What’s new in SwiftUI
- Write Swift macros
WWDC21
-
Rechercher dans cette vidéo…
-
-
3:06 - Hard-to-read compound ternary expression
let bullet = isRoot && (count == 0 || !willExpand) ? "" : count == 0 ? "- " : maxDepth <= 0 ? "▹ " : "▿ " -
3:19 - Familiar and readable chain of if statements
let bullet = if isRoot && (count == 0 || !willExpand) { "" } else if count == 0 { "- " } else if maxDepth <= 0 { "▹ " } else { "▿ " } -
3:30 - Initializing a global variable or stored property
let attributedName = AttributedString(markdown: displayName) -
3:46 - In 5.9, if statements can be an expression
let attributedName = if let displayName, !displayName.isEmpty { AttributedString(markdown: displayName) } else { "Untitled" } -
4:31 - In Swift 5.7, errors may appear in a different place
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { // The issue actually occurs here Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } } -
4:47 - In Swift 5.9, you now receive a more accurate compiler diagnostic
struct ContentView: View { enum Destination { case one, two } var body: some View { List { NavigationLink(value: .one) { //In 5.9, Errors provide a more accurate diagnostic Text("one") } NavigationLink(value: .two) { Text("two") } }.navigationDestination(for: Destination.self) { $0.view // Error occurs here in 5.7 } } } -
5:47 - An API that takes a request type and evaluates it to produce a strongly typed value
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<Result>(_ request: Request<Result>) -> Result } func evaluate(_ request: Request<Bool>) -> Bool { return RequestEvaluator().evaluate(request) } -
6:03 - APIs that abstract over concrete types and varying number of arguments
let value = RequestEvaluator().evaluate(request) let (x, y) = RequestEvaluator().evaluate(r1, r2) let (x, y, z) = RequestEvaluator().evaluate(r1, r2, r3) -
6:35 - Writing multiple overloads for the evaluate function
func evaluate<Result>(_:) -> (Result) func evaluate<R1, R2>(_:_:) -> (R1, R2) func evaluate<R1, R2, R3>(_:_:_:) -> (R1, R2, R3) func evaluate<R1, R2, R3, R4>(_:_:_:_:)-> (R1, R2, R3, R4) func evaluate<R1, R2, R3, R4, R5>(_:_:_:_:_:) -> (R1, R2, R3, R4, R5) func evaluate<R1, R2, R3, R4, R5, R6>(_:_:_:_:_:_:) -> (R1, R2, R3, R4, R5, R6) -
6:47 - Overloads create an arbitrary upper bound for the number of arguments
//This will cause a compiler error "Extra argument in call" let results = evaluator.evaluate(r1, r2, r3, r4, r5, r6, r7) -
7:12 - Individual type parameter
<each Result> -
7:36 - Collapsing the same set of overloads into one single evaluate function
func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) -
8:21 - Calling updated evaluate function looks identical to calling an overload
struct Request<Result> { ... } struct RequestEvaluator { func evaluate<each Result>(_: repeat Request<each Result>) -> (repeat each Result) } let results = RequestEvaluator.evaluate(r1, r2, r3) -
10:01 - It isn't clear why an assert function fails
assert(max(a, b) == c) -
10:20 - XCTest provides an assert-equal operation
XCAssertEqual(max(a, b), c) //XCTAssertEqual failed: ("10") is not equal to ("17") -
11:02 - Assert as a macro
#assert(max(a, b) == c) -
11:42 - Macros are distributed as packages
import PowerAssert #assert(max(a, b) == c) -
12:07 - Macro declaration for assert
public macro assert(_ condition: Bool) -
12:26 - Uses are type checked against the parameters
import PowerAssert #assert(max(a, b)) //Type 'Int' cannot be a used as a boolean; test for '!= 0' instead -
12:52 - A macro definition
public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" ) -
13:11 - Swift compiler passes the source code for the use of the macro
#assert(a == b) -
13:14 - Compiler plugin produces new source code, which is integrated back into the Swift program
PowerAssert.Assertion( "#assert(a == b)" ) { $0.capture(a, column: 8) == $0.capture(b, column: 13) } -
13:33 - Macro declarations include roles
// Freestanding macro roles @freestanding(expression) public macro assert(_ condition: Bool) = #externalMacro( module: “PowerAssertPlugin”, type: “PowerAssertMacro" ) -
13:53 - New Foundation Predicate APIs uses a `@freestanding(expression)` macro role
let pred = #Predicate<Person> { $0.favoriteColor == .blue } let blueLovers = people.filter(pred) -
14:14 - Predicate expression macro
// Predicate expression macro @freestanding(expression) public macro Predicate<each Input>( _ body: (repeat each Input) -> Bool ) -> Predicate<repeat each Input> -
14:48 - Example of a commonly used enum
enum Path { case relative(String) case absolute(String) } -
15:01 - Checking a specific case, like when filtering all absolute paths
let absPaths = paths.filter { $0.isAbsolute } -
15:09 - Write an `isAbsolute` check as a computer property...
extension Path { var isAbsolute: Bool { if case .absolute = self { true } else { false } } } -
15:12 - ...And another for `isRelative`
extension Path { var isRelative: Bool { if case .relative = self { true } else { false } } } -
15:17 - Augmenting the enum with an attached macro
@CaseDetection enum Path { case relative(String) case absolute(String) } let absPaths = paths.filter { $0.isAbsolute } -
15:36 - Macro-expanded code is normal Swift code
enum Path { case relative(String) case absolute(String) //Expanded @CaseDetection macro integrated into the program. var isAbsolute: Bool { if case .absolute = self { true } else { false } } var isRelative: Bool { if case .relative = self { true } else { false } } } -
16:57 - Observation in SwiftUI prior to 5.9
// Observation in SwiftUI final class Person: ObservableObject { @Published var name: String @Published var age: Int @Published var isFavorite: Bool } struct ContentView: View { @ObservedObject var person: Person var body: some View { Text("Hello, \(person.name)") } } -
17:25 - Observation now
// Observation in SwiftUI @Observable final class Person { var name: String var age: Int var isFavorite: Bool } struct ContentView: View { var person: Person var body: some View { Text("Hello, \(person.name)") } } -
17:42 - Observable macro works with 3 macro roles
@attached(member, names: ...) @attached(memberAttribute) @attached(conformance) public macro Observable() = #externalMacro(...). -
17:52 - Unexpanded macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool } -
18:05 - Expanded member attribute role
@Observable final class Person { var name: String var age: Int var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } -
18:12 - Member attribute role adds `@ObservationTracked` to stored properties
@Observable final class Person { @ObservationTracked var name: String @ObservationTracked var age: Int @ObservationTracked var isFavorite: Bool internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } -
18:16 - The @ObservationTracked macro adds getters and setters to stored properties
@Observable final class Person { @ObservationTracked var name: String { get { … } set { … } } @ObservationTracked var age: Int { get { … } set { … } } @ObservationTracked var isFavorite: Bool { get { … } set { … } } internal let _$observationRegistrar = ObservationRegistrar<Person>() internal func access<Member>( keyPath: KeyPath<Person, Member> ) { _$observationRegistrar.access(self, keyPath: keyPath) } internal func withMutation<Member, T>( keyPath: KeyPath<Person, Member>, _ mutation: () throws -> T ) rethrows -> T { try _$observationRegistrar.withMutation(of: self, keyPath: keyPath, mutation) } } -
18:33 - All that Swift code is folded away in the @Observable macro
@Observable final class Person { var name: String var age: Int var isFavorite: Bool } -
23:59 - A wrapper for a file descriptor
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } } -
24:30 - The same FileDescriptor wrapper as a class
class FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { self.close(fd) } } -
25:05 - Going back to the struct
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } } -
26:06 - Using Copyable in the FileDescriptor struct
struct FileDescriptor: ~Copyable { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } } -
26:35 - `close()` can also be marked as consuming
struct FileDescriptor { private var fd: CInt init(descriptor: CInt) { self.fd = descriptor } func write(buffer: [UInt8]) throws { let written = buffer.withUnsafeBufferPointer { Darwin.write(fd, $0.baseAddress, $0.count) } // ... } consuming func close() { Darwin.close(fd) } deinit { Darwin.close(fd) } } -
26:53 - When `close()` is called, it must be the final use
let file = FileDescriptor(fd: descriptor) file.write(buffer: data) file.close() -
27:20 - Compiler errors instead of runtime failures
let file = FileDescriptor(fd: descriptor) file.close() // Compiler will indicate where the consuming use is file.write(buffer: data) // Compiler error: 'file' used after consuming -
28:52 - Using C++ from Swift
// Person.h struct Person { Person(const Person &); Person(Person &&); Person &operator=(const Person &); Person &operator=(Person &&); ~Person(); std::string name; unsigned getAge() const; }; std::vector<Person> everyone(); // Client.swift func greetAdults() { for person in everyone().filter { $0.getAge() >= 18 } { print("Hello, \(person.name)!") } } -
29:51 - Using Swift from C++
// Geometry.swift struct LabeledPoint { var x = 0.0, y = 0.0 var label: String = “origin” mutating func moveBy(x deltaX: Double, y deltaY: Double) { … } var magnitude: Double { … } } // C++ client #include <Geometry-Swift.h> void test() { Point origin = Point() Point unit = Point::init(1.0, 1.0, “unit”) unit.moveBy(2, -2) std::cout << unit.label << “ moved to “ << unit.magnitude() << std::endl; } -
35:30 - An actor that manages a database connection
// Custom actor executors actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> init(filename: String) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries() -
35:58 - MyConnection with a serial dispatch queue and a custom executor
actor MyConnection { private var database: UnsafeMutablePointer<sqlite3> private let queue: DispatchSerialQueue nonisolated var unownedExecutor: UnownedSerialExecutor { queue.asUnownedSerialExecutor() } init(filename: String, queue: DispatchSerialQueue) throws { … } func pruneOldEntries() { … } func fetchEntry<Entry>(named: String, type: Entry.Type) -> Entry? { … } } await connection.pruneOldEntries() -
36:44 - Dispatch queues conform to SerialExecutor protocol
// Executor protocols protocol Executor: AnyObject, Sendable { func enqueue(_ job: consuming ExecutorJob) } protocol SerialExecutor: Executor { func asUnownedSerialExecutor() -> UnownedSerialExecutor func isSameExclusiveExecutionContext(other executor: Self) -> Bool } extension DispatchSerialQueue: SerialExecutor { … } -
39:22 - C++ implementation of FoundationDB's "master data" actor
// C++ implementation of FoundationDB’s “master data” actor ACTOR Future<Void> getVersion(Reference<MasterData> self, GetCommitVersionRequest req) { state std::map<UID, CommitProxyVersionReplies>::iterator proxyItr = self->lastCommitProxyVersionReplies.find(req.requestingProxy); ++self->getCommitVersionRequests; if (proxyItr == self->lastCommitProxyVersionReplies.end()) { req.reply.send(Never()); return Void(); } wait(proxyItr->second.latestRequestNum.whenAtLeast(req.requestNum - 1)); auto itr = proxyItr->second.replies.find(req.requestNum); if (itr != proxyItr->second.replies.end()) { req.reply.send(itr->second); return Void(); } // ... } -
40:18 - Swift implementation of FoundationDB's "master data" actor
// Swift implementation of FoundationDB’s “master data” actor func getVersion( myself: MasterData, req: GetCommitVersionRequest ) async -> GetCommitVersionReply? { myself.getCommitVersionRequests += 1 guard let lastVersionReplies = lastCommitProxyVersionReplies[req.requestingProxy] else { return nil } // ... var latestRequestNum = try await lastVersionReplies.latestRequestNum .atLeast(VersionMetricHandle.ValueType(req.requestNum - UInt64(1))) if let lastReply = lastVersionReplies.replies[req.requestNum] { return lastReply } }
-