Posted at

【Swift】構造体とクラスの使い分け


構造体とクラス

Swiftで用いられるクラスと構造体は関数や変数など、プログラムを組む上で非常に便利な機能です。

どちらも表現力が豊かで、クラスで実現可能な事の大半は構造体でも再現可能だったりします。

それではどのような状況の時にクラスを用い、どんな時に構造体を使う選択をしていけば良いのでしょうか???

今回はそのクラスと構造体の使い方についてまとめていけたらと思います。


構造体の利点

まず以下のclassで実装したコードをみてください。

注目する点はPersonクラスをオブジェクト化したTomとMargaretのfavaritefoodのfood変数を呼び出しているところです。

本来であればTomがCake、MargaretがOrangeと別々の値を出力して欲しいのですが、Tomを生成してから変数の値を変えたにも関わらず、結果として同じ値が出力されています。

このようにクラスは参照型であるため、favaritefood変数を参照し、オブジェクトごとに値は保持せずにどのオブジェクトも共通の値を参照しにいきます。そのため、以下のようなプログラムにはクラスは向いていません。

class FavariteFood {

var food: String = nil
}

class Person {
var favaritefood: FavariteFood

init(favaritefood: FavariteFood) {
self.favaritefood = favaritefood
}
}

let favaritefood = FavariteFood()
favaritefood.food = "Cake"
let Tom = Person(favaritefood: favaritefood)
favaritefood.food = "Orange"
let Margaret = Person(favaritefood: favaritefood)
Tom.favaritefood.food //Orange
Margaret.favaritefood.food //Orange

続いて今度は構造体で先ほどのプログラムを組んでみます。

すると先ほどとは違い、Personのオブジェクトごとに値が異なっています。

これは構造体が参照型ではなく、値型であるためそれぞれのPerson型のインスタンスが別々のFavariteFood型を保持しているためです。

struct FavariteFood {

var food: String = nil
}

struct Person {
var favaritefood: FavariteFood
}

let favaritefood = FavariteFood()
favaritefood.food = "Cake"
let Tom = Person(favaritefood: favaritefood)
favaritefood.food = "Orange"
let Margaret = Person(favaritefood: favaritefood)
Tom.favaritefood.food //Cake
Margaret.favaritefood.food //Orange


コピーオンライト

構造体にはコピーオンライトという機能がついています。

この機能は先ほどのように構造体を複数のオブジェクトとして生成した場合、そのオブジェクトが保持する値のコピーを必要になるまで行わないようにしてくれます。

以上の説明では分かりにくいと思うので噛み砕いて説明していきます。

下記のコードを見ると、二つ目に定義されている配列list2にlist1が代入されています。ここで勘違いされがちなのが、この時点で配列の内容がコピーされているという事です。実はこの時点では配列の値はコピーされておらず、その次の3行目でlistに新しい値が追加されて、値の内容に違いが生じた時に初めてコピーされます。

なぜこのような機能があるのかというと、Array型やDictionary型などのコレクションを表す型を使用する場合サイズの大きなデータを扱う可能性があるため、代入するたびにコピーを行なっていてはパフォーマンスの低下に繋がります。そのため、そのコピーの作業を値が異なった際に行う事で、その時点時点で必要のないコピー作業を省きパフォーマンスを向上させているのです。

var list = ["dog", "cat"]

var list2 = list
list.append("elephant")
list //["dog", "cat", "elephatn"]
list2 //["dog", "cat"]


クラスの利点


参照の共有

クラスの利点の一つとして値を参照して共有することが挙げられます。

以下のコードを見ると、クラスと構造体のオブジェクトごとにcountの値が異なっています。

これは参照型か値型かの違いによるものです。まず構造体型は変数などの値を参照せず、各オブジェクトごとに別々の値を保持しています。

そのため実行結果でcountを刻み出力していても、その値がもともとcount0を保持しているfirstExampleに影響を与え値が変更されるということはありません。

次にクラスをみていくと構造体とは異なり、secondExampleの値にも影響を与え値が変更されています。これはクラスが参照型であるため、同じ名称の変数の値を参照したためになります。

このように構造体とクラスには違いがあり、今回のケースであると数えたcountを保持して使用したい場合はクラスの方を使用するべきだと言えます。

protocol Example {

var text: String {get set}
var count: Int {get set}
mutating func action()
}

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

struct FirstExample : Example {
var text = "Hello World"
var count = 0

init() {}
}

class SecondExample : Example {
var text = "Hello ParallelWorld"
var count = 0
}

Struct Counter {
var example: Example

mutating func start(){
for _ in 0..<5 {
example.action()
}
}
}

let firstExample : Example = FirstExample()
var counter1 = Counter(example: firstExample)
counter.start()
firstExample.count // 0

let secondExample : Example = SecondExample()
var counter2 = Counter(example: secondExample)
counter2.start()
secondExample.count // 5

//実行結果
text: HellWorld, count: 1
text: HellWorld, count: 2
text: HellWorld, count: 3
text: HellWorld, count: 4
text: HellWorld, count: 5
text: HellParallelWorld, count: 1
text: HellParallelWorld, count: 2
text: HellParallelWorld, count: 3
text: HellParallelWorld, count: 4
text: HellParallelWorld, count: 5


インスタンスのライフサイクルでの処理

以下のコードを見るとインスタンス化後のdataの値は"a data"だが、インスタンスの値をnilにした後にはdataがnilになっています。

これは構造体にないクラスの特徴のデイニシャライザが影響しています。デイニシャライザはdenitにより使用でき、インスタンスが破棄された際に実行され、その時に一時ファイルも削除されます。そのため以下のコードのように初期化の際にはinitが実行され、破棄された際にはdeinitが実行されこのような結果になりました。

そのためインスタンスのライフサイクルに合わせて処理を行いたい場合は構造体よりもクラスを使用するべきだと言えます。

var data: String?

class SomeClass {
init() {
print("Build a data")
data = "a data"
}
deinit {
print("Reset adata")
data = nil
}
}

var someClass: SomeClass? = SomeClass()
data //a data

someClass = nil
data //nil

//実行結果
Build a data
Reset a data