Swiftの構造体は値型(Value Types)、クラスは参照型(Reference Types)

クラスと構造体(Classes and Structures)

ここでは、構造体とクラスの決定的な違いである、値型(Value Types)と参照型(Reference Types)に関して詳しく説明していきます。何度も述べていますが、構造体は値型、クラスは参照型です。ここではサンプルコードと図解を用いて、値型と参照型の違いに関して詳しく説明していきます。また、identical operatorに関しても簡単に紹介します。

構造体と列挙型は値型(Value Types)

値型(Value Types)とは何か?

最初の方で、クラスと構造体の最大の違いは

クラスは参照型、構造体は値型

と述べました。値型(value types)というのは変数や定数(関数も)に値が渡された際に、その値がコピーされる型のことを指します。「実はSwiftの基本型は構造体である」とも書きました。これまで学んだ基本的な型(IntArrayなどなど)は全て構造体であり、それらは値型です。つまり、これらの型として作ったインスタンス(さらに、そのインスタンスが持ってるプロパティ)は、そいつらが別の変数などに渡された際には常にコピーされています。

列挙型(Enumerations)も値型ですが、列挙型に関しては別のページで触れたいと思います。

値型が良く分かる実例

実際にサンプルプログラムで見ると、「なるほど」と納得できると思います。先ほどのバッテリー構造体を使います。

// 構造体は値型
let hitachiBattery = Battery(power:18, useTime:40)
print("hitachiBattery | バッテリーの電圧は\(hitachiBattery.power)Vで 、連続稼働時間は\(hitachiBattery.useTime)分です")
//"hitachiBattery | バッテリーの電圧は18.0Vで 、連続稼働時間は40分です"と表示

var anotherHitachiBattery = hitachiBattery
print("anotherHitachiBattery | バッテリーの電圧は\(hitachiBattery.power)Vで 、連続稼働時間は\(hitachiBattery.useTime)分です")
//"anotherHitachiBattery | バッテリーの電圧は18.0Vで 、連続稼働時間は40分です"と表示

// 電圧を変更
anotherHitachiBattery.power = 14.4
print("anotherHitachiBatteryの電圧が\(anotherHitachiBattery.power)Vに変更されました。hitachiBatteryの電圧は\(hitachiBattery.power)Vのままです。")
//"anotherHitachiBatteryの電圧が14.4Vに変更されました。hitachiBatteryの電圧は18.0Vのままです。"と表示

日立工機のバッテリー18Vを想定して、hitachiBatteryインスタンスを生成しました。次に、別の変数anotherHitachiBatteryを用意して最初に作ったインスタンスhitachiBatteryで初期化します。print()関数による出力を見ると、(当然ですが)この2つのバッテリーは全く同一の電圧・連続稼働時間を示しています。

ここでanotherHitachiBatteryの電圧を14.4Vに変更します。出力を見ると確かに電圧は14.4Vになってますよね。ところが、hitachiBatteryの電圧は18Vのままです。

[図解]構造体は値型

構造体は値型ですから、最初にanotherHitachiBatteryを作った時、すでにhitachiBatteryとは独立した新しいインスタンスが生成されています。したがって、anotherHitachiBatteryが保持しているプロパティも、当然独立した新しいプロパティになります。2つの異なるインスタンスですから、片方のプロパティを変更しても(上記の例ではanotherHitachiBatteryの電圧)、もう片方hitachiBatteryの電圧には影響を与えません。

クラスは参照型(Reference Types)

参照型(Reference Types)とは何か?

構造体と違って、クラスは参照型(reference types)です。参照型であるクラスの場合、変数や定数または関数に値が渡された際に、インスタンスがコピーされません。言い換えると、同じクラス型をもつ定数などが「コピー」された場合は、それらは同一のインスタンスを参照することになります。これは後で詳しく説明します。

参照型がどういうものか分かる実例

これも実際の例で見た方が分かりやすいです。構造体の場合と同様に、先ほど作った掃除機クラスCleanerを使って説明します。

// クラスは参照型
let hitachiCleaner = Cleaner()
hitachiCleaner.battery = hitachiBattery
hitachiCleaner.name    = "R18DSAL"
hitachiCleaner.height  = 438
hitachiCleaner.width   = 114
hitachiCleaner.weight  = 1.4
print("hitachiCleanerの重さは\(hitachiCleaner.weight)kgです")
//"hitachiCleanerの重さは1.4kgです"と表示

// 別のインスタンスを初期化、その重さを1.0kgに変更
let anotherR18DSAL    = hitachiCleaner
anotherR18DSAL.weight = 1.0

// 元のインスタンスの重さを表示
print("hitachiCleanerの重さは\(hitachiCleaner.weight)kgです")
//"hitachiCleanerの重さは1.0kgです"と表示

