[Swift] Type design guidelines

As one of the features of Swift, ** Extensive expressiveness of structures, enums, and protocols **.

Also, depending on the optional type, let, and var keywords ** You can also finely control the characteristics of type properties. ** **

With the improvement of these expressive powers Wider choices when designing molds.

I will explain what to choose from these many options.

Classes and structs

In conclusion, ** I think you should basically use structs. ** **

In Swift, most of what you can do with classes can be done with structs. So every time you design a type, you have to consider whether it should be a class or a struct.

But in reality, most of the types in ** Swift's standard library are defined by structs. ** ** In other words, Swift recommends a design that actively utilizes structures.

** You should consider implementing it in a class only if it is difficult to design with a struct. ** **

Class-induced bugs

Basically, I said that the structure is ... but why is that so? Explain why the class is bad.

The sample code below is It is a code that was planned to set the temperature in Japan to 25 ° and the temperature in Egypt to 40 °.


class Tempereture {
    var celsius: Double = 0.0
}

class Country {
    var temperature: Tempereture
    init(temperature: Tempereture) {
        self.temperature = temperature
    }
}

let temperature = Tempereture()
temperature.celsius = 25
let Japan = Country(temperature: temperature)
temperature.celsius = 40
let Egypt = Country(temperature: temperature)
Japan.temperature.celsius   // 40
Egypt.temperature.celsius   // 40

Why are both 40 °? That's because the class is ** reference type **.

For reference types, when an instance is passed as an argument A reference to that instance is passed.

In other words, both Japan and Egypt refer to the same instance.

So if you change the value of the temperature property ** Affects both instances. ** **

However, such mistakes are rudimentary and often noticeable, What if the code gets a little more complicated?

The sample code below uses asynchronous processing. ** Asynchronous processing is processing in a thread different from normal processing. ** **

Normally, if you are doing heavy processing, you cannot proceed to the next processing until it is completed. Therefore, the user has to wait.

** In order to prevent such a situation, process in a thread different from normal processing, It does not interfere with the original processing. ** ** (I will post an article about asynchronous processing at a later date.)


import Dispatch

class Tempereture {
    var celsius: Double = 0.0
}

let temperature = Tempereture()
temperature.celsius = 25

//Edit temperature value in another thread
let dispatch = DispatchQueue.global(qos: .default)

dispatch.async {
    temperature.celsius += 1
}

temperature.celsius += 1
temperature.celsius   // 27

The part of dispatch.async {} is asynchronous processing. As a flow, until let dispatch = DispatchQueue.global (qos: .default) It is executed in one thread.

With dispatch.async, It is divided into thread A, which proceeds with the original processing, and thread B, which performs the processing in dispatch.async.

When the processing in dispatch.async {} is completed, it joins the original processing (thread A).

temperature.celsius + = 1 is done in two threads, The value of temperature.celsius will be 27.

Now, take a look at the following sample code.


import Dispatch
var count = 0

class Tempereture {
    var celsius: Double = 0.0
}

let temperature = Tempereture()
temperature.celsius = 25

//Edit temperature value in another thread
let dispatch = DispatchQueue.global(qos: .default)

dispatch.async {
    print("Performs dispatch processing.")
    for _ in 0...4 {
        count += 1
        print(count)
    }
    temperature.celsius += 1
}

temperature.celsius += 1
print(temperature.celsius)

Execution result
Performs dispatch processing.
26.0
1
2
3
4
5

This code is a little different than before, with a for-in statement added. And the output result of temperature.celsius is 26.

This is because the process in dispatch.async {} This is because the value is output before reaching temperature.celsius + = 1.

As you can see from the execution result After "dispatch processing" is output temperature.celsius is output.

In other words, it was output while reading the for-in statement.

When such processing is performed in various places, For a class that is a reference type, you will not know when the value will change.

** It is difficult to infer the result just by looking at a part of the code. ** ** These class traits tend to be hotbeds for bugs.

Safety provided by value-type structures

Structures, on the other hand, are value types.

So when an instance is passed as an argument, The value itself is passed instead of that reference.

Let's change the sample code from the previous one from a class to a struct.


struct Tempereture {
    var celsius: Double = 0.0
}

struct Country {
    var temperature: Tempereture
    init(temperature: Tempereture) {
        self.temperature = temperature
    }
}

var temperature = Tempereture()
temperature.celsius = 25
let Japan = Country(temperature: temperature)
temperature.celsius = 40
let Egypt = Country(temperature: temperature)
Japan.temperature.celsius   // 25
Egypt.temperature.celsius   // 40

This time the result was as expected.

** Because an instance of Temperature type is copied every time it is passed ** Each Country type instance holds a separate Temperature type instance.

** From another point of view It means that the owner of a structure is guaranteed to be one at all times. ** **

Due to these characteristics of structures, unlike classes, code execution results can be easily predicted.

The safety of this structure is what That's why Swift recommends using structs.

When to use the class

So far, we have explained that design using structures is emphasized. However, this does not mean that the ** class is unnecessary. ** **

You need to use the class in the following cases. ** · Need to share references -Execute processing according to the life cycle of the instance **

Share references

By sharing a reference ** Classes are suitable for sharing operations in one place with others. ** **

In the following sample code Outputs the number of times the action () method has been executed.

