This is because it plays a significant role in choosing the right abstraction mechanism keeping both performance and modeling in mind. As you can only imagine this topic is simply the tip of the iceberg. Many advanced concepts and questions of classes and structs in swift. With that being, I have broken this topic into two parts.
Structs & Classes — What are they?
Consider them like templates or guidebook that contains variables and methods used to all object of the said kind. In short, they assist in keeping the code organized for future maintenance and reusability.
Class features over structs
- Inheritance
- inherit the characteristics of another class
- Type-casting
- checks and interprets the type of a class instance at runtime
- Deinitializers
- enable an instance of a class to free up any resources it has assigned
- Reference Count
- allows multiple references to a class instance
Value & reference types
- Value types
- each instance keeps an independent copy of its data
- example: structs, enums, or tuples
- changing one instance will have no effect on the other
- each instance keeps an independent copy of its data
- reference type
- Instances share a single copy of the data.
- changing data in one instance will change the data for all instance pointing to the same instance
- example: classes
Deciding to class or struct
when deciding on a new model we, as developers, should carefully contemplate the use cases of the model. With that stated, you should decide between structs and classes.
using classes:
class Network { var url:String init(url:String) { self.url = url } } var req = Network(url: "https://daurislittle.com") var newReq = req newReq.url = "https://facebook.com" if req === newReq { print(req.url) print(newReq.url) }
- comparing instance identity with “===” makes sense
- Creating a shared and mutable state
- if your intent is to share the state among the threads and variables use classes
using structs:
You will note the code below compare the value of the Network URLs and not the memory addresses.
struct Network { var url:String } extension Network:Equatable{} let req = Network(url: "https://daurislittle.com") let newReq = Network(url: "https://facebook.com") if req == newReq { print("Both the request contains the same address") }
- comparing instance data with “==” makes sense
- data needs to be compared and the memory locations of these data are not important
Now you can see that both the requests have different URLs as each request have a different copy of the URLs
struct Network { var url:String } extension Network:Equatable{} var req = Network(url: "https://daurislittle.com") var newReq = req newReq.url = "https://facebook.com" print(req.url) //https://daurislittle.com print(newReq.url) //https://facebook.com
Now you can see that both the requests have different URLs as each request have a different copy of the URLs
- Data can be used in code across multiple threads
- when passing and copying value types in a multi-threaded environment we can be sure that each context will have a separate unique copy which will not impact the others
- This helps avoid a lot of unintentional bugs
- when passing and copying value types in a multi-threaded environment we can be sure that each context will have a separate unique copy which will not impact the others
Memory Allocation w/structs and classes
structs are allocated in the stack memory. References of the class objects can be created on the stack but all the properties of the class’s object will be kept in the heap
Reference counting
As stated above classes support heap allocations they need to maintain reference counts for allocation and deallocating objects. On the other hand, structs do not need reference counting. However, if structs contain references then they would incur twice the number of reference counting overhead as compared to a class.
Method Dispatching
Classes use method dispatch but if a class is marked as “final“, the compiler will use static dispatch. Stucts uses “static” dispatch.
Memberwise initializers
Memberwise initializers are those initializers which the compiler generates automatically for structs. We can initialize a struct’s object using these initializers even though we have not provided any custom initializers.
enum RequestType { case get, post, delete } struct Network { val url:String var type:RequestType //Memberwise init generated by the compiler } var req = Network(url:"https://daurislittle.com", type: .get)
Note: order arguments in these initializers are decided by the order of the declared properties in struct.
However, as a developer we can write any custom initializer, the compiler now though will not generate the memberwise initializer. You can use both generated memberwise and custom initializers, just need to add the custom initializer within the extension of the struct.
enum RequestType { case get, post, delete } struct Network { val url:String var type:RequestType //Memberwise init generated by the compiler } extension Network { initi(URL:String) { self = Network.init(url: url, type: .get) } } var req = Network.init(url:"https://daurislittle.com")
Keyword “mutating” and when to use it
When changing the property of any struct, using the keyword “mutating” before the “func” keyword when defining a method. This is because the self parameter that’s implicitly passed into every method is immutable by default.
extension Network { mutating func updateUrl(url:String) { self.url = url } }
//self is passed as a var
The “mutating” keyword allows the compiler to decide which methods can’t be called on let constants. If we try to call a mutating method on a “let” constants. If we try to call the mutating method on a let variable, the compiler shows an error.