Javaを書いてきた人がSwiftを読み始めると、var value = 0 のような単純なプロパティは分かりやすいです。
「Javaでいうフィールドみたいなものかな」と思って読めます。
でも少し進むと、private(set)、mutating、get / set、didSet / willSet が出てきて、急に読みづらくなります。
Javaでは getter / setter メソッドとして分けて書くことが多い処理が、Swiftではプロパティの中にまとまって出てくるからです。
この記事では、Javaのフィールド、getter、setterと比較しながら、Swiftのプロパティまわりの構文を整理します。
まず役割で分ける
Swiftのプロパティは、ただ値を入れておく箱ではありません。
外からは読めるけれど勝手に変更させたくない値、代入された文字列を保存前に整形したい値、変更後に状態を補正したい値。そういった処理を、プロパティの近くにまとめて書けます。
Javaなら、フィールドを private にして、必要に応じて getName() や setName(...) を用意する場面が多いと思います。
Swiftでは、その「読む」「書く」「変更前後に反応する」という処理を、プロパティ構文として書けます。
今回扱う構文は、ざっくり分けると次のようになります。
| 構文 | 使う場面 | 見方 |
|---|---|---|
private(set) | 外から読ませたいが、勝手に変更させたくない | 書き込み権限だけを狭める |
mutating | struct のメソッド内で自分の値を変える | 値型を変更する合図 |
get | 読み取り時の処理を自分で決めたい | Javaの getter に近い |
set | 代入時に整形・検証したい | Javaの setter に近い |
didSet | 変更後に処理を実行したい | 入ったあとの監視 |
willSet | 変更前に処理を実行したい | 入る直前の監視 |
private(set) は「読み取りは公開、書き込みは内部だけ」
struct Counter {
private(set) var value = 0
mutating func increment() { value += 1 }
}private(set) は、プロパティの読み取りは外からできるけれど、値の変更は型の中だけに制限する指定です。
これをJavaで書くと、フィールドを private にして、getterだけを公開するイメージです。
class Counter {
private int value = 0;
int getValue() {
return value;
}
void increment() {
value += 1;
}
}Swift側の Counter では、外から counter.value を読むことはできます。一方で、外から counter.value = 10 のように直接書き換えることはできません。
値を変えたい場合は、increment() のようなメソッドを通します。
var counter = Counter()
counter.increment()
print(counter.value) // 1こうすると、Counter の値がどのように変わるかを型の中に閉じ込められます。
外から自由に書き換えられる値ではなく、「増やす」という操作を通してだけ変わる値として扱える、ということです。
Javaの getValue() は「読むためのメソッド」を用意します。Swiftの private(set) は、counter.value というプロパティアクセスの形は保ったまま、書き込みだけを閉じます。
mutating は struct の中身を変える合図
Swiftの struct は値型です。
そのため、struct のメソッドの中で自分自身のプロパティを変更する場合は、メソッドに mutating を付けます。
mutating func increment() {
value += 1
}mutating は「このメソッドは、この struct 自身の値を変更します」という合図です。
Javaのクラスに慣れていると、メソッド内でフィールドを書き換えるのは自然に見えます。けれどSwiftの struct では、値そのものを書き換える操作として明示する必要があります。
Javaの通常のクラスは参照型なので、インスタンスメソッドの中でフィールドを書き換えても、特別なキーワードは要りません。
void increment() {
value += 1;
}Swiftの struct は値型なので、「このメソッドは自分自身の値を変える」という意味を mutating で明示します。
ここで大事なのは、private(set) と mutating は別の役割だということです。
private(set) は「外から直接書き換えられるか」を制御します。mutating は「struct のメソッド内で自分の値を変えるか」を示します。
同じコードに並んで出てくるので混ざりやすいですが、見ているポイントが違います。
get / set は、読み書きの処理を自分で決める
次は get / set です。
struct User {
private var _name: String = ""
var name: String {
get {
_name
}
set {
_name = newValue.trimmingCharacters(in: .whitespaces)
}
}
}get は、user.name を読むときに呼ばれます。
set は、user.name = " Taro " のように代入するときに呼ばれます。set の中では、代入されようとしている値を newValue として使えます。
これをJavaで書くと、次のような getter / setter に近いです。
class User {
private String name = "";
String getName() {
return name;
}
void setName(String name) {
this.name = name.trim();
}
}この例では、外から name に空白付きの文字列が入ってきても、保存する前に前後の空白を削っています。
var user = User()
user.name = " Taro "
print(user.name) // "Taro"_name は実際に値を保存するためのプロパティです。name は外向きの入り口です。
つまり、外から見ると name という普通のプロパティに見えるけれど、内部では保存前に整形処理を挟んでいます。
Javaでは user.setName(" Taro ") と呼びます。Swiftでは user.name = " Taro " と普通の代入に見えます。
ここが大きな違いです。Swiftの set は setter 的な処理ですが、呼び出し側から見るとメソッド呼び出しではなく、プロパティへの代入として読めます。
get も同じです。Javaでは user.getName() と呼ぶところを、Swiftでは user.name と読みます。
つまり、Swiftの get / set は「Javaの getter / setter に近い処理を、プロパティ構文の中に入れたもの」と見ると分かりやすいです。
didSet は「変更されたあと」に動く
didSet は、値が変更されたあとに呼ばれます。
var score: Int = 0 {
didSet {
if score < 0 {
score = 0
}
}
}この例では、score = -1 のような代入が起きたあとで、score が負の数なら 0 に戻しています。
score = -1
print(score) // 0一度値を受け取ったあと、状態を整える。これが didSet の分かりやすい使い方です。
これをJavaで書くと、setterの中に代入後の処理を書く形に近いと思います。
void setScore(int score) {
this.score = score;
if (this.score < 0) {
this.score = 0;
}
}Swiftでは、この「値が入ったあとに反応する処理」を didSet としてプロパティ側に書けます。
たとえば、値が変わったあとに画面表示用の状態を更新したい、ログを出したい、上限や下限に丸めたい、といった場面で使えます。
ただし、値の補正をあまり複雑にしすぎると、「代入しただけなのに裏でいろいろ起きる」コードになります。小さく使うのがよさそうです。
willSet は「変更される直前」に動く
willSet は、値が変更される直前に呼ばれます。
これから入ろうとしている値は、標準では newValue という名前で参照できます。
var score: Int = 0 {
willSet {
print("これから \(newValue) が入る")
}
}willSet は、変更前にログを出す、変更前の値と新しい値を比較する、といった用途に向いています。
これをJavaで書くと、setterの最初で「これから入る値」をログに出すような処理に近いです。
void setScore(int score) {
System.out.println("これから " + score + " が入る");
this.score = score;
}Swiftでは、これから入る値を newValue として参照できます。
一方で、値を補正する用途には向きにくいです。
たとえば次のようなコードは、直感に反して負の値を防げません。
var score: Int = 0 {
willSet {
if score < 0 {
score = 0
}
}
}
score = -1
print(score) // -1理由は2つあります。
willSetの中のscoreは、これから入る値ではなく、変更前の古い値willSetの中でscoreに代入しても、そのあとに本来の新しい値で上書きされる
実際にこの形のコードを書くと、Swiftは「willSet の中でプロパティへ代入しても、新しい値で上書きされる」という警告を出します。
負の値を防ぎたいなら、基本的には didSet で補正するか、set の中で保存前に整形するほうが読みやすいです。
set と didSet の違い
set と didSet は、どちらも代入に関係するので少し混ざりやすいです。
Javaの setter と比較すると、自分の中では次のように分けると読みやすいです。
set: Javaの setter 本体に近い。保存する前に、入ってきた値をどう扱うか決めるdidSet: setterの中で代入したあとに続けて書く処理に近い。保存されたあとに、結果を見て何かする
たとえば、文字列の前後の空白を削ってから保存したいなら set が自然です。
set {
_name = newValue.trimmingCharacters(in: .whitespaces)
}一方で、いったん入った数値を見て、範囲外なら戻すくらいの処理なら didSet でも読みやすいです。
didSet {
if score < 0 {
score = 0
}
}どちらでも実現できる場面はありますが、「保存前に整える」のか「保存後に反応する」のかで見ると、使い分けやすくなります。
まとめ
Swiftのプロパティまわりの構文は、最初はそれぞれ別の文法に見えます。
でもJavaのフィールド、getter、setterと比較すると、役割はかなり対応づけて読めます。
Swiftでは、プロパティの近くに次のようなルールを置けます。
- 値を誰が変更できるか
structの値をメソッド内で変更するか- 値を読むときに何を返すか
- 値を書くときに何を保存するか
- 値が変わる前後で何をするか
private(set) と mutating は、値型らしい設計に関係します。get / set / didSet / willSet は、プロパティの読み書きに処理を挟むための構文です。
特に get / set は、Javaの getter / setter と比較すると理解しやすいです。
違いは、Javaでは getName() / setName(...) のようにメソッドとして呼び出すのに対して、Swiftでは user.name / user.name = ... のようにプロパティアクセスとして書くことです。
一気に覚えようとすると大変ですが、「アクセス制御」「値型の変更」「読み書き」「変更前後の監視」に分けると、かなり読みやすくなると思います。