As a result, the structure that does not share the reference has an error in the number of executions and count. The class sharing the reference output the same count as the number of executions.


protocol Target {
    var type: String { get set }
    var count: Int { get set }
    mutating func action()
}

extension Target {
    mutating func action() {
        count += 1
        print("type: \(type), count: \(count)")
    }
}

//Define structure(Compliant with Target)
struct CountStruct: Target {
    var type: String = "Struct Type"
    var count: Int = 0
}

//Define class(Compliant with Target)
class CountClass: Target {
    var type: String = "Class Type"
    var count: Int = 0
}

struct Timer {
    //Constraint(Types that comply with the Target protocol)
    var target: Target
    
    mutating func startTimer() {
        for _ in 0..<5 {
            target.action()
        }
    }
}

let countStruct = CountStruct()
var timer1 = Timer(target: countStruct)
timer1.startTimer()
print(countStruct.count)

print("----")

let countClass = CountClass()
var timer2 = Timer(target: countClass)
timer2.startTimer()
print(countClass.count)

Execution result
type: Struct Type, count: 1
type: Struct Type, count: 2
type: Struct Type, count: 3
type: Struct Type, count: 4
type: Struct Type, count: 5
0
----
type: Class Type, count: 1
type: Class Type, count: 2
type: Class Type, count: 3
type: Class Type, count: 4
type: Class Type, count: 5
5

** ~ Code explanation ~ **

protocolProtocol Target {・ ・ ・} Defines the Target protocol. Types that comply with this protocol must define type and count.

Extension Target {・ ・ ・} I have a protocol extension. Types that conform to the Target protocol use mutating func action () {・ ・ ・} You will be able to use it.

In the action () method The value of the count property is incremented by 1 to record the number of executions.

** Type definition ** ・ Struct CountStruct: Target {・ ・ ・}Structure CountClass: Target {・ ・ ・} Both are types that comply with the Target protocol. Implements the requested property.

Structor Timer {・ ・ ・} You have defined var target: Target in the type A type that conforms to the Target protocol is specified in the argument when instantiating.

Define mutating func startTimer () {・ ・ ・} and The action () method of the type contained in the target property is executed 5 times.

The action () method is a method implemented by the protocol extension. A method that can use types that conform to the Target protocol.

** Subsequent processing ** ・ Let countStruct = CountStruct () The constant countStruct is assigned an instantiation of the CountStruct type. It's persistent, but the CountStruct type is Target protocol compliant.

Var timer1 = Timer (target: countStruct) The variable Timer1 is assigned an instantiated Timer type. Since you have to pass a type that conforms to the Target protocol as an argument, You are passing the constant countStruct.

Timer1.startTimer () The startTimer () method defined in the Timer type is being executed. The action () method is executed 5 times with the for-in statement described in the method.

Print (countClass.count) The value of the count property is incremented each time the action () method is executed. So, go to the count property and see what the value is. (Assumed 5 times)

The process is like this. The result is that the ** structure count is 0 and the class count is 5. ** **

Explain why the value of the structure is 0. At the stage of let countStruct = CountStruct (), The value of countStruct.count is 0.

With var timer1 = Timer (target: countStruct) I'm passing countStruct as an argument ** This countStruct is a copy of the constant countStruct. ** **

In other words, if the constant countStruct is countStruct (A), The countStruct of Timer (target: countStruct) becomes countStruct (B).

The process of count + = 1 performed by the startTimer () method is The count of countStruct (B) is incremented.

Since the countStruct of print (countStruct.count) is countStruct (A), This means that the value of count will remain at 0.

For classes The argument of var timer2 = Timer (target: countClass) is Since it is not a copy of countClass but a ** reference **, the count of the same reference will be updated.

So the result is 5.

Execute processing according to the life cycle of the instance.

One of the features of the class that the structure does not have is the de-initializer.

Because the decolorizer runs immediately when an instance of the class is released, ** You can tie a release operation for resources related to the instance life cycle. ** **

In the following sample code I put a value in the variable data at the time of initialization, By assigning nil to the variable sample, the instance of Sample type is destroyed. The variable data is set to nil through the deinitializer when it is destroyed.


var data: String?

class Sample {
    init() {
        print("Create a data")
        data = "a data"
    }
    
    deinit {
        print("Clean up the data")
        data = nil
    }
}

var sample: Sample? = Sample()
print(data ?? "nil")

sample = nil
print(data ?? "nil")

Execution result
Create a data
a data
Clean up the data
nil

Whether to design with a structure or a class I think these introductions made it clear!

The basics are to use structs and classes only when needed.

There is also an article about structures, so See that for the types you want to know about structures.

Thank you for watching until the end.

Recommended Posts

[Swift] Type design guidelines
[Swift] Type type ~ Structure ~
[Swift] Type components ~ Type nesting ~
[Swift] Type component ~ Subscript ~
[Swift] Type component ~ Initializer ~
[Swift] Shared enumeration type
[Swift] Type type-Class sequel-
[Swift] Type component ~ Method ~
[Swift] Summary about Bool type
[Swift] Type type-Class first part-
[Swift] Type component ~ Property Part 2 ~
Great Swift pointer type commentary
[Swift] Converts a UInt64 type integer to [UInt8]