Structures and enumerations
Define your own types: structures that bundle data with behavior, the rational-number data abstraction, and enumerations whose cases make invalid states impossible.
- CS61A
- Objects · Composing Programs 2.4–2.5
- Swift book
- Structures and Classes · Enumerations · Properties · Methods
Arrays, dictionaries, and tuples combine values, but they do not say what the combination means. A pair of integers could be a point, a fraction, or a date. Swift lets you define new types that carry that meaning, and the behavior that goes with it.
Structures
A structure groups named values, called stored properties, into one type:
struct Point {
var x: Int
var y: Int
}
var p = Point(x: 3, y: 4) // a memberwise initializer, written for you
p.x += 1
print(p.x, p.y) // 4 4
print(p) // Point(x: 4, y: 4)
A structure can also have methods, functions that belong to the type, and computed properties, values calculated each time they are read:
struct Point {
var x: Int
var y: Int
var distanceFromOrigin: Double {
Double(x * x + y * y).squareRoot()
}
func moved(by dx: Int, _ dy: Int) -> Point {
Point(x: x + dx, y: y + dy)
}
}
let p = Point(x: 3, y: 4)
print(p.distanceFromOrigin) // 5.0
print(p.moved(by: 1, 1)) // Point(x: 4, y: 5)
Inside a method, the type’s properties are available by name: x means
this point’s x.
Structures are values. Assigning one to a new name copies it, and the two copies are independent from then on:
What would Swift print?
struct Point {
var x: Int
var y: Int
}
var a = Point(x: 1, y: 2)
var b = a
b.x = 9
print(a.x, b.x)Show answer and explanation
Answer1 9
var b = a copies the whole structure. Changing b.x does not affect a,
so a.x is still 1. Lesson 14 is about when this is exactly what you want,
and Lesson 15 about when it is not.
Data abstraction: rational numbers
CS61A’s first data abstraction is the rational number: a fraction with an integer numerator and denominator. The point is not fractions; it is the abstraction barrier. Code that uses rationals should add and multiply them without knowing how they are stored.
func gcd(_ a: Int, _ b: Int) -> Int {
b == 0 ? abs(a) : gcd(b, a % b)
}
struct Rational {
let numerator: Int
let denominator: Int
init(_ numerator: Int, _ denominator: Int) {
precondition(denominator != 0, "a denominator cannot be zero")
let divisor = gcd(numerator, denominator) * (denominator < 0 ? -1 : 1)
self.numerator = numerator / divisor
self.denominator = denominator / divisor
}
var description: String { "\(numerator)/\(denominator)" }
func adding(_ other: Rational) -> Rational {
Rational(numerator * other.denominator + other.numerator * denominator,
denominator * other.denominator)
}
func multiplied(by other: Rational) -> Rational {
Rational(numerator * other.numerator, denominator * other.denominator)
}
}
let half = Rational(1, 2)
let third = Rational(1, 3)
print(half.adding(third).description) // 5/6
print(half.multiplied(by: third).description) // 1/6
The custom initializer, init, runs whenever a Rational is made. It
puts every fraction in lowest terms with a positive denominator, so 2/4
and -1/-2 both become 1/2. self.numerator means “this value’s
property”, distinguishing it from the parameter with the same name. Because
the initializer is the only way in, no code anywhere can create a fraction
that is not reduced.
What would Swift print?
func gcd(_ a: Int, _ b: Int) -> Int { b == 0 ? abs(a) : gcd(b, a % b) }
struct Rational {
let numerator: Int
let denominator: Int
init(_ n: Int, _ d: Int) {
let g = gcd(n, d) * (d < 0 ? -1 : 1)
numerator = n / g
denominator = d / g
}
}
let r = Rational(6, -4)
print(r.numerator, r.denominator)Show answer and explanation
Answer-3 2
The greatest common divisor of 6 and −4 is 2, and the negative denominator flips its sign to −2. Dividing both parts by −2 gives −3 and 2.
let properties cannot change after initialization; trying to assign
r.numerator = 1 is a compile-time error. A value that cannot be changed
cannot be put into an invalid state later.
Enumerations
An enumeration is a type with a fixed list of possible values, its cases:
enum Direction {
case north, south, east, west
}
let heading = Direction.east
switch heading {
case .north, .south:
print("vertical")
case .east, .west:
print("horizontal")
}
Where the type is already known, .east is enough. A switch over an enum
must handle every case, so if a new case is added later, the compiler finds
every switch that needs updating.
Cases can carry raw values of a fixed type, and enums can have computed properties and methods like structures:
enum Suit: String {
case spades = "♠", hearts = "♥", diamonds = "♦", clubs = "♣"
var color: String {
switch self {
case .spades, .clubs: "black"
case .hearts, .diamonds: "red"
}
}
}
print(Suit.hearts.rawValue, Suit.clubs.color) // ♥ black
Associated values
The most powerful feature of Swift’s enums is that each case can carry its own data, called associated values:
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
var area: Double {
switch self {
case .circle(let radius):
Double.pi * radius * radius
case .rectangle(let width, let height):
width * height
}
}
}
print(Shape.rectangle(width: 2, height: 3).area) // 6.0
A Shape is either a circle with a radius or a rectangle with a width
and height, never a circle with a width. The switch takes the associated
values out with let.
What would Swift print?
enum Shape {
case circle(radius: Double)
case square(side: Double)
}
func describe(_ s: Shape) -> String {
switch s {
case .circle(let r) where r > 10: "big circle"
case .circle: "circle"
case .square(let side): "square \(Int(side))"
}
}
print(describe(.circle(radius: 20)), describe(.circle(radius: 1)), describe(.square(side: 3)))Show answer and explanation
Answerbig circle circle square 3
A where clause adds a condition to a case. The first circle has radius 20,
so it matches the first case; the second fails the condition and matches
.circle without binding the radius. The square’s side is converted to an
Int for printing.
Put the lines in order
Put the lines in order so the program prints red.
print(Suit.diamonds.color)switch self {case spades, hearts, diamonds, clubs}}}case .spades, .clubs: return "black"var color: String {case .hearts, .diamonds: return "red"enum Suit {
Show the correct program
enum Suit {
case spades, hearts, diamonds, clubs
var color: String {
switch self {
case .spades, .clubs: return "black"
case .hearts, .diamonds: return "red"
}
}
}
print(Suit.diamonds.color)Lab 11: Your own types
Q1: Subtracting and dividing rationals
Add subtracting(_:) and divided(by:) to Rational. Division by a zero
rational should stop the program with a precondition.
Show solution
func gcd(_ a: Int, _ b: Int) -> Int {
b == 0 ? abs(a) : gcd(b, a % b)
}
struct Rational: Equatable {
let numerator: Int
let denominator: Int
init(_ numerator: Int, _ denominator: Int) {
precondition(denominator != 0, "a denominator cannot be zero")
let divisor = gcd(numerator, denominator) * (denominator < 0 ? -1 : 1)
self.numerator = numerator / divisor
self.denominator = denominator / divisor
}
func adding(_ other: Rational) -> Rational {
Rational(numerator * other.denominator + other.numerator * denominator,
denominator * other.denominator)
}
func subtracting(_ other: Rational) -> Rational {
adding(Rational(-other.numerator, other.denominator))
}
func multiplied(by other: Rational) -> Rational {
Rational(numerator * other.numerator, denominator * other.denominator)
}
func divided(by other: Rational) -> Rational {
precondition(other.numerator != 0, "cannot divide by zero")
return multiplied(by: Rational(other.denominator, other.numerator))
}
}
assert(Rational(1, 2).subtracting(Rational(1, 3)) == Rational(1, 6))
assert(Rational(1, 2).divided(by: Rational(1, 4)) == Rational(2, 1))
assert(Rational(3, 4).subtracting(Rational(3, 4)) == Rational(0, 1))Both new methods reuse the old ones instead of repeating arithmetic.
: Equatable asks Swift to generate == for Rational by comparing its
properties, which is correct here because every rational is stored reduced.
Lesson 16 explains what that declaration means.
Q2: Naming a card
Define an enum Suit with String raw values and a structure Card with a
rank: Int (1 to 13) and a suit: Suit. Give Card a computed property
name that returns strings like "Queen of hearts", where 1 is Ace, 11
Jack, 12 Queen, and 13 King.
Show solution
enum Suit: String {
case spades, hearts, diamonds, clubs
}
struct Card {
let rank: Int
let suit: Suit
var name: String {
let rankName = switch rank {
case 1: "Ace"
case 11: "Jack"
case 12: "Queen"
case 13: "King"
default: String(rank)
}
return "\(rankName) of \(suit.rawValue)"
}
}
assert(Card(rank: 12, suit: .hearts).name == "Queen of hearts")
assert(Card(rank: 1, suit: .spades).name == "Ace of spades")
assert(Card(rank: 7, suit: .clubs).name == "7 of clubs")When raw values are String and none are given, each case’s raw value is
its own name. switch can be an expression, like if, when each case is a
single value.
Q3: Total area
Using Shape from this lesson, extend it with a triangle(base:height:)
case, and write totalArea(_:) for an array of shapes.
Show solution
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
case triangle(base: Double, height: Double)
var area: Double {
switch self {
case .circle(let radius): Double.pi * radius * radius
case .rectangle(let width, let height): width * height
case .triangle(let base, let height): base * height / 2
}
}
}
func totalArea(_ shapes: [Shape]) -> Double {
shapes.map { $0.area }.reduce(0, +)
}
assert(totalArea([.rectangle(width: 2, height: 3), .triangle(base: 4, height: 5)]) == 16)
assert(abs(totalArea([.circle(radius: 1)]) - Double.pi) < 1e-12)
assert(totalArea([]) == 0)Adding the case without updating area would not compile: the switch
would no longer be exhaustive. That is the compiler pointing you to the one
place that needs the new formula.
What’s next
Next lesson: types that contain themselves, used to build linked lists and trees.