1人の母と2人の息子の物語――Swiftの「値型」と「参照型」を徹底解説
Swiftという「母」と、2人の息子たち
Swiftには2人の息子がいます。
- 値型(Value Type)
- 参照型(Reference Type)
しかし、この2人はどんな特徴を持っているのでしょうか?互いに同じ振る舞いをするのか、それとも正反対なのか?
そもそもSwiftとは?
SwiftはAppleが開発したマルチパラダイムプログラミング言語で、iOS、macOS、watchOS、tvOS、Linux、z/OSなど幅広いプラットフォームで動作します。
他のオブジェクト指向言語と同様に、Swiftにもクラスという構成要素があり、メソッドやプロパティ、イニシャライザを定義できるほか、プロトコルへの準拠、継承、ポリモーフィズムもサポートしています。
ところが、ちょっと待ってください……。
実はSwiftには構造体(struct)もあります。構造体もメソッド、プロパティ、イニシャライザを定義でき、プロトコルに準拠することも可能です。唯一できないのは継承だけです。
えっ?混乱してきました……。
さらに混乱に拍車をかけると、値型は構造体だけではありません。タプルや列挙型(enum)も値型です。同様に、参照型もクラスだけではなく、関数やクロージャも参照型に該当します。ただし安心してください。それぞれの型には主な用途・得意分野がきちんとあります。
こうして整理すると、残る大きな疑問は「構造体とクラスをどう使い分けるべきか」だけです。それでは、この混乱をひとつずつ解消していきましょう。
ストレージ(メモリ)上の配置場所
利用可能なストレージには3種類あります。
- レジスタ
- スタック
- ヒープ
寿命の短いオブジェクトはレジスタやスタックに格納され、寿命の長いオブジェクトはヒープに格納されます。
値型はスタックに確保されたメモリへ内容を格納するため、「Swiftでは値型はスタックに割り当てられる」と説明されることが多いです。
しかし、ここにはよくある誤解が潜んでいます。心当たりはありませんか?
その誤解とは――「値型は常にスタックに格納される」と思い込んでいることです。
❌❌ 待ってください。必ずしもそうとは限りません。❌❌
値型は、一時的な変数やローカル変数として扱われる場合にはスタックに格納されます。しかし、値型が参照型の中に含まれていたらどうなるでしょうか?
この場合、ヒープメモリに格納されることがあります。
つまり値型は、寿命に応じてレジスタ・スタック・ヒープのいずれかに配置されます。ローカル変数であればスタック上に存在し、クラスの一部であればヒープメモリ上に存在することもあるのです。✅
一方、参照型はヒープに確保されたメモリへ内容を格納し、変数自体は実際のデータが保存されているメモリ位置への参照のみを保持します。
参照型では具体的にどのように動作するのでしょうか?
参照型の場合、複数の変数が同じメモリ位置への参照を共有するのはごく一般的な状況です。
値型のインスタンスを変数に代入したり関数へ渡したりするときは、インスタンス全体がコピーされて割り当てられます。しかし参照型の場合、コピーされるのは参照だけであり、新しい変数は同じメモリ位置を指す同じ参照を持ちます。
可変性(Mutability)の違い
変数には2つの状態があります。
- 可変(Mutable)
- 不変(Immutable)
値型のインスタンスが不変の変数(let)に代入されると、そのインスタンスも不変になります。その結果、インスタンスに対して一切の変更を加えることができません。
値型のインスタンスが可変の変数(var)に代入された場合に限り、インスタンスは可変になります。
しかし、参照型では事情がまったく異なります。変数と、そこに代入されたインスタンスは完全に別物だからです。クラスへの参照を保持する不変の変数を宣言した場合、それは「保持している参照が決して変わらない」ことを意味します。参照先を差し替えることはできず、常に同じインスタンスを指し続けます。ただし、そのインスタンス内部のプロパティは変更できる点に注意が必要です。
構造型(Structural Types)と同一性
構造型の値は、属性や要素に基づいて等価性が比較されます。対応するすべての属性が等しい場合に限り、ある値型が別の値型と等しいと言えます。
うーん……言葉が難しいですね。具体的に見てみましょう。
例えば、firstNameとlastNameという属性を持つPersonという値型があるとします。
struct Person {
var firstName: String
var lastName: String
}
var person1 = Person(firstName: "foo", lastName: "bar")
var person2 = Person(firstName: "foo", lastName: "bar")
ここでperson1とperson2は、どちらもfirstName("foo")とlastName("bar")に同じ値を保持しています。属性が同じ値を持つため、この2つのインスタンスは互いに等しいと言えます。
しかもそれだけではありません。今後登場するどんなPersonインスタンスでも、firstNameとlastNameが同じ値であれば、互いに等しくなります。
ここまでの理解をまとめると、次のように言えます。
値型には同一性(identity)がないため、それらへの参照は存在し得ません。値型は「顔を持たない」のです。
えっ?どういうこと???
var myAge: Int = 21
var friendAge: Int = 21
myAgeとfriendAgeは、どちらも値21を持つ整数型の変数です。
この2つを区別できるでしょうか?
いいえ、できません。同じ値を保持しているからです。
値21を持つ整数変数は、同じく値21を持つ別の整数変数と区別がつきません。それだけのシンプルな話です。
同一性を持たないことは、値型にもう1つの利点をもたらします。現実に置き換えて考えてみてください。もし自分に同一性がなければ、同じ特性を持つ誰かがあなたを置き換えたり、代わりを務めたりできてしまいます。
人間の世界で同じことが起きたら大変です。もし私に同一性がなければ、同じ特徴を持つ誰かに取って代わられるかもしれません。私たちに同一性があるのは幸いです。そうでなければ存在そのものが危険にさらされるでしょう。
しかし値型にとっては、同一性を持たないことこそが利点になるのです。
値型を使うメリットとは?
🔒 レースコンディションやデッドロックが発生しない
マルチスレッド環境における値型では、あるスレッドがインスタンスを使用している間に、別のスレッドがその状態を変更することはできません。つまり、レースコンディションやデッドロックが発生しません。
⚔️ 保持サイクル(Retain Cycles)が発生しない
2つの参照型インスタンスが互いに強い参照を保持し合い、お互いのメモリ解放を妨げている状態は「保持サイクル(retain cycle)」と呼ばれます。値型は参照として機能しないため、値型に保持サイクルは発生しません。
👨👩👧👦 自動参照カウント(ARC)が不要
参照型の場合、Swiftは自動参照カウント(Automatic Reference Counting)によって、生存中のすべてのオブジェクトを追跡し、強い参照がなくなった時点で初めてインスタンスを解放します。少し考えてみれば分かる通り、Swiftランタイムは常にオブジェクトを監視し続ける必要があるため、これはかなり重い処理です。一方、値型はスタックに割り当てられるためARCが不要。つまり、より低コストで高速に動作します。
でも待ってください……Array、Dictionary、Stringのメモリはどう管理されているのでしょうか?
配列・辞書・文字列の実際のサイズはコンパイル時には確定しないため、コンパイル時に割り当てる余地がありません。内部的には値型ですが、スタックには配置できず、ヒープメモリに割り当てる必要があります。これを効率的に管理するために、SwiftはCopy-on-Write(COW)という仕組みを採用しています。
では、Copy-on-Writeとは何なのでしょうか?
「あるインスタンスが別のインスタンスのコピーである」というのは、本来なら両者が同じ値を含む完全な複製であることを意味します。しかしSwiftでは、Array・Dictionary・Stringなどの型において、実際のコピーはインスタンスが変更(ミューテーション)された瞬間に初めてヒープ上で作成されます。変更されるまでは複数の変数が同じ実体を安全に共有できるのです。これは値型のための優れたパフォーマンス最適化手法と言えます。
まとめ
値型と参照型を「いつ使うべきか」を定める絶対的なルールは存在しません。値型には参照型にない独自の利点があり、その逆もまた然りです。どちらもそれぞれのやり方で個性的です。重要なのは、あなたの要件と達成したいことに合わせて判断すること。コードのセマンティクスを最もよく知っているのは、そのコードを書いたあなただけです。選択の自由はすべてあなたの手にあります。
したがって、「値型 vs 参照型」で争うのではなく、それぞれの特性を理解した上で賢く使い分けましょう。
最後までお読みいただき、ありがとうございました!🎉
-
C#の値型と参照型の違いを徹底解説!メモリの仕組みとサンプルコードで比較
C#におけるすべての型は、大きく分けて「値型」と「参照型」という2つのカテゴリに分類されます。この2つの違いを正しく理解することは、バグの防止やパフォーマンスの最適化において非常に重要です。本記事では、それぞれの型の特徴、動作の仕組み、そして実際のコード例を使った挙動の違いについて詳しく解説します。 値型とは 値型の変数は、データそのものを直接保持します。各変数が独自のデータコピーを持つため、ある変数の値を変更しても、別の変数やオブジェクトには一切影響しません。 C#における値型には、以下のようなものがあります。 int、float、double などのすべての数値型 char 型および b
-
SwiftのGenericsとAny型の違いを徹底解説!型安全なコードを書くために知っておきたいポイント
Swiftは、現代において最も型安全なプログラミング言語の一つとして広く認知されています。そもそも「型安全」とは何か?型安全(タイプセーフ)な言語とは、常に操作がその時点で利用可能な正しい種類のデータに対して行われることを保証する言語のことです。具体的には、Int、Float、String、Array、Dictionaryといった異なるデータ型を宣言できる能力を持ち、かつ特定のデータ型で宣言された変数が、決して別の種類のデータを保持しないことを保証できる言語を指します。型安全な言語では必ず型チェックが行われます。このチェックは言語によって異なり、コンパイル時に行われる場合もあれば、ランタイム時