大多数浏览器和
Developer App 均支持流媒体播放。
-
在 Swift 中使用不可拷贝的类型
开始探索 Swift 中不可拷贝的类型。了解拷贝在 Swift 中的含义、何时需要使用不可拷贝的类型,以及值所有权如何让你清晰声明自己的意图。
章节
- 0:00 - Introduction
- 0:30 - Agenda
- 0:50 - Copying
- 7:05 - Noncopyable Types
- 11:50 - Generics
- 19:12 - Extensions
- 21:24 - Wrap up
资源
- Copyable
- Forum: Programming Languages
- Swift Evolution: Borrowing and consuming pattern matching for noncopyable types
- Swift Evolution: Noncopyable Generics
- Swift Evolution: Noncopyable Standard Library Primitives
相关视频
WWDC19
-
下载
Hi I'm Kavon from the Swift team! Welcome to "Consume noncopyable types in Swift"! You and I are unique but values in Swift are not. That's because values can be copied. Having a guarantee that values are unique, can be a powerful concept when programming. So, I'm happy to tell you that we've recently introduced noncopyable types to Swift! We have a lot of exciting things to cover today. First I'm going to review how copying works. Then we'll cover ownership and noncopyable types and finally I'll go over more advanced topics like using noncopyable types generically and writing extensions of those types. So, let's get copying! I'm working on a new game, so I've defined a Player type, where an emoji represents their icon.
Let's create two players. So far, they're the same.
Intuitively, I know changing one player's icon won't affect the other. Let's examine what's behind that intuition, step-by-step. Starting at the first line, player1 is a frog.
The contents of that variable is the actual data that makes up a Player. That's because it's a struct; which is a value type. Moving on to player2, I'm making it the same as player1. But, what does that really mean? It means I'm making a copy of player1. When you copy a variable, you're copying its contents.
So, when I change the icon for player2, I'm changing a Player that is independent of player1.
I didn't even have to think about copying or destroying values. Swift takes care of it.
OK, but what if Player was a reference type. All that means is that I've changed it from a struct to a class.
With the same code as before, what happens? Let's break down this first statement.
When constructing a PlayerClass, an object is allocated separately to store its data.
The contents of player1 becomes an automatically-managed reference to that object.
That's what it means to be a reference type.
In the next statement, player2 is the same as player1, so that means the reference is copied. Not the object itself! This is sometimes called a shallow copy, which is very quick.
Since both players refer to the same object, changing the icon will change it for both.
So, this assertion does not hold.
Notice how copying worked the same in both cases. The only difference is whether you're copying a value, or a reference. You can make a reference type behave like a value type, by defining an initializer to do a deep copy.
This initializer is an example. It recreates an object, and everything the object points to, recursively.
It does that by calling Icon's initializer, to recreate it using the other Player's icon. That helps ensure the new player doesn't share references with the other one.
Let's go back to our program state from earlier, after we've made player2 the same as player1, to see how deep copying changes the behavior.
Right now, both players are references to the same object.
Before I write to the player's field, I'm now making a deep copy of it, by calling the initializer on itself.
That allocates a separate, but identical, object which ensures no other variables will be affected by the mutation.
In fact, that's the essence of copy-on-write.
It gives you independence under mutation, so you get the same the behavior as a value type.
When designing a new type, you already have control over whether someone can deeply copy its values. What you have not had control over, is whether Swift is able to make automatic copies of it. Copyable is a new protocol that describes the ability for a type to be automatically copied. Like Sendable, it does not have member requirements.
Everything is inferred to be Copyable in Swift by default. And I mean everything.
Every type, tries to automatically conform to Copyable.
Every generic parameter, automatically requires the type you put in it, to be Copyable.
Every protocol and associated type, automatically requires the concrete type, to conform to Copyable.
and, every boxed protocol type, is automatically composed, with Copyable.
You don't have to write Copyable yourself, like I've done. It's already there, even if you can't see it.
Swift assumes you want the ability to copy, because it's much easier to work with Copyable types.
But, there are situations where copying makes your code error-prone. Suppose I'm working on a type to model a bank transfer for my backend. In real life, the transfer is either pending, cancelled, or complete.
We'll give the type a run method that completes the transfer. I need to schedule the transfers, so here's a function to do that. Obviously, my users would not be happy if I accidentally ran a transfer twice, so let's double-check it. If the delay is less than a second, I'd rather run it immediately, but I forgot to return. So I would fall through and run it again. A simple mistake like this can be costly, so how can I defend against it? Well, I can add a variable to track the state of the transfer. So that an assertion will catch attempts to run it again. But, an assertion won't uncover a bug unless if I wrote a test that hits it. So, I might still have a bug like this, which could take down my backend service. In fact, there is another bug in this schedule function! Think about what happens if the sleeping task is cancelled. If callers are not careful to check for the thrown error, they may forget to cancel the transfer.
I can introduce a deinit in my BankTransfer that remembers to cancel. But this is pretty useless in practice.
Take a closer look at the startPayment function. It retains a copy of the transfer, in order to track that it's been scheduled. That's a problem, because a transfer's deinit won't run unless if all copies of it are destroyed. That's the root of my problem: I have no control over how many copies of this transfer are floating around in my program.
So, while the ability to copy values is often the right default for your types, in some situations, it's better if the type were noncopyable.
Putting aside our BankTransfer issue for a moment, let's learn about noncopyable types.
Say I want to model a FloppyDisk. Written like this, the type has a default Copyable conformance.
But, if, in the spot where you declare conformances, I wrote a tilde before the word Copyable, then I'm suppressing that default conformance to Copyable. Now, FloppyDisk has no Copyable conformance at all! Think of tilde-Copyable as declaring the absence of Copyable for the type you've marked. So, what happens when you try to copy that floppy? Well, copying is not supported, so Swift will consume it instead.
I can make that explicit by writing consume, but it happens regardless.
Consuming a variable takes its value, leaving that variable uninitialized.
So, before the consume, only the system disk is initialized.
The consume moves the contents of the system disk out, and into the backup disk.
Reading the system disk afterwards is an error; it's got nothing in it.
Now, consider this function that creates new disks.
When it calls format, what happens to the variable result? It's hard to tell, since the function's signature doesn't declare what ownership it needs over the disk. With copyable parameters, you didn't have to think about this: format would effectively receive a copy of the disk. But, for noncopyable parameters, you must declare what ownership the function has over the value, since we can't copy.
The first kind of ownership is called consuming. It means the function will take the argument away from its caller. It will belong to you, so you can even mutate it.
But, using consuming here would be a problem, since format won't give the disk back; it returns nothing.
If you think about it, though, formatting a disk only requires temporary access to it.
When you have temporary access to something, you're borrowing it. Borrowing gives you read-access to the argument; like a let binding.
Under the hood, that's how nearly all parameters and methods work for Copyable types.
The difference is that you cannot consume or mutate an explicitly borrowed argument. You can only copy it. Since our format function will eventually need to mutate the disk, we can't use borrowing for it, either.
The last kind of ownership, you're already familiar with, it's inout! Or equivalently for methods, it's written mutating.
Inout provides temporary write-access to a variable in the caller. Because you have write access, you're allowed to consume the parameter. But, you have to reinitialize the inout parameter at some point before the function ends. Because the caller expects a value there when you return. OK, let's revisit our BankTransfer example, because now we can model it as a consumable resource. First, I've made BankTransfer a noncopyable struct, and marked its run method as consuming, which takes the value for self away from callers. With those two changes alone, I don't need assertions anymore. Swift guarantees that I cannot call the run method twice on the same transfer. In fact, its ownership is so precisely tracked, that I can add a deinit to this struct that triggers an action if it is destroyed, instead of being 'run'. Since the end of my 'run' method will also automatically destroy self, I'll write discard self there. That will destroy it, but without calling the deinit. Let's see what happens to those bugs in my schedule function. First, I have to add ownership for the transfer parameter. Since 'schedule' is meant to be the last use, consuming it makes sense. Now, when I try compiling this, I'll see that Swift has zero'ed in on my bug. Because the if-statement falls-through, it's possible to consume the transfer twice.
And adding the return prevents that. Now, what about that other bug? It's been defined away, too. Schedule is the last owner of the transfer, so its deinit will run if Sleep throws, and that'll cancel the transfer.
Noncopyable types are a great tool to improve the correctness of your programs. You might want to use them everywhere, including generic code. Well, in Swift 6, now you can, with noncopyable generics! This is not some sort of new generics system. It's built on Swift's existing generics model. So, let's refresh our understanding of generics.
To set the stage, consider the universe of types in Swift.
Every type has happily co-existed here, whether it's a String, or my own type called Command.
My protocol Runnable defines a subspace in this universe, containing the types that conform to it. Right now, nothing conforms, so it's empty. But, if I extend Command with a Runnable conformance, then its point moves into the Runnable space.
A core idea behind generics is that conformance constraints describe generic types. Let's think about that with this generic function called execute.
Notice the T here in the angle brackets. It declares a new generic type parameter, which represents some type in this universe, but we don't know which one. Remember when I said Copyable is everywhere? Well, there's a default constraint on this T. It requires the input type to conform to Copyable.
Command has a default conformance to Copyable; and Runnable inherits from Copyable too. In fact, until recently, the whole universe of types in Swift has really just been Copyable, in disguise.
This means all the types in this space can be passed into my execute function. Because the only constraint is that T conforms to Copyable.
So, even though Command also conforms to Runnable, It still exists in this broader space Copyable, in which any particular type might be Runnable, but it also might not. My generic parameter T does not exclude types that have an additional conformance. As written, the execute function promises to get by without needing any other conformances besides Copyable.
To implement my execute function though, I actually do want T to be Runnable, because I need to call a run method. So, I'll add a Runnable constraint on T, using a where clause.
In doing so, I've further constrained the space of types permitted for T. it's now the narrower space, Runnable & Copyable. That includes Command, but now excludes String, because a Runnable conformance is absent for String.
Well, since Swift 5.9, our universe has expanded. Because, there are types without a conformance to Copyable.
For example, our shiny new BankTransfer type does not conform, so its point is outside.
Since we can only suppress Copyable conformances by writing tilde Copyable, that's what I'll call this broader space.
Most types in Swift that you're familiar with do conform to Copyable.
So, how are they contained within tilde Copyable? As before, within this broader space, you cannot assume any particular type conforms to Copyable. It might be Copyable, but it also might not. That's how you should read tilde Copyable.
What about the Any type? Well, it's always been Copyable; and that's how it should be. When you think of any type in almost every programming language, it's Copyable.
Now, we're ready to talk about noncopyable generics.
Let's start with our Runnable protocol from earlier.
Our universe of types currently looks like this. All Runnable values are Copyable.
BankTransfer is not Copyable, so it cannot be Runnable, either. But, I want BankTransfer to conform, so I can use it generically. The ability to copy a Runnable type is not fundamental to the protocol, so, I'll remove the Copyable constraint from Runnable, by adding tilda Copyable.
This changes the hierarchy, so the Copyable space only overlaps Runnable, rather than containing it. Command is Runnable & Copyable, so it sits within that overlap.
Next, if I extend BankTransfer with a Runnable conformance, its point moves within Runnable, without being Copyable.
Let's revisit our generic function, execute.
There's still a default constraint on generic parameter T. So, only types like Command, that are both Runnable and Copyable, are permitted in execute.
Let's remove the Copyable constraint from T, using tilde Copyable.
Removing the constraint broadens the types permitted, to all types that are Runnable. The execute function is saying that T might not be Copyable. That's the key point: a regular constraint narrows the types permitted, by being more specific. Whereas a tilde constraint broadens by being less specific.
OK. Now, let's put all of that theory into practice. I've got Runnable types, and I want to wrap them inside a new struct called Job. I've defined Job with a generic parameter Action, that is Runnable, and might not be Copyable. But with what I've written so far, I'll get an error. The struct Job defaults to conforming to Copyable, so it can only contain Copyable data. There are two ways you can store noncopyable values inside another. It either must be inside of a class, since copying a class only copies a reference; or you have to suppress Copyable on the containing type itself. I'll go with the second one and make Job noncopyable.
I can still put the type Command in for the Action, because the Action does not prevent Copyable types from appearing. Job is promising that it does not need to copy an Action, so that noncopyable types work too. But, what if I know the type I put in for Action is Copyable. Then Job could be copied since it's just a container for an action.
As the API author, I can allow that by declaring Job is conditionally Copyable. This extension says that a Job is Copyable when its Action is Copyable.
How does that look in our universe? We don't know whether a Job is Copyable, until we put in a concrete type for the Action. So, let's put in Command.
We know a Command is Copyable, so a Command-Job is Copyable too.
But, if I make the Action a BankTransfer, the conditional conformance is not satisfied, so a BankTransfer-Job is not Copyable.
So, the whole idea behind noncopyable generics, is that you're removing default Copyable constraints.
You've seen how to define a type with a noncopyable generic parameter. Let's take a closer look at extensions of that type.
Say I want to define a getter method for the Action.
I'll add it using an ordinary extension of Job.
Calling it works just fine... but isn't it giving me a copy of the Action? Yeah. Returning the action does copy it. And that's not an error in the extension.
Because this plain extension defaults to being constrained to Jobs where the Action is Copyable.
So, this getter is correct, because it simply is not callable, on, say, a BankTransfer job.
That's how all extensions work: any generic parameters in scope of the extended type are constrained to Copyable. That includes Self in a protocol.
There's a really nice advantage to having extensions work this way. Let's say that Job is actually part of some JobKit module I didn't write.
I have my protocol here to describe Cancellable types. And let's pretend I have no idea what a noncopyable type is, but I want Job to conform anyway.
That's OK, because I can write this extension and it'll just work.
That's because the conformance defaults to being conditional on Action being Copyable, because in general, Action might NOT be. And when Action is Copyable, so is the Job, meaning it conforms to Cancellable.
So, you can publish this Job type, and programmers who only work with Copyable types can use it. Now, what if I did want this extension to apply to all jobs, Copyable or not.
Well, I just take the Copyable constraint off of the Action in this extension. Now, Job conforms to Cancellable, without assuming Action is Copyable.
Today, we've seen how copying works in Swift, and where it can create challenges. Noncopyable types are a useful tool to improve program correctness, with the trade-off of thinking about ownership. We've taken our first steps towards adopting noncopyable generics in the standard library, with Optional, UnsafePointer, and Result. You can learn more by reading the Swift Evolution proposals on: noncopyable generics, borrowing and consuming pattern matching, and noncopyable standard library primitives. You can also learn more in The Swift Programming Language book. More generally, if you want to learn about copy-on-write, and best-practices for designing generic types, check out Modern Swift API Design, from WWDC 2019.
Thank you and have a great WWDC!
-
-
0:52 - Player as a struct
struct Player { var icon: String } func test() { let player1 = Player(icon: "🐸") var player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
1:55 - Player as a class
class PlayerClass { var icon: String init(_ icon: String) { self.icon = icon } } func test() { let player1 = PlayerClass("🐸") let player2 = player1 player2.icon = "🚚" assert(player1.icon == "🐸") }
-
3:00 - Deeply copying a PlayerClass
class PlayerClass { var data: Icon init(_ icon: String) { self.data = Icon(icon) } init(from other: PlayerClass) { self.data = Icon(from: other.data) } } func test() { let player1 = PlayerClass("🐸") var player2 = player1 player2 = PlayerClass(from: player2) player2.data.icon = "🚚" assert(player1.data.icon == "🐸") } struct Icon { var icon: String init(_ icon: String) { self.icon = icon } init(from other: Icon) { self.icon = other.icon } }
-
5:10 - Copyable BankTransfer
class BankTransfer { var complete = false func run() { assert(!complete) // .. do it .. complete = true } deinit { if !complete { cancel() } } func cancel() { /* ... */ } } func schedule(_ transfer: BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() } try await Task.sleep(for: delay) transfer.run() } func startPayment() async { let payment = BankTransfer() log.append(payment) try? await schedule(payment, .seconds(3)) } let log = Log() final class Log: Sendable { func append(_ transfer: BankTransfer) { /* ... */ } }
-
7:46 - Copying FloppyDisk
struct FloppyDisk: ~Copyable {} func copyFloppy() { let system = FloppyDisk() let backup = consume system load(system) // ... } func load(_ disk: borrowing FloppyDisk) {}
-
8:18 - Missing ownership for FloppyDisk
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: FloppyDisk) { // ... }
-
9:00 - Consuming ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: consuming FloppyDisk) { // ... }
-
9:26 - Borrowing ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { let result = FloppyDisk() format(result) return result } func format(_ disk: borrowing FloppyDisk) { var tempDisk = disk // ... }
-
9:55 - Inout ownership
struct FloppyDisk: ~Copyable { } func newDisk() -> FloppyDisk { var result = FloppyDisk() format(&result) return result } func format(_ disk: inout FloppyDisk) { var tempDisk = disk // ... disk = tempDisk }
-
10:28 - Noncopyable BankTransfer
struct BankTransfer: ~Copyable { consuming func run() { // .. do it .. discard self } deinit { cancel() } consuming func cancel() { // .. do the cancellation .. discard self } }
-
11:10 - Schedule function for noncopyable BankTransfer
func schedule(_ transfer: consuming BankTransfer, _ delay: Duration) async throws { if delay < .seconds(1) { transfer.run() return } try await Task.sleep(for: delay) transfer.run() }
-
12:12 - Overview of conformance constraints
struct Command { } protocol Runnable { consuming func run() } extension Command: Runnable { func run() { /* ... */ } } func execute1<T>(_ t: T) {} func execute2<T>(_ t: T) where T: Runnable { t.run() } func test(_ cmd: Command, _ str: String) { execute1(cmd) execute1(str) execute2(cmd) execute2(str) // expected error: 'execute2' requires that 'String' conform to 'Runnable' }
-
15:50 - Noncopyable generics: 'execute' function
protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } } func execute2<T>(_ t: T) where T: Runnable { t.run() } func execute3<T>(_ t: consuming T) where T: Runnable, T: ~Copyable { t.run() } func test() { execute2(Command()) execute2(BankTransfer()) // expected error: 'execute2' requires that 'BankTransfer' conform to 'Copyable' execute3(Command()) execute3(BankTransfer()) }
-
18:05 - Conditionally Copyable
struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } func runEndlessly(_ job: consuming Job<Command>) { while true { let current = copy job current.action?.run() } } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } }
-
19:27 - Extensions of types with noncopyable generic parameters
extension Job { func getAction() -> Action? { return action } } func inspectCmd(_ cmdJob: Job<Command>) { let _ = cmdJob.getAction() let _ = cmdJob.getAction() } func inspectXfer(_ transferJob: borrowing Job<BankTransfer>) { let _ = transferJob.getAction() // expected error: method 'getAction' requires that 'BankTransfer' conform to 'Copyable' } struct Job<Action: Runnable & ~Copyable>: ~Copyable { var action: Action? } extension Job: Copyable where Action: Copyable {} protocol Runnable: ~Copyable { consuming func run() } struct Command: Runnable { func run() { /* ... */ } } struct BankTransfer: ~Copyable, Runnable { consuming func run() { /* ... */ } }
-
20:14 - Cancellable for Jobs with Copyable actions
protocol Cancellable { mutating func cancel() } extension Job: Cancellable { mutating func cancel() { action = nil } }
-
21:00 - Cancellable for all Jobs
protocol Cancellable: ~Copyable { mutating func cancel() } extension Job: Cancellable where Action: ~Copyable { mutating func cancel() { action = nil } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。