Highlight
Swift 5.7 makes generic code easier to write and read. Holly used an example of a farm simulator to demonstrate the complete abstraction process from concrete types to function overloading, class inheritance, to protocols and generics, and introduced
someandanyTwo new syntaxes for different uses with parameters and collections.
Core Content
Generics are an essential tool for managing code complexity. Holly used a farm simulator throughout to demonstrate the complete evolution of code from concrete to abstract. (00:17)
The starting point is simple: aCowstruct, aHaystruct, aAlfalfastruct, aFarm struct。Cow.eatacceptHay,Hay.growreturnAlfalfa,Alfalfa.harvestreturnHay。Farm.feedFirst plant alfalfa, then harvest hay, and finally feed the cattle. (01:48)
When you want to add more animals, the most direct way is to overload the function: giveFarmaddfeed(horse:)、feed(chicken:). But the implementation of each overload is almost the same, just the type is different. Too many overloads and it becomes boilerplate code. (02:46)
Next try class inheritance: introductionAnimalbase class,Cow、Horse、Chickeninherit it. But problems soon arise: each animal eats a different food,Animal.eatIt’s awkward to write anything about the parameter type. WriteAnyType safety will be lost, and the wrong food may be passed at runtime; writing generic class parameters will cause all references toAnimalFood type parameters must be provided everywhere, which is very cumbersome. (05:04)
Protocols are better abstraction tools.AnimalThe protocol has an associated typeFeedand a methodeat(_ food: Feed). Each specific animal type matchesAnimalProtocol, automatically inferred by the compilerFeedAssociation type.CowofeatacceptHay, the compiler will knowCow.Feed == Hay。(08:44)
Swift 5.7 introducedsomeKeywords make generic parameter declarations more concise.func feed(_ animal: some Animal)Equivalent to explicit generic parameters with where clause, but with cleaner syntax.someRepresents “a specific type that conforms to the protocol”. The underlying type is determined at the time of call and remains unchanged thereafter. (12:52)
If you need to store different types of animals (for example, there are both cows and horses in an array), usesomeNo, becausesomeRequires the underlying type to be fixed. Use this timeany:[any Animal]Represents “any type that conforms to the Animal protocol”.anyIs an existential type, and the underlying type can change at runtime. (21:04)
any Animalandsome Animalcan be automatically converted between. existfeedAllIn the method, traverse[any Animal], each element will be automatically unboxed intosome Animalpass tofeedmethod, like thisfeedAssociated types can still be used internally. (23:00)
Holly’s suggestion is: write by defaultsome, only use it instead if you need to store any typeany. This is the same idea as “let is used by default, and var is used when modification is needed”. (26:12)
Detailed Content
From Concrete to Abstract: Identifying Repeating Patterns
(01:48) The initial code only has specific types:
struct Cow {
func eat(_ food: Hay) {}
}
struct Hay {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa {
func harvest() -> Hay {
Hay()
}
}
struct Farm {
func feed(_ cow: Cow) {
let crop = Hay.grow()
let produce = crop.harvest()
cow.eat(produce)
}
}
Key points:
- Each type has clear responsibilities: Cow eats, Hay grows, Alfalfa harvests, Farm feeds
- But
feedOnly Cow can be fed, adding Horse and Chicken needs to be rewritten
An attempt at function overloading:
struct Farm {
func feed(_ cow: Cow) { ... }
func feed(_ horse: Horse) { ... }
func feed(_ chicken: Chicken) { ... }
}
Key points:
- The implementation of each overload is almost the same
- Adding new animal types requires adding new overloads
- This is “ad-hoc polymorphism”, not a general solution
Why class inheritance is not suitable for this scenario
(05:04) Try using class inheritance:
class Animal {
func eat(_ food: Any) {} // Problem: what should the parameter type be?
}
class Cow: Animal {
override func eat(_ food: Any) {
guard let hay = food as? Hay else { ... }
// Feed hay
}
}
Key points:
- Enforce reference semantics, but animal instances do not need to share state
- Subclasses must override. If you forget, you will only find it at runtime.
-
AnyParameters lose type safety and are checked at runtime - If using generic class parameters
Animal<Food>, all references must have type parameters
Use protocols to define capability interfaces
(08:44) The protocol abstracts “animals eat food” into an interface:
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
protocol AnimalFeed {
associatedtype CropType: Crop where CropType.Feed == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype Feed: AnimalFeed where Feed.CropType == Self
func harvest() -> Feed
}
Concrete types conform to the protocol:
struct Cow: Animal {
func eat(_ food: Hay) {}
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa: Crop {
func harvest() -> Hay {
Hay()
}
}
Key points:
- Association type
FeedDepends on the specific conforming type -Cow.eatacceptHay, the compiler infersCow.Feed == Hay- The protocol is not limited to class, struct, enum, and actor can all comply - The compiler checks that each conforming type implements all requirements
some keyword: simplify generic parameters
(12:52) Traditional generic writing:
func feed<A: Animal>(_ animal: A) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
Simplified writing for Swift 5.7:
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
Key points:
some AnimalEquivalent to explicit generic parametersA: AnimalsomeRepresents “a concrete, protocol-conforming type”- The underlying type is fixed, and the compiler can use associated types in the function body
- Applies to parameter position and return position (such as SwiftUI’s
some View)
When are explicit type parameters required? When the same type needs to be referenced multiple times in a signature:
func buildHabitat<A: Animal>(for animal: A) -> A.Habitat {
// Both the parameter type and return type depend on A
}
any keyword: store any type
(21:04)someThe underlying type is required to be fixed, so[some Animal]The array can only store the same animal. To save mixed types, useany:
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal) // Automatically opens the existential
}
}
}
Key points:
any AnimalIt is an existing type and can store any specific type that conforms to Animal- Type erasure eliminates static type distinctions between concrete types
- But
any AnimalThe associated type cannot be accessed directly because the underlying type is not fixed - Swift 5.7 supports automatic unpacking:
any AnimalParameters can be passed tosome Animalparameters - After unpacking, in
feedWithin the function body, the underlying type is fixed and associated types can be used normally.
someandanyComparison of:
| Features | some | any |
|---|---|---|
| underlying type | fixed | variable |
| association type | available | not available (unless unpacked) |
| Heterogeneous collections | Not supported | Supported |
| Optional value | The underlying type must be specified | Can be used directlyany Animal? |
| Performance | Untyped erasure overhead | Typed erasure overhead |
Complete example
(27:10) The complete code that combines all the concepts:
protocol AnimalFeed {
associatedtype CropType: Crop where CropType.Feed == Self
static func grow() -> CropType
}
protocol Crop {
associatedtype Feed: AnimalFeed where Feed.CropType == Self
func harvest() -> Feed
}
protocol Animal {
associatedtype Feed: AnimalFeed
func eat(_ food: Feed)
}
struct Farm {
func feed(_ animal: some Animal) {
let crop = type(of: animal).Feed.grow()
let produce = crop.harvest()
animal.eat(produce)
}
func feedAll(_ animals: [any Animal]) {
for animal in animals {
feed(animal)
}
}
}
struct Cow: Animal {
func eat(_ food: Hay) {}
}
struct Hay: AnimalFeed {
static func grow() -> Alfalfa {
Alfalfa()
}
}
struct Alfalfa: Crop {
func harvest() -> Hay {
Hay()
}
}
Key points:
AnimalFeedandCropThe associated types constrain each other to ensure that feed and crops match -Farm.feedusesome AnimalFeeding individual animals -Farm.feedAlluse[any Animal]Feeding mixed animal groups- The compiler guarantees against feeding the wrong food: you can’t put
Haypass to eatCornofChicken
Core Takeaways
-
What to do: Review function overloading in the project and change duplicate implementations to generic functions.
Why It’s Worth Doing: Holly’s Farm Example, 3 AnimalsfeedThe overloaded implementation is almost the same. Change tofunc feed(_ animal: some Animal)After that, add aSheepThe type does not need to be changedFarmany code.
How to start: Search for similar overloaded functions in the project and find out the common capabilities of their operations (such as callingeatmethod), extract it into a protocol, usesomeWrite a generic version of the parameter. -
What to do: Use
someReplace explicit generic parameters to make code more readable.
Why it’s worth doing:func feed(_ animal: some Animal)Comparefunc feed<A: Animal>(_ animal: A)Half the time, and the semantic information appears directly in the parameter declaration. Holly emphasized that this is a common pattern in Swift 5.7.
How to start: Traverse the generic functions in the project. If the type parameter only appears once in the parameter list, change it tosomegrammar. If type parameters appear in both parameter and return types, explicit parameters are retained. -
What to do: Use
anyBuild heterogeneous collections but unpack back as quickly as possiblesome.
Why it’s worth doing:[any Animal]It is possible to store mixed types, but when internal operations require related types, type erasure will get in your way. Holly’sfeedAllDemonstrates best practices: collections withany, passed to the receiver when traversing elementssomefunction that restores full type capabilities within the function body.
How to start: Used in checking projectsany(or where the type exists implicitly), find the code that needs to access the associated type, and extract the operation into acceptsomefunction to let the compiler unpack it automatically.
Related Sessions
- Design protocol interfaces in Swift — learn how to design swift protocols that are easy to use and understand.
- Swift concurrency: Behind the scenes — dive deep into the swift concurrency runtime and learn how async/await and actors work under the hood.
- What’s new in Swift — discover the latest features and improvements in the swift programming language.
Comments
GitHub Issues · utterances