Java経験者がSwiftのプロパティまわりで最初につまずく private(set) / mutating / get / set / didSet / willSet

朝の実(Asanomi)アイコン アプリ開発

Javaを書いてきた人がSwiftを読み始めると、var value = 0 のような単純なプロパティは分かりやすいです。

「Javaでいうフィールドみたいなものかな」と思って読めます。

でも少し進むと、private(set)mutatingget / setdidSet / willSet が出てきて、急に読みづらくなります。

Javaでは getter / setter メソッドとして分けて書くことが多い処理が、Swiftではプロパティの中にまとまって出てくるからです。

この記事では、Javaのフィールド、getter、setterと比較しながら、Swiftのプロパティまわりの構文を整理します。

まず役割で分ける

Swiftのプロパティは、ただ値を入れておく箱ではありません。

外からは読めるけれど勝手に変更させたくない値、代入された文字列を保存前に整形したい値、変更後に状態を補正したい値。そういった処理を、プロパティの近くにまとめて書けます。

Javaなら、フィールドを private にして、必要に応じて getName()setName(...) を用意する場面が多いと思います。

Swiftでは、その「読む」「書く」「変更前後に反応する」という処理を、プロパティ構文として書けます。

今回扱う構文は、ざっくり分けると次のようになります。

構文使う場面見方
private(set)外から読ませたいが、勝手に変更させたくない書き込み権限だけを狭める
mutatingstruct のメソッド内で自分の値を変える値型を変更する合図
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 の違い

setdidSet は、どちらも代入に関係するので少し混ざりやすいです。

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 = ... のようにプロパティアクセスとして書くことです。

一気に覚えようとすると大変ですが、「アクセス制御」「値型の変更」「読み書き」「変更前後の監視」に分けると、かなり読みやすくなると思います。