[Swift] Property Wrapperを理解する 0258-property-wrappersの写経 日本語訳

SwiftのPropertyWrapperを理解できてきた気がするけど、その本質からの理解を得たくて、SE(Swift Evolution)を日本語化して写経してみる

# まだ用語がブレてます。推敲していくことで統一予定。

いうまでもなく、オリジナルは、こちら

Introduction/導入

プロパティを実装するパターンは、いくつかあって、繰り返し利用される。特定の固定コードをコンパイラーに入れ込むよりは、ライブラリとして定義されたこれらのパターンを
一般化した”Property Wrapper”のメカニズムとして提供する方が良い。

2015-106 property behaviorsでの方向性とは反対のアプローチとなる。
いくつかの例は同じだが、この提案は、デザインがよりシンプルになり、ユーザーに理解しやすく、コンパイラの実装も大きな変更を要さない。
以下のセクションでは、この設計の本質的な相違点について議論している。

Pitch #1

Pitch #2

Pitch #3

Motivation/動機

これまでに、lazy や @NSCopying のようなプロパティーに対してのいくつかの重要なパターンを言語でサポートしようとしてきた、
しかし、これらは、非常に狭い領域でのユーティリティだった。
たとえば、Swiftは lazy プロパティを言語の基本機能として備えている、なぜなら、遅延初期化は一般的でありプロパティを optional として外部提供してしまうことを避けるために必要だからである。このサポートなしには、同様の効果を得るためには、非常に多くの定型的なコードが必要になる。

アクセスがあったら値設定

lazy を言語に取り込むことで、いくつかの不利益もあります。言語とコンパイラを複雑にしますし、直交性が犠牲となりました。また、柔軟性も失われました。遅延初期化については、何種類かのバリエーションがありましたが、全てをサポートは、しませんでした。

遅延初期化以外に重要なプロパティのパターンがあります。「一度設定して以降変更不可」を「遅延」させておこなうマルチフェーズ初期化です。

一度設定したら以降変更不可&設定するまでアクセス不可

ここでは、Implicitly-unwrapped optionls(IUO)を使うことで実現されていますが、optionalでないletを使うことに比べて、多くの安全性が失われています。
マルチフェーズ初期化をIUOを使って実装することは、不変性とnil-安全性を失ってしまいます。

アトリビュート @NSCopying を導入したことで、変数設定時のコピー作成に、NSCopying.copy()を使うこととなりました。実装パターンは、これと似たものになるかもしれません。

NSCopying.copyパターン

Proposed solution/提案

“Property wrapper”の導入を提案します。それを使うことで、プロパティ定義に置いて、どのwrapperを使って実装するかを宣言できます。この wrapper はアトリビュートとして記述されます。

アトリビュート記述をもつプロパティfoo

この行によりプロパティ foo は、以下の”Property wrapper” Lazy として記述された方法で実装されます。

コード

“Property wrapper type” は、ラッパーとして使われるプロパティのストレージを提供します。
Wrapper タイプの wrapperdValue プロパティは、ラッパーの実際の実装になります。optionalの init(wrappedValue:)によって、Valueタイププロパティを初期化することが可能です。

コード

上記は、以下のようなコードになります。

コード

Prefix _ は、同期ストレージプロパティ名として意図的に使われていて、同期ストレージプロパティの名前を事前合意していることで、private なプロパティを問題なく定義できます。例えば、Lazy で初期値に戻すための reset(_:)を定義することもできます。

コード

背後にあるプロパティを明示的に初期化することができます。

コード

“Property wrapper”のインスタンスは、名前の後にカッコとイニシャライザの引数を指定することで直接初期化できます。
上記のコードは、以下の1行の変数定義のコードと同値になります。

コード

“Property wrapper”は、グローバル/ローカル もしくは、タイプスコープにかかわらず、使えます。それらプロパティは、監視アクサッサ(willSet/didSet)を持つことができますが、明示的にgetter/setterを書く必要はありません。

“Lazy property wrapper”は、初期化以外にほとんどAPIを持ちませんので、外部に公開する意味は少ないかもしれません。
しかし、”property wrapper”は、興味深いAPIをもつ要素との関係を記述することができます。例えば、名前によって定義されたデータベースフィールドを参照する property wrapper を使うかもしれません。

コード

モデルを上記の Field property wrapper を使って定義できます。

コード

Field そのものが、Person を使うユーザーにとって重要なAPIになっています。使うことで、値をflushしたり、新しい値をfetchしたり、データベースの対応するフィールドの名前を取得することもできます。しかし、アンダースコア付きの変数(_firstName, _lastName, _birthdate)は、privateであり、直接操作することはできません。

APIとして使えるようにするために、property wrapper “Field”は、”projection”を提供し、それ経由でデータベースとのフィールドの関係性を操作できます。
“Projection” プロパティは、前置詞として、”$”を使います。ですので、firstName プロパティは、 $firstName として使われ、firstName が参照可能な場所では、$firstName も参照可能です。”property wrapper”は、projetionを、projectedValueを定義することにより提供します。

