squarefrog
squarefrog

Reputation: 4832

Conforming Swift Protocols to Sendable

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

Answers (2)

Rob
Rob

Reputation: 438122

You note:

We can easily silence the warning by conforming FooProtocol to Sendable … 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

David Pasztor
David Pasztor

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

Related Questions