We will use this protocol definition as an example:
protocol Animal {
associatedtype CommodityType: Food
associatedtype FeedType: AnimalFeed
func produce() -> CommodityType
func eat(_: FeedType)
var isHungry: Bool { get }
}Understand type erasure
when you call a method returning an associated type on an existential type, the compiler will use type erasure to determine the result type of the call
type erasure replaces these associated types with corresponding existential types that have equivalent constraints
Type erasure semantics
Producing position
associatedtypes appearing in the result of a protocol method declaration are in producing position, because calling the method will produce a value of this typee.g., the
produce()return type in theAnimalprotocol definition above
The type
any Foodis called the upper bound of the associatedCommodityTypethe actual concrete type that is returned from
produce()can safely convert to the upper bound:
let animals: [any Animal] = [Cow()]
animals.map { animal in
animal.produce() // we're calling `produce()` on an `any Animal` that holds a `Cow` at runtime.
}Consuming position
associatedtypes appearing in the parameter list of a protocol method declaration are in consuming position, because calling the method will produce a value of this typethe upper bound cannot safely convert to the actual concrete type, because the concrete type is unknown
let animals = [Cow()]
animals.map { animal in
animal.eat(???) // given an arbitrary `any AnimalFeed`, there is no way to statically guarantee that it stores what Cow needs
}type erasure does not allow us to work with associated types in consuming position
instead, you must unbox the existential
anytype by passing it to a function that takes an opaquesometype
Hide implementation details
Constrained opaque result type
new in swift 5.7
written by applying type arguments in angle brackets after the protocol name - e.g.,
some Collection<any Animal>
Identify type relationships
every protocol has a
Selftype, which stands for the concrete conforming typewe can express the relationship between
associatedtypes using a same-type requirement, written in awhereclauseA same-type requirement expresses a static guarantee that two different, possibly nested associated types must in fact be the same concrete type
protocol AnimalFeed{
associatedtype CropType: Crop
where CropType.FeedType == Self // 👈🏻 same-type requirement
static func grow() -> CropType
}
protocol Crop {
associatedtype FeedType: AnimalFeed
where FeedType.CropType == Self // 👈🏻 same-type requirement
func harvest() -> FeedTypeBy understanding your data model, you can use same-type requirements to define equivalences between these different nested associated types
Generic code can then rely on these relationships when chaining together multiple calls to protocol requirements