コード

projectedValue が存在する場合は、projection 変数が、projectedValueをwrappするプロジェクション変数が作られます。例えば、以下のプロパティ

コード

は、以下のように展開されます。

コード

このように展開されることによって、”property”と”projection”の両方を操作することができるようになります。

コード

Examples/例

詳細設計について説明する前に、いくつかの wrapper の例を提示する。

Delayed Initialization/遅延初期化

“Property wrapper”は、遅延初期化としてつられ、そこでは、コンパイル時にではなくより動的に明確な初期化ルールが強制されます。このことで、マルチフェーズ初期化の際に、IUO(Implicitly-unwrapped option)を避けることができます。変更可能(再設定可能なvarを使って)・変更不可(再設定できないletを使って)のいずれの変数も定義することができます。

Mutableバージョン
immutableバージョン

上記の定義を使うことで、マルチフェーズ初期化が可能となります。

マルチフェーズ初期化

NSCopying

多くのCocoaのクラスでは、value-likeなオブジェクトを使っていて、それは、明示的なコピーを必要とします。Swiftでは、@NSCopying アトリビュートを提供していて、Objective-C の @property(copy) のような動作をさせることが可能となっています。プロパティにセットされたときに、新しい Object で copy method が呼ばれます。この振る舞いを wrapper にすることができます。

NSCopying相当

この実装は、SE-0153で説明されている問題を解決します。copy() を init(wrappedValue:) の外側で使うことで、意味的には、SE-0153 の解決策となります。

(訳注:SE-0153ざっくり説明:イニシャライザの中で、setter経由でアクセスしないのがベストプラクティス)

Atomic/一連操作

一連操作(load, store, 増減, 比較して変更)は Swift に一般的に要求される機能である。
それら機能を実装することは、Compilerや標準ライブラリを魔法のようにしてしまうが、インターフェイス自身は、property wrapper として提供することができる。

コード

以下は、Atomic の単純なユースケース。Atomic タイプを使うことで、ローレベル操作を組み合わせることは非常に一般的で、そこでは、シンプルな操作を使いますが、(メモリへのアクセス順番のような)ここの意味を考える必要があります。Property と 同キストレージプロパティは、よく使われます。

コード

Thread-specific storage/Thread 向け Storage

Thread 向け Storage (pthreads ベース) も property wrapper で実装できます。(Daniel Delwood より)

コード

User Defaults

Property wrapper は、プロパティを、string-key データに変換することに使うことができます。以下、user defaults(Harlan Haskins より)の例で、
そこでは、wrapper type にデータを展開するためのメカニズムを作っています。

コード

Copy-on-write

Ref/Box

“Clamping” a value within bounds

Property wrapper types in the wild

Composition of property wrappers/ property wrapper の合成

property に複数の property wrapper が記述されたときには、wrapper は、まとめられ、いずれも効果を持ちます。例えば、以下は DelayedMutable と Copying の合成です。

コード

初期化を後にすることができるプロパティで、値をセットしたときには、NSCopying の copy を使ってコピーが実行されるプロパティになります。

合成は、後ろにある wrapper type が前にある wrapper type の内側になるように階層化されて実装されます。もっとも内側にあるタイプは、オリジナルのプロパティタイプとなります。例えば、上記の例では、DelayedMutable<Copying<UIBezierPath>>というタイプになります。path へのgetter/setter は、2つのレベルの.wrappedValueを使うことになります。

注記:この設計では、property wrapper の合成は、可換ではありません。なぜなら、順番によって影響が異なるからです。

コード

このケースでは、タイプチェックにより、2番目の順番は使えません、なぜなら DelayedMutable は NSCopying に準拠していないからです。
いつでもこのようなことにできるわけではありません。意味的におかしいだけであるならば、タイプチェックによって検出できないかもしれません。
この合成アプローチに対する別案は、”Alternatives considered”で説明されています。

Detailed design/詳細設計

Property wrapper types/Property wrapper types

“property wrapper type” は、Property wrapper として使えるタイプです。”property wrapper type”への基本的な要件は以下の2つです。

1. “Property wrapper type”は、アトリビュート @propertyWrapper と一緒に定義されなければいけません。アトリビュートは、property wrapper type として使われることを示し、コンパイラーに様々なルールをチェックするタイミングを与えます。
2. “Property wrapper type”は、wrappedValue という名前のプロパティを持たなければいけません。そのアクセスレベルは、タイプ自身のものと同じです。これは、wrap された内部の値にアクセスするときにコンパイラによって使われます。

Initialization of synthesized storage properties/同期ストレージプロパティの初期化

Property wrapper を導入することで、(setter/getterを持つ)計算プロパティと wrap されるタイプを持つ格納型プロパティを持つことになります。格納型プロパティは、以下の3つの方法のうちの1つで初期化することができます。

1. オリジナルのプロパティのタイプのイニシャライザ(例:@Lazy var foo: Int のケースでのIntにあたり、init(wrappedValue:)を使います)そのイニシャライザは、1つのwrappedValue プロパティと同型のパラメータを受け取るはずです。アクセスレベルは、property wrapper type と同じです。init(wrappedValue:) が存在するならば、プロパティ定義で使うことができます。以下は例です。

