Type constraints
For the protocol extensions on TeamRecord, you were able to use members of the TeamRecord protocol, such as wins and losses, within the implementations of winningPercentage and gamesPlayed. Much like in an extension on a struct, class or enum, you write code as if you were writing inside of the type you’re extending.
When you write extensions on protocols, there’s an additional dimension to consider: The adopting type could also be any number of other types. In other words, when a type adopts TeamRecord, it could very well also adopt Comparable, CustomStringConvertible, or even another protocol you wrote yourself!
Swift lets you write extensions used only when the type adopting a protocol is also another type you specify. By using a type constraint on a protocol extension, you’re able to use methods and properties from another type inside the implementation of your extension.
Take the following example of a type constraint:
protocol PostSeasonEligible {
var minimumWinsForPlayoffs: Int { get }
}
extension TeamRecord where Self: PostSeasonEligible { var isPlayoffEligible: Bool {
return wins > minimumWinsForPlayoffs
}
}
You have a new protocol, PostSeasonEligible, that defines a minimumWinsForPlayoffs property. The magic happens in the extension of TeamRecord, which has a type constraint on Self: PostSeasonEligible that will apply the extension to all adopters of TeamRecord that also adopt PostSeasonEligible.
Applying the type constraint to the TeamRecord extension means that within the
extension, self is known to be both a TeamRecord and PostSeasonEligible. That means you can use properties and methods defined on both of those types.
You can also use type constraints to create default implementations on specific type combinations. Consider the case of HockeyRecord, which introduced ties in its record along with another implementation of winningPercentage:
struct HockeyRecord: TeamRecord { var wins: Int
var losses: Int var ties: Int
var winningPercentage: Double {
return Double(wins) / (Double(wins) + Double(losses) + Double(ties))
}
}
Ties are common to more than just hockey, so you could make that a protocol, instead of coupling it one specific sport:
protocol Tieable {
var ties: Int { get }
}
With type constraints, you can also make a default implementation for
winningPercentage, specifically for types that are both a TeamRecord and Tieable:
extension TeamRecord where Self: Tieable { var winningPercentage: Double {
return Double(wins) / (Double(wins) + Double(losses) + Double(ties))
}
}
Now any type that is both a TeamRecord and Tieable won’t need to explicitly implement a winningPercentage that factors in ties:
struct RugyRecord: TeamRecord, Tieable { var wins: Int
var losses: Int var ties: Int
}
let rugbyRecord = RugyRecord(wins: 8, losses: 7, ties: 1)
rugbyRecord.winningPercentage // .500
You can see that with a combination of protocol extensions and constrained protocol extensions, you can provide default implementations that make sense for very specific cases.