🧊

再代入不能な遅延格納プロパティを宣言する

2022/12/16に公開3

Swiftの強力な機能として、遅延格納プロパティ(Lazy stored property)というものがあります。
これは、変数の初期化を最初に変数にアクセスされるまで遅延することができる機能です。

lazy var date: Date = .now

print(Date.now) // 2022/12/16 00:00:00
Sleep.for(.second(120))
print(date) // 2022/12/16 00:00:02

便利なlazy varですが、Swiftではletでこの宣言をすることができません。

lazy let value: Int = 0 // 'lazy' cannot be used on a let

そこで、letのように再代入不能な遅延格納プロパティをPropertWrapperを使って作ってみます。

@propertyWrapper
enum LazyLet<Value> {
  case uninitialized(() -> Value)
  case initialized(Value)

  init(wrappedValue: @autoclosure @escaping () -> Value) {
    self = .uninitialized(wrappedValue)
  }

  var wrappedValue: Value {
    mutating get {
      switch self {
      case .uninitialized(let initializer):
        let value = initializer()
        self = .initialized(value)
        return value
      case .initialized(let value):
        return value
      }
    }
    set {
      switch self {
      case .uninitialized:
        self = .initialized(newValue)
      case .initialized:
        break
      }
    }
  }
}

set時にuninitializedで無い場合は値を変更しないことで実現しています。
なお、挙動として再代入不能な遅延格納プロパティは出来たわけですが、当然コンパイルで再代入を検出することはできません。

final class LazyLetTests: XCTestCase {
  func testInit() throws {
    @LazyLet var value: Int = 0
    XCTAssertEqual(value, 0)
    value = 1
    XCTAssertEqual(value, 0)
  }
  
  func testInit2() throws {
    @LazyLet var value: Int = 0
    value = 1
    XCTAssertEqual(value, 1)
    value = 2
    XCTAssertEqual(value, 1)
  }
}

このような挙動になります。

https://github.com/noppefoxwolf/LazyLet

Discussion

uhooiuhooi

面白い記事…!
だけどバグに気づきにくくなりそう…。もし実務で使うなら .initialized 時はエラーをスローしたほうがよさそう…?

noppenoppe

PropertyWrapper全般に言えますが、一見するとただ代入しているだけに見えて暗黙的に特殊な処理が走るのは非常に読みにくいのであんまり実務で使って欲しい実装では無いですね。
実務でどうしようもない時は、Optionalを使ったり、記事中のenumをそのまま宣言するのが良いかなと思います。(そうすることで代入時、取得時に必ずswitchでケースごとの処理が保証できる)
エラースローに関してはwrappedValueにthrowsをつければいけそうですが、PropertyWrapperの制限で多分使えないはず(試してない)

uhooiuhooi

あ、ですよね…ネタにマジレスした感が出てすみませんでしたw