Chapters

Hide chapters

Swift Apprentice

1 min

Section III: Building Your Own Types

Section 3: 8 chapters
Show chapters Hide chapters

Section IV: Advanced Topics

Section 4: 13 chapters
Show chapters Hide chapters

17. Generics
Written by Alexis Gallagher

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

The truth is, you already know about generics. Every time you use a Swift array, you’re using generics. This might give the impression that generics are about collections, but that impression is not correct. In this chapter, you’ll learn the fundamentals of generics, giving you a solid foundation for understanding how to write your own generic code. Finally, you’ll loop back to look at generic types in the Swift standard library — arrays, dictionaries and optionals — using this new perspective.

Introducing generics

To get started, you’ll consider how you might model pets and their keepers. You could do this using different values for each or by using different types for each. You’ll see that using types, instead of values, the Swift type checker can reason about your code at compile time. Not only do you need to do less at runtime, but you can catch problems that would have slipped under the radar had you just used values. Your code also runs faster.

Values defined by other values

Suppose you’re running a pet shop that sells only dogs and cats, and you want to use a Swift playground to model that business. To start, you define a type, PetKind, that can hold two possible values corresponding to the two kinds of pets that you sell:

enum PetKind {
  case cat
  case dog
}
struct KeeperKind {
  var keeperOf: PetKind
}
let catKeeper = KeeperKind(keeperOf: .cat)
let dogKeeper = KeeperKind(keeperOf: .dog)
enum EnumKeeperKind {
  case catKeeper
  case dogKeeper
}
.kig PoedutSubs(guorozUs:.fon) CidZasz yozuir CeomolLulz wogain .lip .iqv SiujobTosr(foalugEk:.did) unp.

Types defined by other types

The model above fundamentally works by varying the values of types. Now consider another way to model the pet-to-keeper system — by varying the types themselves.

class Cat {}
class Dog {}
class KeeperForCats {}
class KeeperForDogs {}
Vuy Jiigox (oq Duv...) Qaz cbnup Mookew wdroy Hax itg. Waeyet (ev Fek...) ovl.

Anatomy of generic types

Generics provide a mechanism for using one set of types to define a new set of types.

class Keeper<Animal> {}
Bef Siamag<Guq> Rub hkqiq Haedec glzuj Cel Xuemod<Zer>

var aCatKeeper = Keeper<Cat>()
var aKeeper = Keeper()  // compile-time error!

Using type parameters

Usually, though, you’ll want to do something with type parameters.

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String

  init(name: String) {
    self.name = name
  }
}
class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let jason = Keeper(name: "Jason",
                   morningCare: Cat(name: "Whiskers"),
                   afternoonCare: Cat(name: "Sleepy"))

Mini-exercises

Type constraints

In your definition of Keeper, the identifier Animal serves as a type parameter named placeholder for some concrete type you supply later.

class Keeper<Animal: Pet> {
   /* definition body as before */
}
protocol Pet { 
  var name: String { get }  // all pets respond to a name
}
extension Cat: Pet {}
extension Dog: Pet {}
extension Array where Element: Cat {
  func meow() {
    forEach { print("\($0.name) says meow!") }
  }
}
protocol Meowable {
  func meow()
}

extension Cat: Meowable { 
  func meow() {
    print("\(self.name) says meow!")
  }
}

extension Array: Meowable where Element: Meowable {
  func meow() {
    forEach { $0.meow() }
  }
}

Arrays

While the original Keeper type illustrates that a generic type doesn’t need to store anything or use its type parameter, Array, one of the most common generic types, does both.

let animalAges: [Int] = [2,5,7,9]
let animalAges: Array<Int> = [2,5,7,9]

Dictionaries

Swift generics allow multiple type parameters, each with unique constraints. A Dictionary is a straightforward example of this.

struct Dictionary<Key: Hashable, Value> // etc..
let intNames: Dictionary<Int, String> = [42: "forty-two"]
let intNames2: [Int: String] = [42: "forty-two", 7: "seven"]
let intNames3 = [42: "forty-two", 7: "seven"]

Optionals

Finally, no discussion of generics would be complete without mentioning optionals. Optionals are enumerations, but they’re also just another generic type, which you could have defined yourself.

enum OptionalDate {
  case none
  case some(Date)
}
enum OptionalString {
  case none
  case some(String)
}
struct FormResults {
  // other properties here
  var birthday: OptionalDate
  var lastName: OptionalString
}
enum Optional<Wrapped> {
  case none
  case some(Wrapped)
}
var birthdate: Optional<Date> = .none
if birthdate == .none {
  // no birthdate
}
var birthdate: Date? = nil
if birthdate == nil {
  // no birthdate
}

Generic function parameters

Functions can be generic as well. A function’s type parameter list comes after the function name. You can then use the generic parameters in the rest of the definition.

func swapped<T, U>(_ x: T, _ y: U) -> (U, T) {
  (y, x)
}

swapped(33, "Jay")  // returns ("Jay", 33)

Challenge

Before moving on, here is a challenge to test your knowledge of generics. It is best if you try to solve it yourself, but, as always, a solution is available if you get stuck.

Challenge 1: Build a collection

Consider the pet and keeper example from earlier in the chapter:

class Cat {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Dog {
  var name: String

  init(name: String) {
    self.name = name
  }
}

class Keeper<Animal> {
  var name: String
  var morningCare: Animal
  var afternoonCare: Animal

  init(name: String, morningCare: Animal, afternoonCare: Animal) {
    self.name = name
    self.morningCare = morningCare
    self.afternoonCare = afternoonCare
  }
}
let christine = Keeper<Cat>(name: "Christine")

christine.lookAfter(someCat)
christine.lookAfter(anotherCat)

Key points

  • Generics are everywhere in Swift: optionals, arrays, dictionaries, other collection structures, and most basic operators like + and ==.
  • Generics express systematic variation at the level of types via type parameters that range over possible concrete types.
  • Generics are like functions for the compiler. They are evaluated at compile-time and result in new types, which are specializations of the generic type.
  • A generic type is not a real type on its own, but more like a recipe, program, or template for defining new types.
  • Swift provides a rich system of type constraints, which lets you specify what types are allowed for various type parameters.
Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2023 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now