- Value Semantics とは
- Value Semantics を持たない型の問題と対処法
- Swift が値型中心の言語になれた理由とその使い方
Value Semantics を持たない型の問題と対処法
Value Semantics とは何かわかったところで、次に、 Value Semantics を持っていると何がうれしいのかを見て行きましょう。
Value Semantics を持たない型が起こしがちな問題
Value Semantics の利点を知るために、 Value Semantics を持たない型がどのような問題を引き起こすのかを見てみます。ここでは、 Value Semantics を持たない型が起こしがちな問題の例として次の二つを取り上げます。
- 意図しない変更
- 整合性の破壊
意図しない変更の例
あるサービスのアイテムを考えます。アイテムというのは、たとえば、そのサービスがフォトアプリなら写真、メモアプリなら個々のメモです。
今、アイテムが次のような Item
クラスで表されているとします。
class Item {
var name: String
...
var settings: Settings
}
settings
はアイテムの設定を表すプロパティです。 Settings
は次のようなクラスです。
class Settings {
var isPublic: Bool
...
}
isPublic
は、このアイテムが公開されているかを表すプロパティです。
この Settings
クラスは Value Semantics を持たず Reference Semantics だけを持っています。このとき、どのような問題が起こり得るかを見てみましょう。
アイテムの複製は様々なアプリでよく行われる処理です。上記の Item
クラスを使ってそれを実装するコードを考えてみます。今、 Item
型のインスタンスが変数 item
に格納されているとして、複製の処理を次のように書いたとします。
let item: Item = ...
let duplicated: Item = Item(
name: item.name,
...
settings: item.settings
)
複製されたアイテムは定数 duplicated
に格納されます。このとき、複製されたアイテムを公開するために次のようなコードを書くと何が起こるでしょうか。
duplicated.settings.isPublic = true
今、 Settings
クラスは Reference Semantics を持っているので、この変更はオリジナルの item
の settings
にも波及します。複製されたアイテムだけを公開したかったのに、オリジナルも公開されてしまいました。これは意図しない挙動で、望ましいものではありません。原因は、 Item
インスタンスを複製するときに Settings
インスタンスも複製しないといけなかった( ディープコピー しなければならなかった)のに、 シャローコピー してしまったことです。
Value Semantics を持たないと、ちょっとしたコーディング時のミスや設計時の考慮漏れによってこのような問題を引き起こしがちです。
整合性の破壊の例
次に、整合性の破壊の例を見てみます。
姓( familyName
)と名( firstName
)を持ったシンプルな Person
クラスを考えてみます。
class Person {
var firstName: String
var familyName: String
}
姓と名を連結してフルネームを返すのはよくある処理なので、この Person
クラスに fullName
プロパティを追加します。 fullName
は Computed Property で、ただ firstName
と familyName
を連結して返すだけです。
class Person {
var firstName: String
var familyName: String
var fullName: String { // 👈
"\(firstName) \(familyName)"
}
}
しかし、 fullName
にアクセスする度に新しい文字列を生成するのは無駄です。そこで、最初に fullName
にアクセスされたときに生成された文字列をキャッシュしておいて、二度目以降はその文字列を返すことにします。たとえば、次のようなコードで実装できます。
private var _fullName: String? // キャッシュ
var fullName: String {
if let fullName = _fullName {
return fullName
}
_fullName = "\(firstName) \(familyName)"
return _fullName!
}
また、 firstName
や familyName
が変更されたときにキャッシュとの整合性が崩れてしまうので、 firstName
や familyName
が変更されたときにはキャッシュを破棄するようにします。
var firstName: String {
didSet { _fullName = nil }
}
var familyName: String {
didSet { _fullName = nil }
}
これで、 Person
クラスのキャッシュは適切に機能するようになりました。
さて、このとき次のような処理を考えてみます。
let person: Person = Person(
firstName: "Taylor",
familyName: "Swift"
)
var familyName: String = person.familyName
familyName.append("y")
このとき、変数 familyName
に格納されるのは String
インスタンスで、 String
は Value Semantics を持つ struct
です。変更に対して独立なので、 append
による変更は person.familyName
に影響を影響を与えません。この処理の後には、変数 familyName
の値は "Swifty"
になりますが、 person.familyName
の値は "Swift"
のままです。
しかし、もし仮に String
が Reference Semantics を持ったクラスだったらどうなるか考えてみましょう( append
メソッドは自身の状態を変更するので、このとき String
は ミュータブルクラス となり Value Semantics を持ちません)。
String
が Reference Semantics を持っていたら、変数 familyName
と person.familyName
は同じ String
インスタンスを共有することになります。そのため、 familyName.append("y")
の結果、変数 familyName
の値も person.familyName
の値も "Swifty"
に書き換えられます。これ自体は一概に悪いこととは言えません。
問題になるのは、 fullName
のキャッシュが存在する場合です。もし、 fullName
のキャッシュとして "Taylor Swift"
という文字列が保存されているとどうなるでしょうか。 familyName
は "Swifty"
に更新されたのに fullName
は Taylor Swift
のままということになってしまいます。
print(person.fullName) // "Taylor Swift"
var familyName: String = person.familyName
familyName.append("y")
print(person.familyName) // "Swifty"
print(person.fullName) // "Taylor Swift" 😵
Person
インスタンスの状態の整合性が破壊されてしまいました。状態変更時にはキャッシュをクリアするように設計したのに、何が問題だったのでしょうか。
person.familyName
を直接 "Swifty"
に更新する場合は、キャッシュが適切にクリアされて問題は起こりません。
print(person.fullName) // "Taylor Swift"
person.familyName = "Swifty"
print(person.familyName) // "Swifty"
print(person.fullName) // "Taylor Swifty" 😄
問題が起こった原因は、 firstName
プロパティや familyName
プロパティなどの Person
の API を使わずに、 String
のメソッドを介して間接的に内部状態が変更されるという想定しない変更のパスが存在したことです。 Reference Semantics はそのようなパスを作りやすいので、設計時にそれを見落とすと、この例のように内部的な整合性が破壊されてしまうことがあります。
問題への対処法
このような問題は、特に 参照型 中心の言語ではよく起こるので、様々な対処法が考えられています。よく取られる対処法として次の三つがあります。
- 防御的コピー
- Read-only View
- イミュータブルクラス
Swift は 参照型 中心ではなく 値型 中心の言語です。上記とは異なり、 値型 を用いることで問題に対処しています。
- 値型
ここでは、 イミュータブルクラス と 値型 について取り上げて比較し、 値型 の優位性を見てみます。
イミュータブルクラス
前に見たように イミュータブルクラス は Value Semantics を持ちます。そのため、 イミュータブルクラス の導入は、 Value Semantics を持たない型が起こす問題を直接的に解決できます。しかし、 イミュータブルクラス も万能ではなく、別の問題を引き起こします。それらを順に見てましょう。
イミュータブルクラスによる問題の解決
まずは、先の二つの問題が イミュータブルクラス によってどのように解決されるのかを見てみます。
一つ目は「意図しない変更」の例です。 Settings
クラスが イミュータブルクラス なら、複製されたアイテムの isPublic
プロパティを変更しようとするコードはコンパイルエラーになり、意図しない変更が防止できます。
let duplicated: Item = Item(
name: item.name,
...
settings: item.settings
)
duplicated.settings.isPublic = true // ⛔
Settings
クラスは イミュータブル なので、 isPublic
プロパティを変更することはできません。変更するためには、 isPublic
が true
である新たな Settings
インスタンスを生成し、 duplicated.settings
に代入する必要があります。
duplicated.settings = Settings(isPublic: true, ...)
この場合、 duplicated.settings
に代入されるのはまったく新しい Settings
インスタンスです。 item.settings
と共有された Settings
インスタンスを変更したわけではないので、 item.settings
に影響を与えることはありません。
同じように、「整合性の破壊」の例も イミュータブルクラス によって解決されます。
var familyName: String = person.familyName
familyName.append("y") // ⛔
今、 String
が イミュータブルクラス だとすると、自身の状態を変更する append
メソッドを持つことはできません。よって、 append
メソッドを介して Person
インスタンスの内部状態が変更され、整合性が破壊されることもありません。
実際、 Java や Kotlin など、参照型中心の言語の多くでは String
は イミュータブルクラス として実装されています。それは、ここで挙げたような問題を防止するためです。 Foundation の NSString
クラスのインスタンスも イミュータブル ですが、残念ながら NSString
には NSMutableString
というサブクラスが存在します。 NSString
型としては イミュータブル にならないので、 NSMutableString
を使って上記のような問題を引き起こすことができます。
イミュータブルクラスによって引き起こされる問題
イミュータブルクラス で問題が解決できるなら何でも イミュータブルクラス にすれば良いかというと、そう簡単な話ではありません。 イミュータブルクラス は変更が容易でないという別の問題を引き起こします。例を見てみましょう。
何らかのサービスのユーザーが User
クラスで表されるとします。ユーザーはそのサービスで利用できるポイントを持っており、 User
クラスの points
プロパティで表されます。
// ミュータブルクラスの場合
class User {
let id: Int
var name: String
...
var points: Int
...
}
このように User
が ミュータブルクラス なら、ユーザーに 100 ポイント付与するコードは次のように簡潔に書けます。
// ミュータブルクラスの場合
let user: User = ...
user.points += 100
しかし、 User
が イミュータブルクラス だとこのように簡単にはいきません。
// イミュータブルクラスの場合
final class User {
let id: Int
let name: String
...
let points: Int
...
}
イミュータブルクラス のインスタンスの状態を変更することはできないので、 100 ポイント付与したくても User
インスタンスの points
だけを増やすことはできません。状態を変更するためには、 points
以外はまったく同じで points
だけが 100 増えた新しい User
インスタンスを生成し、変数 user
の値をその新しいインスタンスに差し替える必要があります。
// イミュータブルクラスの場合
var user: User = ...
user = User(
id: user.id,
name: user.name,
...
points: user.points + 100,
...
)
これは、コードとしても複雑ですし、たった一つのプロパティを更新するためだけにインスタンスを丸ごと作り直す必要があるのでパフォーマンスもよくありません。
参照型 中心の言語の中には、コードの複雑さを軽減するための特殊な構文や言語仕様を提供しているものもあります。たとえば、 Kotlin の data class
を使えば copy
メソッドが自動生成され、目的のプロパティだけを変更した新しいインスタンスを生成することができます。
// Kotlin
// イミュータブルクラスの場合
var user: User = ...
user = user.copy(points = user.points + 100)
しかし、それでもミュータブルクラスの場合と比べると複雑ですし、ネストしたプロパティを更新しようとするとさらに大変です。
// Kotlin
// ミュータブルクラスの場合
group.owner.points += 100
// Kotlin
// イミュータブルクラスの場合
group = group.copy(
owenr = group.owner.copy(
points = group.owner.points + 100
)
)
このように、 イミュータブルクラス を用いると ミュータブルクラス と比べて状態の変更が複雑です。 イミュータブルクラス は Value Semantics を持たないことに起因する問題を解決してくれますが、状態の変更が複雑になるため、すべてを イミュータブル にすると利益が不利益に見合わないことが多いです。そのため、 参照型 中心の言語では ミュータブルクラス と イミュータブルクラス を利益のバランスを取って適切に使い分ける必要があります。
値型
前述のように、 Swift は イミュータブルクラス ではなく 値型 によって問題の解決を図ります。 値型 であれば簡単に Value Semantics を持たせることができます。 Value Semantics を持たせるという意味では、 イミュータブルクラス と同じ方向性を持った対処法です。
「意図しない変更」の例は、 Settings
が Value Semantics を持った struct
であれば何の問題も起こりません。
// 値型の場合
let duplicated: Item = item // 複製
duplicated.settings.isPublic = true // item.settings.isPublic は変更されない😄
「整合性の破壊」の例についても、変数 familyName
に対する変更は person
には何の影響も及ぼさないため、整合性が破壊されることもありません。
// 値型の場合
print(person.fullName) // "Taylor Swift"
var familyName: String = person.familyName
familyName.append("y")
print(person.familyName) // "Swift"
print(person.fullName) // "Taylor Swift"
加えて、 値型 には、 ミュータブルクラス のように変更が容易であるという特徴があります。先の User
にポイントを付与するコードも、 User
が struct
なら次のように書けます。
// 値型の場合
struct User {
let id: Int
var name: String
...
var points: Int
...
}
// 値型の場合
var user: User = ...
user.points += 100
user.points += 100
というのは ミュータブルクラス におけるコードと全く同じです。しかも、この User
は Value Semantics も持っています。
つまり、 値型 は ミュータブルクラス の持つ変更の容易さと、 イミュータブルクラス の持つ Value Semantics のいいとこ取りをしたような存在だと考えられる わけです。
Swift の標準ライブラリで提供される型は、ほぼすべてが 値型 です。次に挙げるぱっと思い付くような型は全部 値型 です。
Int
,Float
,Double
,Bool
String
,Character
Array
,Dictionary
,Set
Optional
,Result
もちろん、ここで挙げたすべての型が Value Semantics を持っています(ただし、型パラメータを持つ型は、型パラメータに Value Semantics を持つ型が指定された場合のみ Value Semantics を持ちます)。
まとめ
Value Semantics を持たない型は、「意図しない変更」や「整合性の破壊」などの問題を引き起こしがちです。それに対処するために、 防御的コピー や Read-only View 、 イミュータブルクラス の使用など方法が考えられてきました。しかし、それらには一長一短があり、たとえば イミュータブルクラス には変更が容易でないという問題があります。
値型 を使えば簡単に Value Semantics を実現でき、変更も容易です。 値型 は、 ミュータブルクラス と イミュータブルクラス のいいとこ取りをしたような存在だと言えます。 Swift の標準ライブラリの型はほぼすべてが Value Semantics を持つ 値型 であり、問題の回避と変更の容易さを両立できます。