先ず、定数であるhitachiCleanerを掃除機クラスCleanerで初期化します。この時hitachiCleanerは、Cleanerクラスのインスタンスを「参照」しています。

次に、クラスのプロパティをセットしていきます。構造体であるbatteryには、先ほど作ったhitachiBatteryを流用します。残りのプロパティには値を代入して掃除機のスペックを設定します。掃除機の重さに注目すると、最初の値は1.4kgです。

ここで、新たな定数anotherR18DSALを先ほどのhitachiCleanerで初期化し、さらに重さを1.0kgに変更しました。

// 別のインスタンスを初期化、その重さを1.0kgに変更
let anotherR18DSAL    = hitachiCleaner
anotherR18DSAL.weight = 1.0

もし構造体であれば、hitachiCleaneranotherR18DSALは2つの独立したインスタンスです。したがって、anotherR18DSALの重さを変更しても、hitachiCleanerの重さには影響がありません。

ところが、hitachiCleanerの重さを出力してみると、1.0kgになっていることが分かります。

[図解]クラスは参照型

なんとなく参照の意味がつかめたでしょうか?上記の例ですと、hitachiCleaneranotherR18DSALは、両方同じCleanerクラスのインスタンスを参照しています。これは、同じ掃除機を別名で呼んでいるようなもので、例えば同じ掃除機を「日立クリーナー」や「R18DSAL」と呼ぶようなイメージです。

定数なのにプロパティを変更できるのか?

何気なく定数を使って初期化していましたが、「あれ?」と思ったかもしれません。上記の例では、hitachiCleaneranotherR18DSALは定数ですが、そのプロパティをまるで変数のように変更しています。

これはhitachiCleaneranotherR18DSAL自体の値が変わっていないからです。というのは、これらの定数がCleanerクラスのインスタンスを保持しているのではなくて、Cleanerインスタンスを参照しているからです。実際に変わっているのは、裏に潜んでいる(参照されている)Cleanerのインスタンスで、hitachiCleanerなどの定数リファレンスではありません。

結局、定数リファレンスでは何が固定されているのか?というと、参照しているインスタンスが固定されています。Xcode上で色々試してもらえると分かると思いますが、仮に新しいCleanerインスタンスを生成したとしても、hitachiCleanerなどは定数なので、代入することは出来ません。

//定数リファレンスは定数。新しいインスタンスを代入することはできない
let newCleaner = Cleaner()
hitachiCleaner = newCleaner
// コンパイルエラー、error: cannot assign to value: 'hitachiCleaner' is a 'let' constant

Identity Operators | クラスインスタンスの参照を調べる

参照型であるクラスは、上で示した例のように同じクラスインスタンスを参照している場合があります。そういうケースはどうやって確かめれば良いでしょうか?

Swiftでは、クラスインスタンスの参照を調べるのに2つの演算子が用意されています。=== (Identical to)!== (Not identical to)です。=の数に注意です。”Identical”は「全く同じの」とか「一致している」という意味です。

以下にidentical operatorの使用例を示します。

// Identity operator
if hitachiCleaner === anotherR18DSAL {
    print("hitachiCleanerとanotherR18DSALは同じCleanerインスタンスを参照しています ")
}

var makitaCleaner = Cleaner()
print("makitaCleanerの重さは\(makitaCleaner.weight)kgです")
if hitachiCleaner === makitaCleaner {
    print("hitachiCleanerとmakitaCleanerは同じCleanerインスタンスを参照しています ")
} else {
    print("hitachiCleanerとmakitaCleanerは別のCleanerインスタンスを参照しています ")
}

先ほど使ったhitachiCleaneranotherR18DSALは同一のインスタンスを指します。図でも示しましたが、これは自明です。次に、新しいCleanerインスタンスのmakitaCleanerを作りました。結果はどうでしょうか?出力は載せませんでしたが、予想どおりになるはずです。是非自作クラスを作って試して見て下さい。

Identical operatorと比較演算子(equal to)の違い

Identical operatorを使ってクラスインスタンスが同一であると判断された場合でも、それは比較演算子==の”equal to”であるとは限りません。2つの違いを簡単にまとめると、

  • “Identical to”は、例えば、あるクラス型の2つの定数が、全く同じクラスインスタンスを参照していることを意味
  • “Equal to”は、2つのインスタンスが「値として同じ(同一)」であるということを意味

となります。”Equal to”の「同じ」は、クラス(または構造体)が「どう同じ」(または「どう違う」)なのかを我々が実装しなければいけません。これはまた別の記事で書こうと思います。

まとめ

  • 構造体は「値型(value types)」、クラスは「参照型(reference types)」
  • 値型では、値渡しの時にインスタンスがコピー(複製)される。一方参照型では、値渡しをしても、変数・定数は同じインスタンスを指す
  • Identical operator(===!==)はクラス(構造体)インスタンスを調べる演算子。Equal to(==)とは違う