Reputation: 4832
We are trying to prepare for Swift 6 by enabling strict concurrency checking. For the most part I understand how we may solve the produced warnings, but there's one area that I'm struggling with, and thats when using protocol abstractions.
For example, what's is the most appropriate way to solve the following warning?
import Foundation
protocol FooProtocol {
func foo()
}
struct Foo: FooProtocol {
func foo() {}
}
struct Bar: Sendable {
// Stored property 'foo' of 'Sendable'-conforming struct 'Bar'
// has non-sendable type 'any FooProtocol'
let foo: FooProtocol
init(foo: FooProtocol) {
self.foo = foo
}
}
let bar = Bar(foo: Foo())
We can easily silence the warning by conforming FooProtocol
to Sendable
:
protocol FooProtocol: Sendable {}
However I'm not sure this feels correct. By adding this annotation, are we saying that the concrete type conforming to FooProtocol
is Sendable, or only that the functions inside conform to Sendable
?
Upvotes: 3
Views: 5904
Reputation: 438122
You note:
We can easily silence the warning by conforming
FooProtocol
toSendable
… However I'm not sure this feels correct.
I understand the general concern, but if you are going to use this property within a Sendable
type, the compiler obviously needs to know that the property is also Sendable
before it can be assured that Bar
truly conforms to Sendable
or not.
So, yes, you are correct that you could resolve this by requiring that all FooProtocol
types be Sendable
.
The alternative (if it does not make sense for all FooProtocol
conforming types to require Sendable
conformance, too) is to define Bar
such that it only accepts those particular FooProtocol
types that also are Sendable
, too. Perhaps Bar
does not need to accept all FooProtocol
conforming types, but rather only explicitly those that conform to both FooProtocol & Sendable
. E.g.:
protocol FooProtocol {
func foo()
}
// can be used as property of `Bar`
struct Foo: FooProtocol, Sendable {
func foo() {…}
}
// will never be used as property of `Bar`
class Foo2: FooProtocol {
func foo() {…}
}
// This will be used with only those `FooProtocol` conforming types that are also `Sendable`
struct Bar: Sendable {
let foo: FooProtocol & Sendable
init(foo: FooProtocol & Sendable) {
self.foo = foo
}
}
let bar1 = Bar(foo: Foo()) // OK
let bar2 = Bar(foo: Foo2()) // As anticipated, an error: “Type 'Foo2' does not conform to the 'Sendable' protocol”
It comes down to the intrinsic nature of FooProtocol
:
For example, if FooProtocol
represents model types and you plan on routinely sending these object across concurrency contexts, then it might make sense to just have this the protocol inherit the Sendable
requirement, too, as you outlined in your question.
However, perhaps FooProtocol
is some more abstract protocol, less intrinsic to the fundamental nature of the object, but rather just represents some behavior that only some types might adopt. A few example protocols might include things like Encodable
/Decodable
or CustomStringConvertible
. These are scenarios where you could easily have Foo
-like objects that conform and others that do not, and where the context of Bar
makes it clear that it is only applicable to those types that conform to both protocols.
It all comes down to the intrinsic nature of the FooProtocol
protocol and the Bar
type. It is hard to answer in the abstract.
Upvotes: 1
Reputation: 54755
If you make a protocol inherit from another protocol, all conforming types have to conform to the inherited protocol as well.
So by making your FooProtocol
inherit Sendable
, all conforming types of FooProtocol
also have to conform to Sendable
.
Upvotes: 1