[Swift] computed property の init と storageRestrictions

     
⌛️ 3 min.

Swift5.9 から computed property に指定できるようになった init accessor と storageRestrictions について、説明して(自分の)理解を深めます。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS14.5
  • Xcode 16 Beta
  • iOS 17.5
  • Swift 5.9
MEMO

理由は不明ですが、一部のコードは、PlayGround では動作しませんでした。

computed property

computed property は、get や set を指定して使用します。

[Swift] コードスニペット computed property [Swift] コードスニペット computed property

以下の実装は、evenNumber は、常に偶数を返すのですが、内部的には 2で割った値を _hiddenNumber で保持しているという例になっています。(外部に見える情報と内部で保持する情報が異なるという例です)

class SomeClass {
    var _hiddenNumber: Int = 0
    var evenNumber: Int {
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
}

使ってみると、以下のようになります。

let myClass = SomeClass()

print(myClass.evenNumber)     // 0
print(myClass._hiddenNumber)  // 0
myClass.evenNumber = 8       
print(myClass.evenNumber)     // 8
print(myClass._hiddenNumber)  // 4

PlayGround で確認するために、private 指定していませんが、通常は、_hiddenNumber は、private 指定して外部からは見えないようにすると思います。

このように、外部から見えるインターフェースに対して、内部の storage が少し異なるというケースはよくあります。

このようなときには、property wrapper にするケースも多いですが、いずれにしても computed property がよく使用されます。

ここで、SomeClass の実装を確認してみると、内部の _hiddenNumber は、evenNumber と非常に深い関わりがある(実質同じものを表現している)にもかかわらず、_hiddenNumber は、eventNumber の実装とは別の場所で初期化されています。

ここでのポイントは2つです。
・eventNumber は、内部の _hiddenNumber により実装されている
・_hiddenNumber は、いわゆる実装詳細なので、外部からは見えないし理解されない

初期値を指定したい

ここで、初期値を指定したくなったとします。

現在は、0 ですが、これを外部から与えたいということです。

(_hiddenNumber は、eventNumber の 1/2 の値を保持しているということを知っている前提で)
実直に実装すると以下のようになります。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ evenNumber: Int) {
        self._hiddenNumber = evenNumber / 2
    }
}

let myClass = SomeClass(4)

print(myClass.evenNumber)      // 4
print(myClass._hiddenNumber)   // 2
myClass.evenNumber = 8
print(myClass.evenNumber)      // 8
print(myClass._hiddenNumber)   // 4

期待通りの動作をします。

しかし、ここでの懸念点は、init で _hiddenNumber と evenNumber の関係性を記述しなければいけないことです。(できれば、computed property 内部に留めたいと考えてます)

できれば、class の init でも以下のように書ければ、_hiddenNumber を本当に(?) 隠しておくことができます。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ evenNumber: Int) {
        self.evenNumber = evenNumber  // set 経由で、_hiddenNumber を設定してほしい・・・
    }
}

しかし、コンパイル段階でエラーとなります。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ evenNumber: Int) {
        // ERROR!!  self' used in property access 'evenNumber' before all stored properties are initialized
        self.evenNumber = evenNumber
    }
}

get/set を指定しているので動いてくれても良さそうですが、初期化のこの時点では オブジェクトとしての初期化が完了していないので、get/set を使用できないということです。

そこで、init accessor が登場してきます。

init accessor

ここでの init は、init accessor であり、struct や class の init ではないです。

該当 SE は、こちらです。

init accessor を使うことで、computed property に対しても初期化できるようになります。

実際には、computed property 経由で、背後にある要素に必要な初期化を行うことができるようになるということです。

以下のように get/set と同じような感じで、init も指定できます。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        init(initialValue) {
            _hiddenNumber = initialValue / 2
        }
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ number: Int) {
        self.evenNumber = number
        //self._hiddenNumber = number / 2
    }
}

computed property ではありますが、class/struct の init で設定することができるようになります。

このときは、set ではなく、computed property の init accessor を呼んでいます。

しかし 残念ながら、上記は、コンパイルエラーとなります。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        init(initialValue) {
      // ERROR!! : Cannot reference instance member '_hiddenNumber'; 
             //         init accessors can only refer to instance properties listed in 'initializes' and 'accesses' attributes
            _hiddenNumber = initialValue / 2
        }
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ number: Int) {
        self.evenNumber = number
        //self._hiddenNumber = number / 2
    }
}

エラーの理由は、init accessor 内部で初期化する/参照する プロパティを宣言していないためです。

init accessor は、init の流れの中で呼ばれるハズなのですが、どの プロパティを初期化するのか/ どのプロパティを参照するのかを 宣言することが必要です。
# このような宣言をすることで コンパイラが矛盾を検知できるようになります。

具体的には、init accessor で初期化/参照するプロパティは、 @storageRestrictions という attribute を使用して宣言することが必要です。

今回の例では、_hiddenNumber を初期化しますので、以下のように宣言することになります。

class SomeClass {
    var _hiddenNumber: Int
    var evenNumber: Int {
        @storageRestrictions(initializes: _hiddenNumber)
        init(initialValue) {
            _hiddenNumber = initialValue / 2
        }
        get {
            return _hiddenNumber * 2
        }
        set {
            _hiddenNumber = newValue / 2
        }
    }
    init(_ number: Int) {
        self.evenNumber = number
        //self._hiddenNumber = number / 2
    }
}

こうすることで、evenNumber の init accessor 内部で_hiddenNumber への初期化が可能となります。

つまり、evenNumber に対しての初期化が、_hiddenNumber への初期化に相当することとなります。

なお、宣言すると初期化することが要求されますので、init accessor 内で 初期化しないとエラーになります。

今回の例では使用していませんが、参照する場合は、accesses という引数で指定します。

accesses は、init accessor が初期化するために参照するプロパティを宣言するため、init accessor が呼ばれる前に初期化されている必要があり、swift コンパイラは その順番もチェックし、初期化されていないのであれば、エラーとして検知されます。

# コンパイラは、attribute の宣言を基準に判断しますので、実際に参照されているかはチェックしていないようです。
# initializes で指定されていると 初期化されていない場合はエラーになります。

まとめ

computed property の init accessor と storageRestrictions の使用例を見てきました。

computed property の init accessor と storageRestrictions
  • computed property に対して init を定義できる
  • computed property の init から別プロパティを初期化するには、storageRestrictions という attribute 指定が必要
  • init 内部から初期化するプロパティは、initializes 引数で指定する
  • init 内部から参照するプロパティは、accesses 引数で指定する
  • コンパイラは、storageRestrictions という attribute を参考に エラーチェックを行う

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です