Swift Existential Types
An existential type stores a value through a protocol without fixing its concrete type. The word any marks that the stored value can come from different conforming types.
A mixed collection
The items list is typed as an array of any Named values. It holds Person and Team values side by side, and each label call runs on the stored type.
protocol Named {
var name: String { get }
func label() -> String
}
struct Person: Named {
var name: String
func label() -> String {
return "Person \(name)"
}
}
class Team: Named {
var name: String
init(name: String) {
self.name = name
}
func label() -> String {
return "Team \(name)"
}
}
let items: [any Named] = [Person(name: "Ana"), Team(name: "Falcons"), Person(name: "Ben")]
for item in items {
print(item.label())
}Reassigning an existential value
The current variable starts with a Person value. It is then reassigned to a Team value, and the second label line reflects the new stored type.
protocol Named {
var name: String { get }
func label() -> String
}
struct Person: Named {
var name: String
func label() -> String {
return "Person \(name)"
}
}
class Team: Named {
var name: String
init(name: String) {
self.name = name
}
func label() -> String {
return "Team \(name)"
}
}
var current: any Named = Person(name: "Ana")
print(current.label())
current = Team(name: "Falcons")
print(current.label())Reading through the protocol only
Each value in the list is read through the Named requirements alone. The name property prints for a Person and a Team in the same loop.
protocol Named {
var name: String { get }
func label() -> String
}
struct Person: Named {
var name: String
func label() -> String {
return "Person \(name)"
}
}
class Team: Named {
var name: String
init(name: String) {
self.name = name
}
func label() -> String {
return "Team \(name)"
}
}
let items: [any Named] = [Team(name: "Falcons"), Person(name: "Ana")]
print(items.count)
for item in items {
print(item.name)
}Example
protocol Performer {
func perform() -> String
}
struct Singer: Performer {
func perform() -> String {
return "Singer sings a song"
}
}
struct Dancer: Performer {
func perform() -> String {
return "Dancer spins"
}
}
let show: [any Performer] = [Singer(), Dancer(), Singer()]
for performer in show {
print(performer.perform())
}Key points
- A type written as any followed by a protocol name is an existential type.
- One collection of an existential type can hold values of several conforming types side by side.
- Each stored value keeps its own concrete type.
- A method call on an existential value runs the method of its stored concrete type.
- A variable of an existential type can be reassigned to a value of a different conforming type.
- An existential value lives in a container of fixed size.
- A larger value is stored in a separate buffer, and the container refers to that buffer.
In this example, the show array holds Singer and Dancer values together under any Performer. The loop calls perform on each stored value in turn. Each call runs the version from the stored concrete type, so the singer and dancer lines alternate in array order.
Note
That container and buffer are the extra storage cost of choosing an existential type.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
What can an array typed as [any Named] hold?