iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧊

Declaring Non-reassignable Lazy Stored Properties

に公開3

Swift has a powerful feature called lazy stored properties.
This allows you to delay the initialization of a variable until it is first accessed.

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

While lazy var is convenient, Swift does not allow this declaration with let.

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

Therefore, I will try to create a non-reassignable lazy stored property, similar to let, using a Property Wrapper.

@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
      }
    }
  }
}

This is achieved by not changing the value during set if it is not in the uninitialized state.
Note that while we have achieved a functionally non-reassignable lazy stored property, we naturally cannot detect reassignments at compile time.

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)
  }
}

This results in the following behavior.

https://github.com/noppefoxwolf/LazyLet

Discussion

uhooiuhooi

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

noppenoppe

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

uhooiuhooi

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