前書き
最近は今までKotlinしか触ってこなかったエンジニアも、iOSの開発に参画することが増えてSwiftを触り始めた人が多いではないかと感じます。そんな中で、KotlinとSwiftでは似たようで全然違う data class と struct の概念がそれぞれあります。この記事では、この2つの比較に着目し、その振る舞いの違いをご紹介していきたいと思います。Swift自身の struct と class の違いについては、以前このような発表をしていたので、ぜひその発表資料をご参考にできたら幸いです。
免責事項
筆者は宗教上の理由によりSwift以外のプログラミング言語を書かない人ですので、Kotlinのコードは基本生成AIにお願いして作られています。もし何か不備がありましたら教えてくれたら嬉しいです。
また、本記事は構成やサンプルも生成AIにお願いして色々参考にさせてもらっていますが、記事内容自体は全て筆者自身が執筆しております。
本文
Kotlinの開発で、データモデルを表現するために、data class を使う人が多いかと思います。さらにプロパティーの宣言を val にすれば、該当プロパティーへの直接な再代入は禁止され、もし再代入したい場合は必ず copy() を介してインスタンスを作り直して行う必要があります。これにより、暗黙でうっかりな状態変更の共有がなくなり、より安全で堅牢な状態管理が可能になります。
Swiftにももちろんデータモデルの表現に非常に適しているものがあります。そうです struct です。なので struct を data class のキーワード置き換えの気持ちで使う人もいるではないでしょうか?
残念ながら struct は data class のキーワード置き換え版ではないのです、両者は根本的に違うキャラクターなのです。その違いを理解するためには、「参照型」と「値型」と言う概念を理解する必要があります。
参照型と値型
これまでKotlinしか触ってこなかったら、参照型と値型の概念はあまり気にしたことがないかもしれません。なぜならKotlinはJVMで動くもの1で、極一部の限られたPrimitive型以外は全て参照型で、ユーザが作れるものも参照型だけだから、参照型と値型の概念すら知らなくても別に困ったりしません。と言うわけで一旦この概念を忘れて、とりあえずまずはこんなコードを見てみましょう:
class Profile {
var name: String
init(name: String) { self.name = name }
}
var user = Profile(name: "Yoshiko")
var player = user
player.name = "Yohane"
// user.name == ??
上記のコードでは、まず Profile 型で name が "Yoshiko" の user を作りました。そしてこの user をそのまま player に代入しました。まあプログラムではよくあるコードじゃないかと思いますね。さて次に、player の name を "Yohane" に変えました。この状態で、user.name の値はなんでしょうか。
上記のコードはSwiftで書いていますが、Kotlinの場合も基本同じ動きで、user.name も "Yohane" に変わるのは、Kotlinエンジニアならすぐわかるでしょう。しかしこれでは、不用意の状態変更の共有になりかねないです。ユーザはあくまで player の名前を変えたいだけなのに、user の名前まで一緒に変わってしまうので。と言うわけでこの問題を解決すべく、Kotlinは色々工夫しており、例えば簡単にインスタンスをコピーできる data class を導入したり、原則その data class のプロパティーは定数の val 宣言を推奨したりしています。上記の工夫により、もし player.name を変えたいなら、このように書く必要があります:
data class Profile(
val name: String
)
var user = Profile(name = "Yoshiko")
var player = user
-player.name = "Yohane"
+player = player.copy(name = "Yohane")
// user.name == "Yoshiko"
上記のコードでは、name は val で定義したので、player.name をそのまま変えられないから、必ず copy メソッドを介して変える必要がありました。この操作で何が発生したかというと、今までの player を一つコピーして、コピーの途中で name を "Yohane" に変えて、そしてこのコピーをもう一回 player に代入する、と言う一連の流れになります。もちろん実際のコードではこんな面倒な書き方をせずに、こんな書き方になるじゃないかと思います:
var user = Profile(name = "Yoshiko")
var player = user.copy(name = "Yohane")
// user.name == "Yoshiko"
さて今まではKotlinの data class を復習してきましたが、もし Profile がSwiftの struct だったらどうなるかというと、user.name は変わらず "Yoshiko" のままです。
struct Profile {
var name: String
}
var user = Profile(name: "Yoshiko")
var player = user
player.name = "Yohane"
// user.name == "Yoshiko"
なぜSwiftの struct はこのような動きになるかというと、これがまさに「参照型」と「値型」の違いの表れです。「参照型」は代入される時に、インスタンスの値ではなく、インスタンスへの参照です、つまり var player = user の処理では、player も user も全く同じインスタンスを持ちます。ところが値型はあくまでインスタンスの値のみ代入されるので、var player = user の処理では、あくまで user の値をコピーして、それを player に代入しているわけです。もしこの「値のコピー」のイメージが湧かないなら、この場合はKotlinの var player = user.copy() と同じもの、つまり player のインスタンスを複製して user に代入されたと考えて問題ないです2。
え?var でいいの???
ここで長年のKotlinエンジニアなら、一つびっくりすることがあるかもしれません:そう、上記のSwiftのコードでは、Profile の name は定数宣言の let ではなく、変数宣言の var を使っているのです。暗黙な共有と可変性はバグの源なので、上にも書いた通りKotlinは定数宣言の val を推奨してきました、それと data class の copy() の合わせ技により、状態の変更は必ず変更する変数のみにとどまり、他の変数に共有されることがなくなります。
ところが、先ほど書いた通り、Swiftの struct の場合はそもそも代入時に .copy() の呼び出しと実質同じ処理がコンパイラーにより強制的に行われます、なので代入した時点で user と player は内容が同じだけでそれぞれ違うものを持つことになるから、name を var で宣言しても特に状態変更の暗黙共有問題が発生しません。
じゃあ let は使わなくていいの?
もちろん、場合によっては、そのプロパティーを let で宣言した方が嬉しいこともあります。例えば name を let に直すと、上記のコードはこうなります:
struct Profile {
- var name: String
+ let name: String
}
var user = Profile(name: "Yoshiko")
var player = user
player.name = "Yohane" // ❌エラー:Cannot assign to property: 'name' is a 'let' constant
そうです、name を let に直すと、なんと player が var にもかかわらず、name の変更をコンパイラーが許してくれませんでした。これは Profile と言う型に関して、一度作られたら name の不変が保証されるからです。この場合仮にもしどうしても name を変えたいなら、Profile 丸ごと作り直さないといけないです:
-player.name = "Yohane"
+player = Profile(name: "Yohane")
これにより、例えば id のようなインスタンスのアイデンティティに関わるプロパティーは、変えたいとき必ずイニシャライザーを通さないとできなくなります。
じゃあ name を変えられないようにしたかったらどうすればいいの
もし一度 name が決まったら二度と変更できない仕様でしたら、Kotlinならこう書きますよね:
-var player = user.copy(name = "Yohane")
+val player = user.copy(name = "Yohane")
これは実はSwiftも同様なアプローチを取ることになります。ただ残念ながら data class みたいに copy() メソッドを自動で作ってくれないので、この場合はKotlinと比べたら少し面倒な書き方が必要です:
-var player = user
-player.name = "Yohane"
+var _player = user
+_player.name = "Yohane"
+let player = _player
これでは最後に player.name = "Yohane" を追加しようとすると、ビルドエラーになるから、name が一度決まったら二度と変更できない仕様になります。
え!?name が var 宣言なのに!??
そう、Kotlinエンジニアならここ驚くかもしれないですね、なぜならKotlinの場合はこのような実装は仕組み上可能です:
data class Profile(
var name: String
)
val user = Profile(name = "Yoshiko")
user.name = "Yohane"
上記のコードでは、user 自体は定数宣言ですが、そのプロパティーの name は変数宣言なので、user.name を後から変えることだってできますが、Swiftでは同じようなコードだとビルドエラーになります。なぜでしょう?
改めて「参照型」と「値型」の違いを振り返ってみましょう。参照型は代入時にそのインスタンスの参照を代入する型、そして値型は代入時にそのインスタンスの値、すなわち内容そのものを代入する型です。実はこの違いはここにも反映されており、「可変」「不変」の判断基準も、参照型ならその参照が変わらないなら「不変」であり、値型は内容そのものが変わらないでない限り「不変」になりません。つまり上記のKotlinのコードでは、name を確かに変えたのですが、user が保持している Profile のインスタンスは変わっていないから、「不変」に抵触することなくそのままビルドが通ります。
一方、Swiftの場合はどうでしょうか?
struct Profile {
var name: String
}
let user = Profile(name: "Yoshiko")
user.name = "Yohane" // ❌エラー:Cannot assign to property: 'name' is a 'let' constant
Swiftの場合、Profile は値型なので、不変は「内容そのものが変わらない」ことを保証することになります。なのでこの場合、直接 user.name を変更しているので、抽象的にはKotlinと同じく user のインスタンスへの参照を変えていないですが、user の内容が変わってしまったので、ビルドが通らないです。34
さらにこんなことも…
時には、変更処理に何かバリデーションなり追加の処理を入れたいこともあるから、それをメソッドにまとめたいこともありますよね。それでは例えばSwiftでこんな処理があったらどうでしょうか
func updateProfile(_ profile: Profile, withName name: String) {
// Validate `profile` and `name`
var p = profile
p.name = name
}
var user = Profile(name: "Yoshiko")
updateProfile(user, withName: "Yohane")
// user.name == ??
Kotlinの data class の場合、p に代入されるのは profile への参照なので、p.name の変更は profile.name の変更と同価です。ところが先ほど話した通り、Swiftの struct の場合、p に代入されるのは profile の内容のコピーです、なので p.name を変えても、profile.name に全く影響がありません。つまり updateProfile メソッドを呼び出しても、user.name は "Yoshiko" のままです。
ではSwiftでこのようなメソッドを作りたい場合はどうすればいいでしょうか?
よくあるやり方はアプローチが三つあります、一つは純粋関数のように変更後のものを返すパターンです:
func updatingProfile(_ profile: Profile, withName name: String) -> Profile {
// Validate `profile` and `name`
var p = profile
p.name = name
return p
}
var user = Profile(name: "Yoshiko")
user = updatingProfile(user, withName: "Yohane")
// user.name == "Yohane"
もちろんこの方法でもやることはできますが、ちょっと面倒ですよね。実はこうしなくても、inout を使うことによって、profile そのものを渡すこと5も可能です:
func updateProfile(_ profile: inout Profile, withName name: String) {
// Validate `profile` and `name`
profile.name = name
}
var user = Profile(name: "Yoshiko")
updateProfile(&user, withName: "Yohane")
// user.name == "Yohane"
この方法ではだいぶ楽に書けるし、面倒な user への再代入もないので、慣れたら楽ですが、問題はそのよくわからない inout や & の存在ですよね。なので実際の開発では、もしこの処理に他のコンポーネントを介さないなら、おそらく最後に紹介するこちらのアプローチが一番よく使われると思います:
extension Profile {
mutating func updateName(to name: String) {
// Validate `self` and `name`
self.name = name
}
}
var user = Profile(name: "Yoshiko")
user.updateName(to: "Yohane")
// user.name == "Yohane"
この方法も非常に楽で、唯一見慣れてないのはこの mutating キーワードかと思います。このキーワードは、「このメソッドを呼び出すとインスタンスの値が変わるから、var 宣言のインスタンスしか呼び出せないよ」と言うことです。つまり下記のような呼び出しならビルドエラーになります:
let user = Profile(name: "Yoshiko")
user.updateName(to: "Yohane") // ❌エラー:Cannot use mutating member on immutable value: 'user' is a 'let' constant
まとめ
まとめると、Kotlinの data class とSwiftの struct は、下記のように違います:
data class |
struct |
|
|---|---|---|
| 代入時の動作 | インスタンスへの参照をコピーして代入 | インスタンスの内容そのものをコピーして代入 |
| 変数が「不変」の保証 | インスタンスへの参照が変わらないこと | インスタンスの内容が変わらないこと |
| プロパティーの宣言 | 不変性担保のために基本 val 推奨 |
プロパティーの変更にイニシャライザーを通したい場合は let、それ以外は var
|
-
ここはあくまでAndroidエンジニアが触るKotlinの話です。Kotlin/Nativeなどの話は本記事では扱いません。 ↩
-
もちろんもし
Profile自体が大きかったりすると、そのコピー自体が非常にコストが高いので、Swiftではコンパイル時の「Copy-on-Write」と言う最適化を施しており、実際は値の変更があってから始めてコピーされますが、ここら辺の話は今は気にしなくていいです。 ↩ -
ただしさらに気をつけないといけないのは、ここで
user.nameを変更しようとするとビルドエラーになるのも、あくまでnameであるString型自身も値型だから、つまりnameの値が変わるからです。仮にもしnameは参照型で、ここもname自身ではなくさらに子供のプロパティーの変更でしたら、nameが持つ参照が変わらないので、ビルドエラーになりません。 ↩ -
これを理解するには、「参照型」「値型」の他に、さらに「参照渡し」「値渡し」の概念も理解する必要があるので、もし今わからなくても大丈夫です;ここはあくまで一つの例として挙げているだけです。 ↩