https://www.youtube.com/watch?v=mBg5MCkwfP0
引き続き A Swift Tour
の「オブジェクトとクラス」を見ていきますけれど、今回はその中から 計算型プロパティー
あたりについて眺めていきます。その他にも 継承におけるイニシャライザー
や 値設定における追加処理
みたいな細々とした基礎的なところを再確認していく感じになりそうです。どうぞよろしくお願いしますね。
——————————————————————— 熊谷さんのやさしい Swift 勉強会 #49
00:00 開始 00:55 計算型プロパティー 02:32 計算型プロパティーのデータサイズ 06:01 計算型プロパティーの書式 08:30 セッター 09:44 UserDefaults の特徴 12:30 nonmutating set 15:26 mutating get 20:36 構造体で作るべきか 21:46 名前空間としての列挙型 27:57 関数と計算型プロパティーの違い 30:26 プロパティーの計算量 31:40 質疑応答 34:53 get throws 41:35 get async 44:54 謎な機能も果敢に使ってみるのを推奨 46:27 計算型プロパティーの存在意義 49:02 新しい値を受け取るときの名前 50:39 ローカル変数でも使える 53:22 クロージング ———————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #49
はい、じゃあ始めていきましょう。今日は比較的簡単な分野について話します。前回のようなオーバーライディングのような難しい話ではなく、もっと基本的なところを見ていきます。今回は「オブジェクトとクラス」の一環として見ていく形になりますが、ここで取り上げる計算型プロパティは、他の型、つまり構造体や列挙型でも使えるものですので、クラスの話にこだわらずに見ていってもらえればと思います。
それでは、計算型プロパティがどういうものかというと、関数のようなものと考えると分かりやすいかもしれません。普通のプロパティは、一般にメモリー領域が割り当てられ、そのメモリー領域を読み書きすることができます。Swiftの場合、これは保存型プロパティと呼ばれます。それに対して、計算型プロパティは保存領域を持たず、プロパティにアクセスするときに、あたかもそこにメモリー領域があるかのように読み書きできる仕組みです。平たく言うとゲッターとセッターのことです。他のオブジェクト指向言語でいうところのものと同じです。
前回か前々回にも話しましたが、Swiftのプロパティは保存型プロパティであっても、裏ではゲッターとセッターを持っています。計算型プロパティも同様にゲッターとセッターを持っていますが、どちらも似たようなもので、重要なポイントはメモリー領域が割り当てられているかどうかです。具体的にどのように違いが出るのかを見ていきましょう。
例として、構造体があります。構造体が何らかの型を内包してプロパティを持っている場合、その構造体のサイズは内包している型のデータ容量、つまりメモリー容量のサイズになります。例えば、Int64
が2つあると128ビット分のサイズになります。これでメモリーレイアウトを確認できます。
struct Value1 {
var a: Int64
var b: Int64
}
上記の構造体 Value1
では、128ビット、つまり16バイト分のサイズになります。
次に、計算型プロパティの例を見てみましょう。保存領域を持たないので計算型プロパティに変更します。
struct Value2 {
var a: Int64 { return 0 }
var b: Int64 { return 0 }
}
この場合、計算型プロパティになっているため、保存領域がなくなり、構造体のサイズが変わります。保存型プロパティと計算型プロパティの違いを確認するために、実際のメモリーサイズをチェックします。
print(MemoryLayout<Value1>.size) // 16バイト
print(MemoryLayout<Value2>.size) // 0バイト
このように Value1
が16バイトであるのに対し、Value2
は0バイトになります。この結果からも、計算型プロパティは保存領域を持たないことがわかります。
計算型プロパティの書き方についてまとめると、保存型プロパティとは異なり、ゲッターやセッターを実装し、直接保存領域を持たず、必要に応じて値を計算して返す仕組みになっています。受け取った値をどこかに設定することも可能です。 なので、よくあるとまではいかないですけど、他の保管場所、データベースとかに保管するみたいなときに、わざわざメモリー領域を持たなくても UserDefaults.standard
の中の integer(forKey: "someKey")
みたいなふうにして、Swiftの普通のメモリー領域ではなく、永続化ストレージから読んだりします。
同じように、どんな処理でもいいわけではなく、一般的には UserDefaults.standard
の同じキーに対して読み書きするのがよくある書き方です。例えば、 setValue(_:forKey:)
や set(_:forKey:)
のように書きます。このような書き方をして保存領域がないものを適切に扱う方法です。
もう少しセッターについて解説しておくと、セッターというのは何らかの値が代入されたときに呼ばれる場所です。実際にどういうときに呼ばれるかを書いてみると、例えば Value
クラスが用意されていたときに、この Value
のインスタンス a
に対して何かを代入すると、このときに UserDefaults
の中のセッターが呼ばれて代入されるという処理をするわけです。代入式で指定された値は newValue
という変数に渡されます。これをどこかに保存したり、どこかのI/Oに投げてあげたりすることで、自由に代入された値を扱うことができます。
なるほど、いいコメントですね。UserDefaults
に対して Int64
をそのまま代入して大丈夫なのかという点ですが、裏方の動きが面白いポイントです。確かこれ、NSNumber
に変換されて保存されるので、大丈夫ということになります。
他にも NSString
とかも同様に保存されると思うので、整数値(インテジャー)で入れたものでも Double
で入れても問題ありません。例えば、setValue(_:forKey:)
をあまりやらないですが、Double
型に変換して保存しても、Int
で取得するような仕組みにしても、NSNumber
ならちゃんと動くことでしょう。
仮に Value
クラス(例えば value
に代入して value.a
を表示する)で操作したときにタイプミスマッチにはなりません。UserDefaults
は数値の具体的な型についてあまり気にしていないので、最大限表現できる数値で保存・取得します。
再びいいコメントですね。UserDefaults
に値を入れるだけなら、non-mutating set
でもいけます。 これはどういうことかというと、まず最初に実際にやってみますか。今、値に書き込むということを試してみます。value2
は構造体で定義されていたため、mutating
な変数にしておかないと代入できませんでした。これをnon-mutating
あるいはimmutable
な変数にすると、代入式がセッターに対して値を入れようとするとエラーが発生します。これはSwiftの構造体、つまり値型のデータ保護の一環です。
データ保護についてもう少し掘り下げると、Swiftにおいてデータ保護がどこで行われているのかというと、この構造体の内部状態、つまりどんな値が入っているかというところになります。この計算型プロパティは保存領域を持っていないため、value2
自体の内部状態を書き換える役割を持っていません。実際のところ、書き込むときにはvalue2
の内部状態ではなく、ユーザーデフォルトを書き換えています。そのため、このセッターはmutable
のときだけに呼び出されるという制約に縛られる必要がありません。
デフォルトでは、ゲッターの方がnon-mutating
で、セッターの方がmutating
になっているという暗黙的なルールがあります。しかし、このときにセッターが自分自身の状態を変えない場合は、non-mutating
と明示することもできます。そうすると、let
で扱っていても、この計算型プロパティのセッターがnon-mutating
であるため、let
に対しても呼び出せるようになります。これにより、let
のままでも構造体であるのに書き換え可能になるのです。
ここまで話すと、次に何を話したいかが分かってくるかもしれませんが、ここでゲッターもmutating
にできるのです。これで内部変数が例えばあったとします。そのときにゲッターは普通non-mutating
なので、この中で自分自身の状態を書き換えることは想定されていません。しかし、mutating
にすると自分自身の内容を書き換えることができるようになります。
例えば、乱数でint
の0から100までの乱数を入れるようにすることもできます。ここで、ゲッターで値を読むたびに自分自身の内部状態を書き換えることができます。
色々な心配もありますが、例えばmutating
ゲッターの場合、それがimmutable
なインスタンスでは呼べなくなります。これを使うときには、var
に変えなければなりません。例えば、以下のようにします。
var x = 0
print(value)
これで、ゲッターのところでエラーが解消されて呼べるようになります。
こういった形で、mutating
やnon-mutating
を自由に付けることができます。関数と同じように、自由に設定できるのです。しかし、構造体でこれを行うことの危険性もあります。クラスであれば当たり前にできることでも、構造体に適用することは問題になることがあります。mutating
ゲッターが必要以上に危険性を含んでいる可能性もあります。
以上のように、mutating
やnon-mutating
についての理解を深めていきましょう。 なので、まああんまりね、このミューティングゲットをしたい場合とか他にもノンミューティングセットをしたいような場合、どっちにしてもこれをやるような場面って、だいたいクラスで作る気がします。なんとなくそっちの方が似合うかなという気がします。わざわざ値型をノンミューティングセットにする必要もないし、ちょっと思ったんですけど、内部ストレージがユーザーデフォルトだと値型の性質をちょっと見失ってますよね。他のインスタンスに代入すると関与しなくなる話から外れてますよね。これ、構造体で作るべきなのかどうなのかが疑問が湧いてきました。
よくあるじゃないですか、ユーザーデフォルトに値を保存するとか、最近の流行りだとプロパティラッパーを使って代入すると思うんですけど、そのプロパティラッパーを使って代入する際に構造体のプロパティに割り当てたとき、値型の性質が若干壊れていないかどうか、どうなんですかね。なんか急に不安になってきました。ずっとやってたんですけど、ほぼネームスペースのためというかそのためにストラクトを定義するのがあんま好きじゃないんですよね。そういう場合はプロパティラッパーを使うことが多いです。それ以外だと、enumのケースレスを使っている人も多いですね。中間層やサービス層といったものを設ける場合に、SwiftLintでもスタティックレットやスタティックファンクしかないようなストラクトとクラスはケースレスのenumに変えた方がいいという警告を書いています。その通りで、確かにそういう中間させるだけのネームスペースを分けたい場合は、enumでケースがない設計はいいなと思ってます。
なるほど。そうですね、例えば今回の例だと、そのままenumに変えちゃえばいいですね。こうやってストア型みたいにして、内部プロパティを持たず、あるAという値はユーザーデフォルトのAに入れる、セットするときはAに入れる、キーをAにしておく。こういう仲介を構造体よりも列挙型で、そしてスタティックを使う感じです。例えば static var A
のようにしておけば、Bも同様にして使うときには Store.A
に対して代入するみたいな。
ストラクトでスタティックバーにした場合には、インスタンス生成を無駄にしないようストラクトのイニシャライズをプライベートにしないといけませんが、これは無駄な作業です。enumを使用することでその問題を解決できるので、割といい解決策だと思います。
確かにそうですね。ストラクトだとイニシャライズできちゃうから、使い手を混乱させてしまう。だから使わせないようにするためにプライベートイニットにしてたんですよね。これは昔の王道の方法です。昔は構造体や列挙型がメソッドを持てなかったため、対応としてクラスを使用し、イニシャライザを設定することで解決していましたが、Swiftでは列挙型も機能を持てるようになったので、無駄なコードを書く必要がなくなった。一番スマートな書き方ですね。
そうですね、こうすることで値型としての性質が失われる心配もなく、列挙型がインスタンスを作れないため、ダイレクトにインターフェイスを提供するだけの時にはこれで十分です。逆に、テンポラリーとして保存型プロパティを持つ必要がある場合にはまた違った話になりますが、今ならこれで十分です。
現在のコード例では、ストアがプロパティを持てないため、確かに一部また違う話になりますが、基本的にはこれで十分だと思います。 その通りですね。こんな感じで面白いですよね。デフォルト値とかいろいろ違うけど、ここでこんな感じのいろんな雰囲気のゲッターから、ちょっと話が広がりましたけど、まぁいろいろとオッケーかなと思います。
ちょっと話が飛びましたが、戻すのが大変なので、この辺はザクザク戻していって大丈夫だと思います。
さて、ちなみにこうやってゲッター・セッターの話をしましたが、保存領域を持たずにプロパティの中で処理をするというのは、どちらかというと、関数と似た性質を持っています。例えば、INT64
を受け取って中間のX
を使わずにセットする場合などです。
set {
// 例えば、ここに保存するコードを書く
}
このセッターが関数ととても似ている感じです。同様にゲッターについても同じで、次のような感じになります。
var value: INT64 {
// 計算型プロパティになってしまうから
// あえて関数にする場合
func get() -> INT64 {
// 処理を書く
}
}
このあたりについては、SwiftのAPIデザインガイドラインにどちらを使うべきかが書いてあります。この使い分けは人の感覚にもよりますが、基本的には計算量が内部のデータ量によらず一定(オーダー1)で速やかに値を返せる場合には計算型プロパティを使います。機能的な話の場合には関数を使うべきというガイドラインになっています。
value(for key: String) -> INT64 {
// 具体的な実装
}
このように、何かしらの保存型プロパティがあった時に、プロパティが保存型である場合には、一般的にストレージからデータをサクッと取り、値を書き込む時もそのストアに素直に書き込むという使い方があります。計算型プロパティはその感覚からかけ離れてしまうと良くないので、計算量がかかるものは関数、かからないものは計算型プロパティという使い分けをしていくのがガイドラインです。
ここまでで何か聞き忘れていることや、自分が話し忘れていることがないか、コメントを全部拾ったか見直してみます。
驚き最小限の原則についても触れておきます。ミューテーティングゲット(mutating get)の話ですが、プログラマーが驚くとバグが生まれるなどの危険性があるため、可能な限りミューテーティングゲットは避けたほうが良いと思います。ただし、画像キャッシュのようにネットからロードした画像をキャッシュしておきたい時には、次に同じプロパティが参照された時にキャッシュされた値を返すという使い方でミューテーティングゲットが役に立つ場合もあります。
プロパティの計算量の話についても特殊な場合を除いて、明確に決まっていないことが多いので、その点も頭に入れて使っていくと良いでしょう。 計算量があまりに高い場合は、プロパティではなく普通のメソッドにすべきというガイドラインがありますね。APIデザインガイドラインにもそのような記載があるはずです。たとえば、JSONパーサーのようなものはメソッドで処理すべきですね。
Swiftの言語仕様は非常に柔軟でシンプルです。デフォルトのget
は非ミューティングですが、ミューティングするかどうかは実装者次第です。このシンプルさが使いやすさにつながっています。ただ、実際にコードを書く際には使い手のスキルと倫理観が重要になります。
例えば、get
にthrows
を付けることができるようになったのは新しい機能です。しかし、set
にはthrows
を付けられないのが現状です。これがやりたい事情でコードを書いていて、うまくいかないことに気づくことがあります。
この制限の裏には、SwiftUIの都合が関わっているかもしれません。Swiftの一部の機能はSwiftUIのために追加されることが多いです。
非同期処理(async/await)の話に戻ると、get
には非同期処理を付けることができますが、set
にも非同期処理を付けたいと思う場合があります。しかし、現状ではそれができません。たとえば、Actor
でプロパティを取得する場合にawait
が必要ですが、セットはすぐにできてしまいます。
非同期処理やエラーハンドリングをより詳細にプロパティに組み込もうとすると、複雑になるので、メソッドにすべきだという雰囲気があるかもしれません。
やはり、set
にthrows
が付けられないのは不便です。get
で非同期処理やエラー処理ができるのなら、set
にも同様の機能があっても良いのではないかと思います。
結局、Swiftのプロパティに関する設計には、一定の思想や制限があります。その背景には使いやすさやパフォーマンスに関する理由があるでしょう。これらの理由を深掘りするのも面白いかもしれませんね。 そうですよね、ほんとに。セットに失敗してたら、そのセットのところで教えてくれませんよね。どうしましょうかね。メソッドにすると、ちょっと重すぎる表現のときもありますし。
例えば、X
には正の数しか入らないとか、100までしか入らないみたいなとき、プロパティラッパーで実現はできますけど、プロパティラッパーでオーバーするとどうなるんでしたっけ?忘れちゃいましたね。まあ、いろいろと方法はあるけど、ここでさらっとガードできればいいのに。そう思います。
コメントにもありましたが、失敗する可能性があるセッターはメソッドにしろという主張にしかなってないですよね、現状では。まあいいや、ちょっと読んでみましょう。このあたりの書いてあるところを探して読んでみましょう。
とにかく、get throws
ができるようになったのは面白いところですよね。どんな話からこれが出たのか忘れちゃいましたけど。でもやはり、set throws
がなぜできないのかを理解してからじゃないと、この get throws
は使いづらいですね。とりあえず無闇に使わないほうがいいかもしれません。後々変な表現になってしまうかもしれないので。ただ、何事も実際に使ってみないとわからないですしね。なんとなく変な感じになりそうですけど。
さっきの「ミューテイティングゲット」も、あり得ないと思って触らないよりは、果敢にこの機能がどこで役に立つかを試してみる方が後々実りがあるので、好奇心のある方は積極的に使ってみてほしいですね。
さて、計算型プロパティについて話します。このスライドに紹介されている計算型プロパティでは、プロパティの getter
にアクセスしたときに、他の保存型プロパティの値を3倍して返しています。逆に setter
では同じクラス内にある別の保存型プロパティに、その値を3分の1にして代入しています。
一般的な例として、他の保存型プロパティを加工して使うときに、計算型プロパティを使うのがSwiftでは一般的です。他の言語だと、オブジェクト指向の観点からポリモーフィズムやカプセル化のために getter
・setter
が必須だったりしますが、Swiftでは言語が自動的にそのあたりを処理してくれるので、プログラマーが getter
・setter
を意識して使うときは、まさにある値を加工する計算型プロパティとして使うことがほとんどです。
他の言語を触るときには、getter
・setter
の意味合いが少し変わってくるかもしれませんね。 なのでSwiftしかやったことがなくて、他の言語をいじる機会があったときには、このゲッターとセッターを思い出してもらったら良いことがあるかもしれません。
あと、もう一つ重要なポイントです。ゲッターとセッターで、特にセッターは代入式で与えられた値が newValue
で渡ってくるという話をしましたが、この名前を自分で変えることもできるようになっています。例えば、このプロパティがIDだったとして、newValue
だとプロパティ名とずれていてわかりにくいなと感じるときには、名前を newID
などに変えることも可能です。このあたりは可読性にも関わってくるので、一応大事なポイントかなと思います。知っている人は当然のように知っていますが、知らない人も時折いるので、名前を付け替えられるというのは結構大事なポイントです。
計算型プロパティについての説明は以上です。もう一つ重要なポイントは、計算型プロパティはローカル変数でも使えるということです。Swiftの場合、プロパティはグローバル変数やローカル変数など全ての変数として同じように使えるので、ちょっとしたメソッド内でも利用可能です。例えば、乱数を取得するアルゴリズムを利用して計算をするときに、ロジックが複雑であった場合、スコープを作って計算型プロパティとして持たせることができます。ゲッターとして符号化のロジックを全て閉じ込めておき、後でそのプロパティを使用する、例えば print
で結果を表示するなどが可能になります。
これにより、わざわざ関数 getEB25519
のようなものを作らなくても良くなりますし、引数リストが不要なのに書きたくない場合にも有用です。計算型プロパティを入れ子にして使うことで、スマートに見えることもあります。この点を頭の片隅に入れておくと良いかもしれません。
少し時間がオーバーしましたが、今日の計算型プロパティの回はこれで終わりにしようと思います。お疲れ様でした。ありがとうございました。