Classes and Structs
In Swift, there are four methods to create custom data types. We have already gone over two: enum
s and tuple
s! There are two left that are pretty similar: class
es and struct
s.
Classes
Classes are just how you think of them in any other imperative language. Here's the syntax for a class:
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:
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:
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:
Tag your initializer with a postfix
?
:init?
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.
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.
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
:
Generally speaking, you can think of struct
s 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.
The above struct has an implicit initializer:
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
.
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)
.
Accessing Fields and Methods
To access a field or method do <instance>.<field or method>
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:
Note: You cannot create new variables unless they are computed properties or static.
You can even add conformances to types using extensions:
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!
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, struct
s do not have this problem/feature. struct
s 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!
This property makes struct
s 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 struct
s are Dictionary
s aka hash maps.
Subclasses
There's one other big difference between the class
es and struct
s. struct
s do not allow for subclassing. If you want shared behavior either use class
es or protocol
s.
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 enum
s, you are familiar with the idea of compiler-time memory. struct
s 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 struct
s. I.e. let's say I have a type called MyType
if myType has a field of type MyType
, or MyType?
, it must be a class
and not a struct
.
Last updated