Classes and Structs

In Swift, there are four methods to create custom data types. We have already gone over two: enums and tuples! There are two left that are pretty similar: classes and structs.

Classes

Classes are just how you think of them in any other imperative language. Here's the syntax for a class:

class <Insert Name>: <Insert SuperClass>, <Insert Protocols to Confrom to> {}

Note: The : should only be present if there is a superclass or protocol to conform to. To see more about conformance, read the protocol page.

Fields

We're going to want to store data somehow. We use fields to do this. Fields are variables and constants that are stored within an object.

We have two choices when setting up fields. We can declare a type and force setup onto initializers or we can assign it. For constants, if we assign a value, it will be the same for all instances. Kind of... If we are literally doing the same thing every time, consider using a static variable/constant–a field that is stored in the class, not in objects. For fields that need to be distinct (you will come to understand this when we discuss when to use classes and structs later) then this it is valid to not use statics. For fields that cannot be setup at initialization, consider using an optional or implicitly-unwrapped optional. See optionals for more:

pageOptionals

Initializers

An initializer sets up all fields for an object that do not have default values and are not nil–optional fields that are not set up in an initializer will default to nil.

The syntax for an initializer is–this is a function and shares everything about them other than the func keyword:

init(<parameters>) {
    ...
}

Note for Python programmers: you do not need to pass self to initializers or methods!

When assigning or accessing fields and methods, self is implied. This means that you don't need to write self.<some method or field> unless there is a local variable with the same name.

Another note about initializers: You cannot reference self until you have set up all of your properties. I.e. you cannot call functions in your initializer until you have finished setup.

Default Variables

To define a default value for a field you have two options. The first is to create a default parameter within the init function. This allows for different default values depending on the manner of initialization. The other method is to, when declaring a variable, assign it. This makes the default variable the same no matter the method of initialization; however if it is a variable and not a constant, init functions can change this variable. There is no difference between the two choices other than style.

If all of your fields have assignments or are optional, you don't need an initializer.

Failable Initializers

This won't make much sense if you have not read optionals–in this case, skip this section, it's quite a rare case. Sometimes, the arguments that are passed to your initializer may not be valid. In this case, you may not be able to initialize an object. You can make a choice to either crash/raise an error or return nil–you can think of this as an optional initializer. In this case, you do everything the same as you would normally, except for the following:

  1. Tag your initializer with a postfix ?: init?

  2. Return nil when the initializer fails

Convenience Initializers

You may find yourself doing a lot of the same initialization work over and over in different initializers. In fact, you may be sometimes initializing from different representations of the same data. In such a case, you may convert from one representation to another and then do the same initialization work. This is a lot of copy and paste–this is where a convenience initializer comes in handy. A convenience initializer allows you to handle this representation conversion and then call an initializer you have already written! You just need to do self.init(...) to call the other initializer.

class Person {
    
    // MARK: Fields
    var name: String
    var age: Int
    
    // MARK: Initializers
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    init(name: String, yearOfBirth: Int = 2002) {
        self.name = name
        age = 2022 - yearOfBirth // with rounding errors
        // all fields setup
        describe() // referencing self
    }
    
    init?(name: String?, age: Int?) {
        if let name = name, let age = age {
            self.name = name
            self.age = age
        } else {
            return nil
        }
    }
    
    convenience init(names: [String], age: Int) {
        self.init(name:
                    {
                        var name: String = ""
                        for i in names {
                            if name == "" {
                                name = i
                            } else {
                                name += " " + i
                            }
                        }
                        return name
                    }(), age: age)
    }
    // a more concise way would be to use `reduce`, but that's too advanced for this part of the course
    // for those that are interested, look at advanced array operations
    
    // MARK: Methods
    func describe() {
        print(name, age)
    }
    
}

Subclasses

To create a subclass of a class, all you need to do is add : superClass in the header. To implement the subclass you must be careful in your order of initialization. First, setup the new fields that are declared in your subclass. Afterward, call your designated super.init.

You may be familiar with the idea of overriding from other languages. This idea lets you modify functions, variable behavior, and initializers that are defined in the super-class. When looking for a method/field in an object, the lowest instance takes precedence. I.e. sub-class trumps super-class. To declare an override we use the override keyword. This is true for fields, methods, and initializers. If you override something, Xcode will handle adding the override keyword for you–or will let you know if you stopped this process.

class Noah: Person {
    
    // MARK: Fields
    var lastName: String // There are a lot of us, so we need to distinguish
    
    // MARK: Initializers
    init(lastName: String, age: Int) {
        self.lastName = lastName
        super.init(name: "Noah", age: age)
        describe()
    }
    
    // MARK: Methods
    override func describe() {
        print(name, lastName, age)
    }
    
}

Structs

Structs are very similar to classes, so much so that we have an entire section dedicated to their differences below. If not stated to be different below, assume it has the same behavior as classes. To declare a struct instead of a class we use the keyword struct:

struct <Insert Name>: <Insert Protocols to Confrom to> {}

Generally speaking, you can think of structs as a more rigid class. Remember how adding rigidity to variable types added for security and safety–this can be thought of similar. This rigidty comes in the form of memory management and a different form of inheritance (there are no substructs)! There is an alternate form of inheritance, but that is more of a topic for protocols. For times where you want your data to have a very specific memory setup (how the fields are setup), size, and explicit times with mutability, use structs.

Initializers

We don't actually ever need to define initializers for structs. Structs are aware of their fields, and if no initializers are specified, will construct an initializer for you that takes all of your fields as arguments.

struct Person {
    
    var name: String
    var age: Int
    
    func describe() {
        print(name, age)
    }
    
}

The above struct has an implicit initializer:

init(name: String, age: Int) {
    self.name = name
    self.age = age
}

You may find it useful to add extra initializers. You can do that just as you would with a class. If you add an initializer, you will lose the implicit initializer. You can define it yourself, so it doesn't matter. Just be careful when adding initializers that you handle the implicit initializer.

Mutating

As I mentioned earlier, mutability within structs is more rigid. Any function that changes a field must be declared mutating.

mutating func newName(name: String) {
    self.name = name
}

This allows for data type implementations to circumvent unintended side effects that may be present in classes.

Objects

Instantiation

Instantiation is pretty dang easy. You're just calling a function with the data type being the name: <Data Type>(arguments).

let noah = Person(name: "Noah", age: 19)

Accessing Fields and Methods

To access a field or method do <instance>.<field or method>

noah.name
// "Noah": String
noah.describe()
// Noah 19

Extensions

We can factor out code to different areas of our file or even different files! We do this through something called an extension. As the name implies, we can build on types that have been given to us! Types like String, Int, and Double are all types that you can add functionality to!

All that we need to do is enclose our new features in curly brackets after the keyword extension and the type:

extension Person {
    
    var anotherDescription: String { name + ", " + String(age) }
    
    func getAge() -> Int {
        return age
    }
    
}

extension Person {

    func anotherDescribe() {
        print(anotherDescription)
    }
    
}

Note: You cannot create new variables unless they are computed properties or static.

You can even add conformances to types using extensions:

extension Person: Equatable {

    static func == (_ lhs: Person, _ rhs: Person) -> Bool {
        return lhs.name == rhs.name && lhs.age == rhs.agesw
    }
}

Note: Equatable doesn't actually require you to implement ==, but you can if you want to! If you leave it alone, it will compare fields.

Differences

Okay, so these seem pretty similar. Yes, structs are slightly more rigid, but that seems like a small change and not a big reason as to have two different ways to construct data types, so what's the difference?

Reference Vs. Value Types

This is the real reason. Everything else is good to know, but not nearly important. Classes are something called reference types. This means that when you store an object in a variable you are storing a pointer to the object, not the object itself. This means you can store one object in multiple places, and if you change the object from one location, it will change the object from the other location as well because they are the same!

var noah = Person(name: "Noah", age: 19)
// id1: Person =
//    name: String = "Noah"
//    age: Int = 19

// noah: Person = id1
var noahsCopy = noah
// noahsCopy: Person = id1

noahsCopy.age = -1
noah.age
// -1: Int

Remember when I was talking about why mutating can be helpful in mitigating side effects, yeah that wasn't the only thing mitigating side effects. That being said, you may want this behavior! This is why classes exist.

As you may have guessed, structs do not have this problem/feature. structs are something called value-types. As the name implies, instead of storing references or ids, the data is directly stored. This means that if we assign a struct to a variable, it will copy its contents into a new struct. Aka. no side effects!

var noah = Person(name: "Noah", age: 19)
// noah: Person =
//    name: String = "Noah"
//    age: Int = 19

var noahsCopy = noah
// noahsCopy: Person =
//    name: String = "Noah"
//    age: Int = 19


noahsCopy.age = -1
noah.age
// 19: Int

This property makes structs good for storing and passing data. If you're thinking MVC, your models will often be written as structs because you do not want changes in your data for the sake of presentation to affect your main store of data unintentionally.

A data type that you will use frequently that is implemented with structs are Dictionarys aka hash maps.

Subclasses

There's one other big difference between the classes and structs. structs do not allow for subclassing. If you want shared behavior either use classes or protocols.

Rigidity

I have already talked about rigidity, so I will keep this to new information, as this is an advanced part of this discussion that is not very important to note. For those of you that read the section on indirect enums, you are familiar with the idea of compiler-time memory. structs are no different. The compiler must be able to figure out the size of the struct at compile time. This means you cannot do recursion in structs. I.e. let's say I have a type called MyTypeif myType has a field of type MyType, or MyType?, it must be a class and not a struct.

Last updated