[Swift] PropertyWrapper 手を動かして理解する(その1 wrappedValue)

Swift

     
⌛️ 2 min.
SwiftUI では、@State 等が非常に重要な役割をしていますが、そのベースになっている PropertyWrapper を説明します。

そもそも PropertyWrapper はどういう意味なのか、どのような目的で使用されるのか、$ をつけることの意味? みたいなことを説明します。

話がややこしいので、いくつかに分けて、説明していきます。この記事では、PropertyWrapper の動作を説明しますので、@State の具体的な説明は、別記事予定です。

記載しているコードは、Xcode12 の playground で動作することを確認していますので、実際に動かしながら読んでもらうとわかりやすいです。

PropertyWrapper のアイデア

「プロパティにそのままアクセスするのではなく、アクセス時に、なんらかの制御を入れ込みたい」が このアイデアの発端です。

例えば、以下のようなコードを書いたことがある人は多いかもしれません。

example code


public class OneClass: Object {
  private var internalValue: Int?
  var value:Int {
    get {
      if internalValue == nil {
        self.internalValue = bigEffortCalc()
      }
      return internalValue!
    }
    set {
      internalValue = newValue
    }
  }
}

プロパティの値を計算することに時間やリソースがかかるので、プロパティ初期化時には設定したくなくて、必要と言われた段階(get される時)に、計算して求めたいケースです。
実際に必要になったら計算されるので、Int で定義できるはずなのに、設定のタイミングから、Int? で宣言する必要があります。
Int? で宣言してしまうと、アクセス時に、チェックを必要とするので避けたい でも、Int と定義するための計算を初期化時に行いたくない・・・ということで、
値を設定するタイミングの問題で、optional での宣言になってしまうというジレンマが発生することは多いです。

そのために、外部からアクセスするプロパティを、実際の stored property とせずに、computed property で実装していくことが必要になります。
どのような理由で必要となるかはケースバイケースですが、「内部変数を定義して、外部からは computed property 経由でアクセスさせる」というコードは、よく見るコードになっています。このようなコード(boiler plate と呼ばれます)を減らすことがこの propertyWrapper の目的です。

もちろん、同様のことが、上記のように private/public 等のアクセス指定子や setter/getter 等を組み合わせることでできますが、同じようなコードを複数箇所になんども記述することになってしまいます。

このような制御をすることを “wrap する”というふうに表現されていて、そのような仕組みを Swift 言語としてサポートするのが、property wrapper です。

MEMO

propertyWrapper の実例としては、SwiftUI での @State や @Binding が有名かと思います。Xcode を使って、@State から “Jump to Definition” すると、Apple が定義した propertyWrapper をみることができ、@State は、struct を使用して定義されていることがわかります。



Swift 言語仕様からの propertyWrapper とは?

Swift の言語仕様では以下のように説明されています。

To define a property wrapper, you make a structure, enumeration, or class that defines a wrappedValue property.

つまり、”wrappedValue” という インスタンス プロパティ を定義した struct/enum/class を定義することで、propertyWrapper を定義することになります。

意味のない propertyWrapper を作ってみる

propertyWrapper をよく理解するために、実際には、Wrap していない propertyWrapper を作ってみます。

意味のない propertyWrapper 定義

先に見たように、propertyWrapper の定義には、”wrappedValue” という名称で、property が必要です。”NoWrapInt” という名前で property wrapper を作ってみます。

example code


@propertyWrapper struct NoWrapInt {
  init() {
    self.wrappedValue = 0
  }
  var wrappedValue: Int
}

wrappdValue をそのまま変数として定義して、get されれば、wrappedValue を返し、set されればその値を wrappedValue に記憶させる propertyWrapper です。

特に、何の制御も行なっていないので、機能的な意味はありません。これを使って、どのように動くのか見ていきます。

意味のない propertyWrapper を使ってみる

PlayGround を使って、以下のようにして実行することができます。

example code


@propertyWrapper struct NoWrapInt {
  init() {
    self.wrappedValue = 0             // (1) デフォルト値セット
  }
  var wrappedValue: Int
}

struct CheckNoWrapInt {
  @NoWrapInt var value:Int
}

var checkNoWrapInt = CheckNoWrapInt()
print(checkNoWrapInt.value)           // (2)
checkNoWrapInt.value = 5              // (3)
print(checkNoWrapInt.value)           // (4)

(1) では、 wrappedValue のデフォルト値をセットしています。(2) では、その値 “0” が表示されます。(3) で別の値をセットしていますので、(4) では、 その値 “5” が表示されます。

CheckNoWrapInt という struct から見ると value という値ですが、@NoWrapInt という propertyWrapper は、その値を内部変数である wrappedValue に保存しているということになります。

意味を考える

もともと、value という Int 型の変数を定義しようとしているので、動作的に何かが変わるわけではありません。

@NoWrapInt という propertyWrapper 付きの 変数 value は、実は、value という変数は存在せずに、propertyWrapper 内に用意された wrappedValue に保存されているということです。

このことから、プログラム側から value にアクセスしようとしたときに、そのアクセスを propertyWrapper で制御できることがわかります。

今回は、propertyWrapper の内部に用意した変数へのアクセスに切り替えましたが、外部のDBに接続かもしれませんし、Cloud 上の何かかもしれません。
このようにアクセス先を切り替えることも、propertyWrapper の実装次第です。

その他には、set に渡された値の妥当性をチェックしてから保存する というような 意味的なアクセス制御や、(一番初めに例に出した)アクセスが発生したときに計算して返す というような時間的な制御も可能となります。

使い道そのものについては、とくに限定はありません。

propertyWrapper を使って value を定義したことで、value という変数そのものへのアクセスではなく、さらに内部の wrappedValue へのアクセスに切り替わっていることがポイントです。

言い換えると、プロパティをアクセスする側は、通常通り プロパティにアクセスしているつもりでも、propertyWrapper を使うことで、アクセス制御を変更することができるということです。

MEMO

背景を理解せずにアクセスできるのは、情報の遮蔽という意味では良いのですが、あまりにも理解がないとブラックボックスになってしまいがちです。

今の SwiftUI の @State は、すこしその領域に入り始めているかと思います。

wrappedValue のタイプ

なお、wrappedValue のタイプは、propertyWrapper で修飾している変数のタイプと同じであることが必要です。

example code


@propertyWrapper struct NoWrapInt {
  init() {
    self.wrappedValue = 0            
  }
  var wrappedValue: Int      // (1)
}

struct CheckNoWrapInt {
  @NoWrapInt var value:Int   // (2)
}

(1) と (2) は、同じタイプである必要があります。

まとめ

property wrapper とは
  • プロパティへのアクセスを制御する boiler plate を省略できる仕組み
  • struct/enum/class を使って定義する
  • wrappedValue というプロパティを持つことが必要

みてもらってわかるように、property wrapper 自体は難しくないんです。

長くなってきたので、初期化については、別記事にします。

Swift 学習におすすめの本

詳解Swift

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

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

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

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

Swift ポケットリファレンス

Swift を学んでも、プログラミング言語の文法を全て記憶しておくことは無理なので、ちょっとした文法の確認をするために、リファレンス本を手元に置いておくと便利です。

注意

Swift4 までしか対応していないので、相違点を理解して参照する必要があります。

説明は以上です。
不明な点やおかしな点ありましたら、ご連絡いただけるとありがたいです。

コメントを残す

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