Protocols and Generics
Protocol-oriented programming, associated types, generic constraints, `some` vs `any`, and how protocol requirements are dispatched.
13 questions
JuniorTheoryVery commonWhat is a protocol, how does it differ from a base class, and what is protocol-oriented programming?
What is a protocol, how does it differ from a base class, and what is protocol-oriented programming?
A protocol is a contract of requirements with no stored state of its own. Unlike a base class it allows many conformances and fits structs and enums, not just classes. Protocol-oriented programming composes small protocols with default extensions.
Common mistakes
- ✗Thinking a protocol stores state the way a base class does
- ✗Believing only classes can conform to a protocol
- ✗Assuming a type can conform to just one protocol
Follow-up questions
- →Why can a struct adopt a protocol but not inherit from a class?
- →What do default extensions add to protocol-oriented programming?
JuniorTheoryCommonWhat can and cannot go into an extension, and why can't it add a stored property?
What can and cannot go into an extension, and why can't it add a stored property?
An extension can add computed properties, methods, initializers, subscripts, nested types, and protocol conformances to a type. It cannot add a stored property, because that changes the type's memory layout, fixed at the original declaration.
Common mistakes
- ✗Believing an extension can add a stored property
- ✗Thinking an extension can override an existing method of the base type
- ✗Assuming extensions work only on your own types, not standard-library ones
Follow-up questions
- →Why does adding a stored property require changing the type's declaration?
- →How can an extension add per-instance state without a stored property?
JuniorTheoryCommonWhich standard-library protocols should a model type conform to, and what does each unlock?
Which standard-library protocols should a model type conform to, and what does each unlock?
Codable unlocks JSON encoding and decoding, Equatable enables ==, Hashable lets a type be a Set element or dictionary key, Identifiable gives SwiftUI a stable id for lists, and CustomStringConvertible sets its description in logs.
Common mistakes
- ✗Confusing which protocol enables
Setand dictionary-key use — it isHashable - ✗Thinking
Identifiableis about encoding rather than a stableid - ✗Assuming all these protocols are always synthesized without opting in
Follow-up questions
- →Which of these does the compiler synthesize for you, and when?
- →Why does a SwiftUI
ListneedIdentifiable?
MiddleCodeCommonWrite a conditional conformance that makes Array Summable, and explain what it unlocks.
Write a conditional conformance that makes Array Summable, and explain what it unlocks.
A conditional conformance makes a generic type conform only when its parameter does — extension Array: Summable where Element: Summable. It unlocks the API for [Int] but not [UIView], as [T] becomes Equatable exactly when its T is.
Common mistakes
- ✗Thinking the conformance applies to every element type unconditionally
- ✗Reimplementing element logic instead of delegating to the element's conformance
- ✗Believing a
whereclause on a function equals a conditional conformance
Follow-up questions
- →Why does
[UIView]fail to compile against the conformance? - →How does the standard library make
[T]Equatablethis way?
MiddleCodeCommonWhen does Swift synthesize Equatable/Hashable, and when must you write ==/hash(into:) by hand?
When does Swift synthesize Equatable/Hashable, and when must you write ==/hash(into:) by hand?
Swift synthesizes Equatable/Hashable when every stored property conforms and you declare it in the type's file. Write them by hand when equality should ignore fields — compare only id in == and feed just id to hash(into:).
Common mistakes
- ✗Thinking synthesis still works when a stored property doesn't conform
- ✗Feeding different fields to
==andhash(into:), breaking the hash contract - ✗Believing you must always write both by hand
Follow-up questions
- →Why must equal values always produce equal hashes?
- →Where must the conformance be declared for synthesis to kick in?
MiddleTheoryCommonA protocol supplies a method default in an extension — which runs through any P versus a generic some P?
A protocol supplies a method default in an extension — which runs through any P versus a generic some P?
A declared requirement runs the type's own override through both any P and some P — the witness table picks it, and the default fills in only when the type adds none. A method living only in the extension is static; its default always wins.
Common mistakes
- ✗Believing the extension default always wins, even when the type declares the requirement itself
- ✗Thinking
some Pandany Ppick different implementations for the same declared requirement - ✗Not distinguishing a protocol requirement from a method that exists only in the extension
Follow-up questions
- →Why does a method declared only in the extension dispatch on the static type?
- →How does adding the method to the protocol requirement change which implementation runs?
MiddleTheoryCommonWhat do some P and any P each mean, and when do you actually need the existential any?
What do some P and any P each mean, and when do you actually need the existential any?
some P is an opaque type — one concrete type the compiler knows, so calls stay static and unboxed. any P is an existential box holding any conforming type, dispatched dynamically via the witness table. Reach for any when the type must vary.
Common mistakes
- ✗Treating
some Pandany Pas interchangeable - ✗Thinking
some Perases the concrete type at run time the wayany Pdoes - ✗Reaching for
any Pwhen a single fixed return type would do
Follow-up questions
- →Why can
some Pbe specialized whileany Pcannot? - →When does an existential's boxing cost actually matter?
SeniorCodeCommonWrite a generic mostFrequent function with a where-style constraint and explain each constraint.
Write a generic mostFrequent function with a where-style constraint and explain each constraint.
func mostFrequent<T: Hashable>(_ xs: [T]) -> T? — the Hashable bound lets T be a dictionary key to tally counts, and T? is returned because empty input has no mode. A where clause adds constraints like T: Comparable to break ties.
Common mistakes
- ✗Dropping the
Hashableconstraint while still usingTas a dictionary key - ✗Returning a non-optional
Teven though an empty input has no mode - ✗Thinking a
whereclause is illegal on a free function
Follow-up questions
- →Why does using
Tas a dictionary key forceT: Hashable? - →How would
T: Comparablehelp break a frequency tie?
SeniorTheoryOccasionalAssociated type versus generic parameter on a protocol — which do you reach for, and what does each cost the caller?
Associated type versus generic parameter on a protocol — which do you reach for, and what does each cost the caller?
An associated type is fixed by the conformer — one per conformance, not chosen by the caller. A generic parameter is picked by the caller at each use. Use an associated type for a type intrinsic to the conformer, a generic when the caller decides.
Common mistakes
- ✗Thinking the caller chooses an associated type
- ✗Believing a generic parameter and an associated type are always interchangeable
- ✗Assuming an associated type can vary per call the way a generic parameter does
Follow-up questions
- →Why can a protocol have only one associated type value per conformance?
- →When does forcing the caller to pick the type hurt the API?
SeniorPerformanceOccasionalHow do whole-module optimization, final, and private let the compiler devirtualize and specialize protocol calls?
How do whole-module optimization, final, and private let the compiler devirtualize and specialize protocol calls?
Whole-module optimization sees every call site at once, so it can prove a method is never overridden and make a vtable or witness call direct. final and private state that locally, letting generic specialization inline the concrete type.
Common mistakes
- ✗Thinking whole-module optimization only speeds up compilation
- ✗Believing
finalandprivatechange behavior rather than enabling devirtualization - ✗Assuming generic specialization happens at run time
Follow-up questions
- →Why does a
privatemethod give the optimizer the same guarantee asfinal? - →What blocks devirtualization across a module boundary?
SeniorTheoryOccasionalWhen does Swift use direct, vtable, witness-table, or the Objective-C runtime's objc_msgSend dispatch?
When does Swift use direct, vtable, witness-table, or the Objective-C runtime's objc_msgSend dispatch?
Direct (static) dispatch inlines the call for final, private, static, and struct methods. Non-final class methods use a vtable; protocol requirements via generics or existentials use a witness table; @objc dynamic members use objc_msgSend.
Common mistakes
- ✗Assuming every Swift call uses
objc_msgSendthe way Objective-C did - ✗Confusing the class vtable with the protocol witness table
- ✗Forgetting that
final,private, andstaticenable direct dispatch
Follow-up questions
- →Why can the optimizer turn a vtable call into a direct one?
- →What makes a member use
objc_msgSendinstead of a vtable?
SeniorPerformanceOccasionalWhat does [any Shape] cost at run time compared with a generic [T] where T: Shape?
What does [any Shape] cost at run time compared with a generic [T] where T: Shape?
[any Shape] wraps each element in an existential container and dispatches calls via the witness table, unspecialized. [T] where T: Shape is one concrete type the optimizer specializes and inlines, so calls stay static, elements unboxed.
Common mistakes
- ✗Assuming
[any Shape]and a generic array compile to the same code - ✗Thinking existential boxing is free for small values
- ✗Believing the optimizer can specialize calls made through an existential
Follow-up questions
- →When is the flexibility of
[any Shape]worth its run-time cost? - →What is stored inline in an existential container before it spills to the heap?
SeniorTheoryRareWhy did a protocol with an associated type historically break any P, and what did primary associated types change?
Why did a protocol with an associated type historically break any P, and what did primary associated types change?
An associated type gives a protocol no single concrete type, so before Swift 5.7 it worked only as a generic constraint, not an existential. Swift 5.7 allowed the existential and added primary associated types to pin one — any Collection<Int>.
Common mistakes
- ✗Thinking an associated type is just a generic parameter with the same power as
any - ✗Believing primary associated types drop constraints entirely rather than pin one
- ✗Assuming protocols with associated types still can't be existentials after Swift 5.7
Follow-up questions
- →What does
any Collection<Int>guarantee that bareany Collectiondoes not? - →Why did the compiler once reject such a protocol as an existential?