It's a Struct world

It's a Struct world

Β·

4 min read

Structures

A Struct is a Swift language construct that can be used to encapsulate state and behaviour. It's the recommended way of storing state and modeling behaviour. What's the alternative you ask? Classes. The two are similar in many ways but also have a some huge distinctions. Luckily there are some guidelines from Apple on when to use which.

Value vs Reference

value-ref.gif

Structs are value types.

A value type is a type whose value is copied when it’s assigned to a variable or constant, or when it’s passed to a function.

Passing a struct around results in a copy. This is optimised in the language in the case of large structs. Nevertheless, any changes you make to a struct instance result in a different instance. The alternative is a class -- a reference type. Classes are fundamentally shared instances. Upon creating an instance, you pass around its reference and any local modifications are visible to the caller code.

Syntax

struct Tweet { }

We'd then use let awesomeTweet = Tweet() to create an instance. Well, that's not useful enough and so structs can store state through properties: stored (variable or constant) or computed. In addition, we can model behaviour via methods on the struct.

struct Tweet {
    let text: String
    let date: Date
    var imageUrl: String?

    mutating func edit(_ newText: String) {
        // Sorry you can't edit a tweet! 😎
        // The text is declared as a **let** property
    }

}

Memberwise Initializers

One of the nice features a struct has over a class is a memberwise initializer. This is an initializer generated automatically by the compiler. For example, the above tweet definition can be initialized as follows: let tweet = Tweet(text: "πŸ‡°πŸ‡ͺ", date: .distantFuture, imageUrl: nil) without us declaring an explicit initializer.

One of my best Swift proposal was by Alejandro Alonso. This was a proposal for the compiler to synthesize a memberwise initializer taking into account the default values. This was implemented in Swift 5.1. So you can already enjoy this welcome addition.

Now, say I'm using the iOS Twitter app for real-time tweeting. The Tweet struct can declare a default date (.now -> private extension Date { static let now: Date = .init(timeIntervalSinceNow: 0) }) and all I have to provide is the text and perhaps the optional image url.

struct Tweet {
    let text: String
    let date: Date = .now
    var imageUrl: String? = nil

    ...
}

Voila! let tweet = Tweet(text: "πŸ‡°πŸ‡ͺ")

Adopting Protocols

One of the strong arguments in using structs is to share behaviour using protocol inheritance. Structs can adopt protocols. Equatable and Hashable protocols get great language support -- the compiler automatically synthesizes their requirements.

Consider two instances of tweets:

let tweet = Tweet(text: "πŸ‡°πŸ‡ͺ")
let anotherTweet = Tweet(text: "πŸ€·πŸΎβ€β™‚οΈ")

A comparison is of course not supported automatically and the compiler won't allow it. After all what is the comparison on? Memory address? One property?. Thus let areEqual = tweet == anotherTweet results in a compiler error. But say we need to enable this comparison behavior based on the properties. Well, we simply adopt the Equatable protocol and automatically get this behavior (implementation).

struct Tweet: Equatable {
    ...
}

Now, the previous comparison is permitted. Hashable protocol adoption can also get automatic protocol requirements synthesis. Nothing prevents us from implementing these requirements but some compiler 'magic' is always welcome.

Deep down

Structs are used extensively in the language. Numbers, arrays, strings, dictionaries etc are all implemented as Structs. Take arrays for instance.

The Swift's collection library declares an array as a generic struct:

@frozen public struct Array<Element> {
    @inlinable public mutating func append(_ newElement: Element)
    ...
}

You'll also note that all methods that mutate an array say:

  • append
  • insert
  • remove
  • replace
  • pop

are all marked as mutating -- a compiler requirement when modifying value types.

Nonetheless, as mentioned earlier, collections get some optimization to reduce the performance cost of copying by sharing the storage memory. Instance copying is still effectively done after a modification on the copy. You can read more on the swift language guide here.

Conclusion

Much more can be said about structs but the key takeaway is:

Always use structures by default

The idea of protocol inheritance and using structs has been heavily adopted by SwiftUI as demonstrated in its usage samples. So it's a worthwhile investment to learn and use. And there you have it: Structs. Happy coding!

References

Did you find this article valuable?

Support Charles Muchene by becoming a sponsor. Any amount is appreciated!