Constraints

Constraints

When you define a generic function or type, you can set constraints on the types that may be used. Constraints can be protocols or classes, and the syntax is <T: Protocol> or <T: Class>.

<T: Protocol>

The generic function swapValues<T> works with all types, but consider this generic function:

func max<T>(lhs: T, rhs: T) -> T {
  return lhs > rhs ? lhs : rhs
}

This produces a compiler error informing you that T must conform to Comparable because the type T must be able to supply an answer to lhs > rhs ?:

func max<T: Comparable>(lhs: T, rhs: T) -> T {
  return lhs > rhs ? lhs : rhs
}

Here’s a class you saw in Introduction to Protocol-Oriented Programming:

class FamilyCar: Vehicle { ... }

To follow along, open Starter/POPIntro.playground.

Instantiate a second instance of this class, then try to call max(lhs:rhs:):

let myCar = FamilyCar()
max(lhs: familyCar, rhs: myCar)

The compiler flags an error: Global function ‘max(lhs:rhs:)’ requires that ‘FamilyCar’ conform to ‘Comparable’. Actually, as soon as you type max, Xcode suggests code that uses the first Comparable property of familyCar:

max(lhs: familyCar.maxSpeed, rhs: myCar.maxSpeed)

<T: Class>

Another class in the same module is VehicleType, which has a move method:

class VehicleType { 
  ...
  func move(direction: Direction, duration: TimeInterval, speed: Double) { ... } 
}

In your app, you might want to define a function that moves a person from locationA to locationB. You want to use the move method, and the person could use any VehicleType, so you set the constraint T: VehicleType:

func moveAtoB<T: VehicleType>(vehicle: T, locationA: CLLocation, locationB: CLLocation) { ... }

Note: This code is for illustration only. It’s not in the playground.

Where Clause

Use where when you need to set more than one constraint, for example, if T must inherit from the VehicleType class and also conform to the Comparable protocol:

<T where T: VehicleType, T: Comparable>  // illustration only

And you’ve already seen this array initializer that creates an array containing the elements of a sequence S:

init<S>(_ s: S) where Element == S.Element, S : Sequence

You can also use where to constrain extensions of generic types. Return to Generics.playground and find extension Setting:

extension Setting {
  mutating func save(from untypedValue: Any) {
    if let value = untypedValue as? T {
      self.value = value
    }
  }
}

If you add another save(from:) method to decode JSON data, you need a Decodable constraint:

extension Setting where T: Decodable {
  mutating func save(from untypedValue: Any) {
    if let value = untypedValue as? T {
      self.value = value
    }
  }
  
  mutating func save(from json: Data) throws {
    let decoder = JSONDecoder()
    self.value = try decoder.decode(T.self, from: json)
  }  
}

Or you can constrain only the save(from json:) method that requires the constraint:

extension Setting {
  mutating func save(from untypedValue: Any) {
    if let value = untypedValue as? T {
      self.value = value
    }
  }
  
  mutating func save(from json: Data) throws where T: Decodable {
    let decoder = JSONDecoder()
    self.value = try decoder.decode(T.self, from: json)
  }
}

Here, the save(from json:) method doesn’t have a parameter of type T, so you must use where T: Decodable instead of save<T: Decodable>.

Those are the basics of generics. You’ll learn about two more topics in the next lesson.

See forum comments
Download course materials from Github
Previous: Generics Next: Generics - Conclusion