コード

複数の合成された property wrapper の場合は、その全てが、init(wrappedValue:)を用意する必要があり、最終的なイニシャライザは、全てのレベルの呼び出しを wrap します。

2. property wrapper type の値を使って初期化, property wrapper typeをイニシャライザの引数後に配置するということ

コード

複数の合成された property wrapper の場合は、一番外側の wrapper だけが、イニシャライザを持つかもしれません。

3. イニシャライザがなく、property wrapper type が引数のないイニシャライザ(init())を提供するときは、wrapper type の init() が格納型プロパティを初期化するために呼ばれます。

コード

複数の合成された property wrapper の場合は、一番外側の wrapper だけが、イニシャライザ( init() )を持つ必要があります。

Type inference with property wrappers / property wrapper でのタイプ推論

最初のプロパティラッパーが generic である時は、その generic 情報は、明示的に与えられるか、Swift がその変数定義から推測できなければいけない。その推測は以下のように行われる。

変数が、初期値 E を与えられている時は、A(wrapperValue: E, argsA…)の呼び出し結果のタイプと同じとなる、そこでは、A はアトリビュートのタイプであり、argsA は、そのアトリビュートに渡される引数である。以下は、例。

コード

もし複数のラッパアトリビュートが存在するならば、呼び出しの引数は、ネストされる。以下は、例。

コード

上記ではなく、最初のラッパー亜トリビュートが、直接の初期化引数 E…を持つならば、一番外側のラッパータイプは、A(E…)と同じタイプになる、そこでは、A は、最初に書かれたアトリビュートのタイプとなる。最初に書かれたものの後にあるラッパアトリビュートは、直接のInitializerを持っていないかもしれない。以下は、例。

コード

上記ではなく、初期化されずに、オリジナルのプロパティが、タイプ指定をもっているならば、wrappedValue プロパティのタイプは、オリジナルプロパティのタイプと同じとなる。以下は例。

コード

いずれの場合でも、最初のラッパタイプは、アトリビュートに記述された最初の指定となる。さらにいうと、2つ目以降のラッパアトリビュートについては、その前のラッパータイプのwrappedValue プロパティのタイプになる。最終的に、もし、タイプ指定があるならば、最後のラッパータイプのwrappedValue プロパティのタイプは、その指定と一致する。もしこれらのルールで推測できない、もしくは、不整合が発生するならば、その変数定義は正しくないということになる。以下は、例。

コード

その推測プロセスは、オリジナルのタイプ指定が省略されていても、オリジナルプロパティのタイプ情報を提供する、もしくは、タイプ指定から省略された generic を提供する。以下は、例。

コード

Custom attributes

プロパティラッパーは、一種のカスタムアトリビュートであり、アトリビュートの文法は、Swift で定義される要素へと参照されるために使われる。
文法的には、プロパティラッパーは、以下のように記述される。

コード

Type-identifier は、プロパティラッパータイプを参照しなければいけなく、そこに generic を含むことができる。このことにより、アトリビュート名に修正を入れることができるようになる。以下は、例。

コード

Expr-paren は、(もし存在するならば)、ラッパーインスタンスの初期化のための引数となる。

このカスタムアトリビュートの定式化は、lerger proposal for custom attributes と齟齬がなく、そこでも上記と同様のカスタムアトリビュートのための文法が使われているだけでなく、アトリビュートとして使用できるためのタイプを定義する別の方法も言及されている。このやり方では、@propertyWrapper は、一種のカスタムアトリビュートであり、コンパイル時にのみや実行時のみに有効なカスタムアトリビュートも存在すると考えられる。

Mutability of properties with wrappers

一般に、プロパティは、getter と setter をもつプロパティラッパーを持つ。ただし、プロパティタイプの wrappedValue プロパティに setter がない時やアクセスできないときには、プロパティタイプの setter もないかもしれない。

プロパティラッパーの wrappedValue プロパティが、mutating であり、struct の一部であるならば、同期getter が mutating となる。
同様に、プロパティラッパーの wrappedValue プロパティが nonmutating であるか、プロパティラッパーが class であるならば、同期 setter は nonmutating となる。以下は、例。

コード

訳注:とりあえず、ここまで

誤訳, Typo 等、お気づきの点ありましたら、教えてください。

残り

Out-of-line initialization of properties with wrappers

Memberwise initializers

Codable, Hashable, and Equatable synthesis

$ identifiers

Projections

Restrictions on the use of property wrappers

Impact on existing code

Backward compatibility

Alternatives considered

Composition

Composition via nested type lookup

using a formal protocol instead of @property wrapper

Kotlin-like by syntax

Alternative spellings for the $ projection property

The 2015-2016 property behaviors design

Future Directions

Finer-grained access control

Referencing the enclosing ‘self’ in a wrapper type

Delegating to an existing property

Revisions

Acknowledgements

コメントを残す

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