https://www.youtube.com/watch?v=YTtVU4NlmFo
今回は A Swift Tour
の中から新たに「オブジェクトとクラス」について見ていこうと思うのですけれど、その前に、前回に話しそびれてしまったクロージャーの “Trailing Closure” についての補足からしていこうと思っています。
クロージャー周りは使い方まで含めると何気なく複雑だったりするので、前回までに聴いたことなどを踏まえてもう少し詳しく知りたいみたいなことなどがあればみんなで見ていく時間も作れると思いますので、疑問などあれば用意してきてくださいね。どうぞよろしくお願いします。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #44
00:00 開始 00:43 クロージャーの多彩な表記 01:09 複数の末尾クロージャーの扱い 01:28 クロージャーを採る引数 02:44 末尾クロージャー 03:43 前に他の引数を伴うとき 04:05 複数のクロージャーが末尾にあるとき 08:14 省略時の扱いに注意 14:03 2つ目の末尾クロージャーのラベルの扱い 15:17 UIView での使用例 17:12 質疑応答 19:57 オブジェクトとクラス 21:28 オブジェクトとインスタンス 23:23 クラスの立ち位置 24:46 クラス定義の特色 26:20 変数とプロパティーは同じ方法で定義可能 28:05 カプセル化 31:24 オーバーライド 33:50 ゲッターとセッター 35:06 組み込みのカプセル化 41:14 変数でゲッターや didSet を使う 47:45 ここまでのまとめ 48:17 練習問題 48:45 定数とイミュータブルクラス 51:05 定数の特徴 54:54 次回の展望 —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #44
はい、じゃあね、今日のお話。コメントで早速、オブジェクトとクラスにとても興味があるというコメントをいただいているので、なるべくそちらの話に早めに移ったほうがいいかなという気はしますが、やはり前回の続きとして、少しだけクロージャーの話を触れてからいきたいと思います。実は前回、全部話したつもりで語り忘れた部分があったので、それを少し補足したいと思います。
今、画面に映しているスライドは、前回説明し忘れたもので、この部分の話を前回行っていました。略していくやり方については、前回お話した通りですね。もう一度、言い忘れていたことをお話しますと、特にトレーディングクロージャー(Trailing Closure)の話です。この部分は、Swiftで仕様が変わったので、知らない方もいるかもしれないので、補足しておきたいと思います。
まず、トレーディングクロージャーとはどういうものか、おさらいから始めます。何かしらのメソッドがあって、そのメソッドがパラメーターとして関数を受け取るとします。例えば、Int
を受け取ってBool
を返す関数をパラメーターとして取る場合ですね。例えば、次のように書くとします。
func someFunction(f: (Int) -> Bool) {
// 何かしらの処理
}
このように定義して、何らかのクロージャーを渡すという形です。これでとりあえずエラーにはならないでしょう。
これをトレーディングクロージャーで書く方法について話しました。トレーディングクロージャーを使用する際、クロージャーを引数リストの外に出すことができます。トレーディングクロージャーのポイントは、最後の引数がクロージャーである場合に適用できるという点です。具体的には次のようになります。
someFunction { (number: Int) -> Bool in
return number > 0
}
このように、引数リストの外に出して書くことができます。さらに、通常の値がある時には、値はそのまま引数リストに書いて、クロージャーだけを外に書くこともできます。
この書き方は以前からありましたが、Swift 4の途中ぐらい(正確なバージョンは覚えていませんが)から、クロージャーが複数ある場合に、両方のクロージャーをトレーディングクロージャーとして外に出すことができる仕様が追加されました。具体的には次のようになります。
func anotherFunction(f1: (Int) -> Bool, f2: (String) -> Void) {
// 何かしらの処理
}
// トレーディングクロージャーとして書く場合
anotherFunction { (number: Int) -> Bool in
return number > 0
} f2: { (text: String) in
print(text)
}
このように、最初のトレーディングクロージャーはラベルを省略し、続くクロージャーにはラベルを付けるという仕様です。これにより、コードがより読みやすくなります。
トレーディングクロージャーのこの新しい仕様を使うと、例えば、以下のようにエラーハンドリングのクロージャーを定義する場合に役立ちます。
performTask { (result: Bool) in
// 成功した場合の処理
} onError: { (error: Error) in
// エラーが発生した場合の処理
}
このように、正常系のクロージャーとエラー処理のクロージャーを分けて記述することで、より直感的にコードを記述することができます。
これがトレーディングクロージャーの基本的な使い方です。この部分がSwiftの重要なポイントとなりますので、理解しておくと良いでしょう。 Swift 5.3から右からマッチングするか左からマッチングするかが変わる予定があるので、Swift 5.5時点ではまだ昔からの仕様で、右からマッチングするんですよ。クロージャーについても同様で、右のフェイラーが実行される仕様になっています。しかし、これが左からに変わるとサクセスが実行されるようになります。
現在は警告が出せるみたいな話もありますが、片推論が行われるために警告がどのように表示されるかはケースバイケースかもしれません。このケースでは問題ないとは思いますが、一律にマッチング順序が変わるため、注意が必要です。
実際に試していたときも、マッチングの問題で「これは明らかにそっちじゃないでしょう」と思うような結果を取っちゃうことがありました。それを補正するように変わってくれたら嬉しい反面、怖い気もします。違うクロージャーが実行されないように警告が出るなら少し安心ですが、ラベルを両方付けてしまった方が安全でしょうね。
テイリングクロージャーを使わないで普通にパラメーターとして指定する場合には、ラベルを省略しない方が良いかもしれません。この仕様変更が行われるならば、Swift 6.0で変わるべきですよね。破壊的変更になりますから。
特にいつ変更が行われるかは書かれていないですが、現時点のSwift 5.5ではまだ右からマッチングなので、将来的に変わるという話になっています。もしSwift 5.5でこの変更が実装されたら、多くの人が混乱するかもしれません。
これについては、ちょっとやばめな仕様変更だと感じました。ラベルは適切に使うべきですし、略したければ略すこともできますが、仕様変更がどう影響するかは検討が必要です。変わる理由をすぐに思い出せないのですが、実験してみたとき今スピーカーノートを見ていても具体例が思い出せませんでした。
とりあえず、トレーリングクロージャー省略の場合などに直感的に反するマッチングのされ方をしたことがあるので、もしかするとその辺りに修正が入るのかもしれません。期待と不安が入り混じった感じです。把握しきれていない部分もあるので、いろんなパターンを試してみてほしいと感じます。
ちなみに、フェイラーのラベルを省略することができるかどうかについてですが、確かできたような気がします。ただ、サクセスを消さないといけない場合がありますね。しかし、中に入れることを要求されることがありますね。
このように、中に入れさせようとする仕様変更は無理がある気がしますし、あまり使う人はいないかもしれませんが、こういう仕様もあるということは覚えておくべきですね。
最後に、リンクをくださった方がいて、なるほど「アニメート」というメソッドがUIView
にあるんですね。animateWithDuration:delay:options:animations:completion:
というメソッドのアニメーションとコンプリーションについての話をしていたのですね。これを使いこなすのは少し難しそうです。
とりあえず、ここまでで話し忘れた部分はないでしょうか?話したいことがあればどうぞ。アニメートのメソッドについては、最初のクロージャーがアニメーションの内容で、2番目のクロージャーがコンプリーションになっています。適切な感じですが、こういうケースはあまりないかなとも思います。 そうですね、Zoomでコメントくださってるリンクのお話をしてたんですけど、このあたりが適切に表現できるかどうか。でも、思いつかないで使わないのと、思いついて使わないのとでは全然違うので、知らなかった人や存在を忘れていた人は、実践で使い道ないかなーみたいに考えてみると面白いかもしれませんね。
じゃあ、トレーニングクロージャーのお話は、とりあえずこのぐらいにしましょうかね。結構コメントいただいてるので、ちょっと確認してみると、大体話せたかな。なるほど、APIデザインで頑張ってくださいという仕様らしいけど、なかなかうまく使えていないよう的な話らしいですね。とはいっても、やっぱり目に見えてフェイラーみたいに分かるっていうのは、分かりやすそうだなという感もありますので、研究しがいがあるかもしれませんね。
あと、省略の順番とかは、やっぱりトレーニングクロージャーは後ろから省略していくべきという発想や、そういう先入観がありますよね、確かに。だから、その先入観と実際のコンパイラーの動きを合わせていこうということなんだとは思います。まだちょっと自分はそこを把握してなかったので、後でまた見てみますね。
そうですね、ここまで結構クロージャーについて長く話してきましたけど、何か分からなかったことや新たに見つけたことがある人がいれば、ぜひお話を聞きたいなと思います。大丈夫ですかね、結構ゆっくり見ましたからね。
あと、クロージャーは結構難しいなと思うんですけど、最近の人たちは当たり前に使えるんですかね。そこもちょっと気になります。こういう世界に最初からいれば馴染んでくるのかもしれませんね。思いついたらいつでも話してくださいね。とりあえず次へ行きましょう。
続いてのセクションですが、「Swiftツアーのオブジェクトとクラス」という表題が付けられてました。なんかこの表題面白くないですか?「クラス」だけじゃなくて「オブジェクト」って言う言葉を使っているんですよね。オブジェクトっていう言葉はあんまり聞かないなと思ってましたが、それについては細かく話が出てくるのかなと期待しつつ読んだんですけど、そうでもなかったですね。
NSObject
っていうのはあるんですけどね、Swiftには。そういえばそうですね、確かに。どんなクラスも入るほうですね。もうちょっと言うと、Objective-Cクラスに親和性が高くて、Objective-Cのメッセージパッシングをできちゃうクラスですけど、NSObject
もね、とても面白いので興味のある方はぜひ調べてみてください。その話をすると長くなるので、とりあえずはやめておきますが、確かに「オブジェクト」という名前はそこで出てきますね。
「クラスオブジェクト」「インスタンスオブジェクト」とコメントをいただいてますけど、ここがまた際どいところで、『The Swift Programming Language』を読んでいくと、その本の中では「オブジェクトとクラス」じゃなくて「オブジェクトとインスタンス」という言葉が出てきて、どっちも似たようなものだみたいな感じで濁していた場所があった気がします。その時は「インスタンスオブジェクト」と呼ばずに「インスタンス」と呼ぶんですよね。でも「インスタンス」も「オブジェクト」の気がするし、クラスもオブジェクトな気がする。ただし、構造体のインスタンスは「オブジェクト」と厳密には呼ばない気もするけども、呼んでもおかしくない気もしますね、どうですかね。
大丈夫ですかね、「大丈夫ですかね」っていうのは、喋りたい人いるかなと思っての問いかけです。とりあえず、このあたりちょっと曖昧な言葉としてSwiftで存在しているなっていう感覚が自分の中であるというのを、ちょっと頭に置きながら皆さんも聞いてもらえれば、何か発見があるかもしれません。何か見つけたら教えてもらえると嬉しいですね。
とりあえず、オブジェクトとクラスが型で、オブジェクトがそのインスタンスと捉えてもらえれば大丈夫かなという気がします。矛盾が出たらまた考えましょう。
で、Swiftのクラスの定義の話がこうやって書いてあるんですけど、もう一つ面白いなと思ったのが、Swiftってどっちかというと構造体が主役な感じがするんですよ。でもSwiftツアーではクラスが先に説明されているというのがちょっと面白いなと思いました。この後に構造体が出てきます。なので、まずは今までの世の中がオブジェクト指向が一般的で、今も一般的ですよね。主流がオブジェクト指向だから、まずそのオブジェクトの説明をしてるのかなという感じで自分は捉えています。 Swift的には、オブジェクト指向の使用は減少しつつあります。そのため、クラスの用途も減ってくると考えられますが、まずはクラスの定義方法についてお話しします。
クラスの定義
Swiftでクラスを定義する場合、class
キーワードの後にクラス名を付けて定義します。プロパティやメソッドは、それぞれ変数や関数の定義と同じ書き方ができる点が特徴です。以下にその例を示します。
class MyClass {
var myProperty: Int = 10
func myMethod() {
print("Hello, world!")
}
}
これは他の言語では一般的ですが、例えばJavaScriptではクラス内のメソッド定義が通常の関数と異なる文法を取るなど、言語ごとに若干の違いがあります。
プロパティの定義
プロパティの定義は変数と同じです。たとえば、var x: Int = 10
という変数定義がクラス内でも同様に使えます。
GetterとSetter
Swift以外の多くの言語(C++やJavaなど)では、プロパティに対してGetter(ゲッター)とSetter(セッター)を用意するのが一般的です。以下はObjective-C風の例です。
@interface MyClass : NSObject {
int _x;
}
- (int)x;
- (void)setX:(int)newValue;
@end
@implementation MyClass
- (int)x {
return _x;
}
- (void)setX:(int)newValue {
_x = newValue;
}
@end
カプセル化
カプセル化も重要な概念です。カプセル化の利点は以下の通りです。
- 内部属性の保護: ゲッターとセッターを介してのみ属性にアクセスすることで、外部からの直接操作を防ぎ、内部の状態を保護します。
- 制御の容易さ: セッターでは値の検証を行うことで、一定の条件を維持することができます。例えば、負の値を設定しないように制御することが可能です。
- 拡張性: ゲッターとセッターを介して、将来的にメソッドをオーバーライドし、動作を変更することができます。
以下はSwiftでのカプセル化の例です。
class MyClass {
private var _x: Int = 0
var x: Int {
get {
return _x
}
set(newValue) {
if newValue >= 0 {
_x = newValue
} else {
print("Error: Negative value is not allowed.")
}
}
}
}
このように、Swiftを含めて多くのオブジェクト指向プログラミング(OOP)言語では、カプセル化を利用してデータが不正な状態にならないようにし、また柔軟に拡張できるようにしています。
まとめ
Swiftではクラスの定義、プロパティやメソッドの宣言が他の言語と類似していますが、独自の文法や設計思想もあります。特にカプセル化には注意を払う必要があります。次回は実際にSwiftのプレイグラウンドを使って、具体的なコードを書きながら進めていくことをおすすめします。 とりあえず、ファンクションを抜いてしまった感じですね。まあ、とりあえず、この計算は一旦置いておいて、こういう雰囲気で書いてあげることによって、例えばオブジェクト型のベースクラスに対してサブクラスのインスタンスが入っていた時に、そのオブジェクトのx
メソッドを呼んだ際、オーバーライドがちゃんと制御されてサブクラスのメソッドを呼び出せるといったことが可能になるのです。こういう風にカプセル化というのは、とても大事なものになってきます。このあたりはC++を学ぶとよく見えてくるところですが、Javaなどのオブジェクト指向がしっかりしている言語ではあまり気にする必要はないかもしれません。ただ、C++だと色々とやばいことも起こったりします。
これをSwiftらしくもう少し書くと、例えば以下のように書けます。
var x: Int {
get {
return parameterX
}
set {
parameterX = newValue
}
}
こういう書き方ができるわけです。オーバーライドも可能です。Swiftの言語構文を利用してオーソドックスなオブジェクト指向を実現しようとすると、雰囲気的にはこのような定義になります。
Swiftは非常にシンプルに変数の宣言とプロパティの宣言ができます。例えば、var x: Int
と書くだけで、値の保存場所とカプセル化の両方が自動的に処理されます。これがとても大きな特徴です。これにより、クラスを簡単に定義することができます。
JavaなどではAPIを作る際に大量のゲッターとセッターを書くことが必要になります。フィールドを定義して、それに対するゲッターセッターも同様に定義する必要があります。一方、Swiftではこういった手間を省略できるようになっているのです。そのため、内部的にはゲッターセッターが存在し、必要に応じて介入できる状況にはなっていますが、明示的に書く必要はありません。
情報の管理や操作が容易になり、結果としてクラス定義がシンプルかつ読みやすくなります。オーバーライドも簡単に行え、例えばサブクラスでオブジェクトを継承した場合でも、override var x
と書き、その上で独自のゲッターとセッターを定義することが可能です。
さらに、コンパイラが不要なゲッターセッターの最適化を自動的に行ってくれるため、不要なメモリの使用を避けることができます。この自動的な最適化は非常に便利で、手動で最適化する場合と比べて効率的です。
つまり、Swiftの言語仕様は便利で効率的なだけでなく、開発者の負担を軽減する仕組みが組み込まれているのです。
そういった意味で、以前のプログラミング言語を使用していた方には慣れるまでに時間がかかるかもしれませんが、Swiftのように洗練された言語仕様に触れると、そのありがたみを感じるのが難しくなることもあります。
まるで、昔は井戸から水を汲んでいたのが、今では蛇口をひねれば水が出るようになった状況に似ています。井戸を使っていた人からすれば、水道のありがたみを強く感じるでしょうが、現代ではそれが当たり前になっているのです。これが、プログラミング言語についても同じような移行を辿っていると言えるでしょう。ポインターなどの古い概念も同様に少しずつ淘汰されていくのではないでしょうか。 ポインターでも便利ですよね。今もちょっと使いたくなるところがあったりしますが、危なかしさもありますし、別の方法が模索されていくこともあります。そういった中で、使い勝手はどんどん良くなっていくのかなと思います。
この書きやすさやカプセル化という点では、従来の方法に比べてもかなり便利です。これはなかなか良い選択だなと思いますし、Swiftの売りの一つだと思います。普通にコードを書いていると、あまり意識しないかもしれませんが、書籍などでそういう特徴をアピールされると、確かにありがたいなぁと感じます。
さらに、面白いポイントをもう少し話しておきたいと思います。通常、セッター・ゲッターはプロパティに使うものですが、Swiftでは普通の変数でもゲッター・セッターが使えるんですよね。例えば、変数 Y
を Int
型として、ゲッターの場合は一律0を返すように設定することができます。セッターがなければ読み取り専用の変数になりますし、セッターがあれば何らかの処理を実行できます。例えば、変数 X
を変更するような書き方も可能です。
var Y: Int {
get {
return 0
}
set {
// 何らかの処理を実行
}
}
これで print(Y)
とすると、0が出力されます。Y = 7
と代入し、print(X, Y)
を実行してみるとどうなるかというと、Xが10でYが0だったのが、Yに7を代入したときにXが7になってYが0のままだったりします。一律0になるので、変数っぽく動かないように見えます。
このように、普通の変数でありながらゲッター・セッターが使えるというのはとても面白い仕様だと思います。オブジェクトのプロパティは変数と同じ書き方ができる点がSwiftの売りの一つですが、逆に普通の変数もプロパティと同じ振る舞いをすることができます。これも非常に面白いポイントです。
たとえば、メソッド内にローカル変数としても同様に書くことができます。次に、func
としてメソッドを定義したとき、このメソッドの中で var A: Int
として変数を定義し、それに対して didSet
を使います。
func exampleMethod() {
var A: Int = 5 {
didSet {
// 代入後に実行する処理を追加できます
print("古い値: \\(oldValue), 新しい値: \\(A)")
}
}
A = 10
}
こうすると、変数 A
に10を代入したことで、初期値の5が10になったとプリントされます。didSet
で追加の処理を記述することで、ローカル変数でも役に立つことがあります。
確かに、ゲッター・セッターを使うときにレビューで「それ使っちゃやばいよ」となることもあるかもしれません。特に、変数 Y
のゲッター・セッターの場合、セットしているように見えて実際には別の変数 X
をセットしているようなコードは混乱を招く可能性があります。しかし、ローカル変数で didSet
を使うことも可能だと分かったことは発見でした。
プロパティやメソッドも関数と同じように定義できること、普通の関数や変数もメソッドと同等に扱えることがSwiftの面白いポイントです。ぜひ、こうした特徴を生かしてみてください。 はい、ここまでですね。では、練習問題です。let
でプロパティを追加してみましょう。次に引数を取るメソッドを追加してみましょう。
そんなに難しい話ではありません。問題を解くというよりは、let
によるプロパティの定義とメソッドにパラメータを取ることの両方を体感してみようという趣旨です。それではやってみましょう。
プロパティも変数と同じ定義の仕方ができます。具体的には、変数というよりは定数として定義する方法です。たとえば、絶対に変わらない値をlet
として定義することで、その値が保証されることになります。以下に例を示します。
let z = -1
こうすると、z
に対して代入ができないため、z
は常に-1
が保証されるという性質を持った定数になります。これと同じことがプロパティでも同様に定義できるという話です。
また、メソッド内でself.z
を他の値に書き換えることはできません。要するに、セッターを持たず、ゲッターしか持たないプロパティのような感覚です。これをvar
でゲッターだけにする、みたいな明記をしなくてもlet
で簡単に定義できるという話です。
これは非常に便利な機能で、使い道は少ないかもしれませんが、固定させたいイミュータブルクラスを作る場合には、let
で統一すれば簡単に実現できます。イミュータブルクラスを作るなら、構造体で値型を作る方がSwift的には最適に動くので、あまり出番はないかもしれませんが、知識として持っておくと良いでしょう。
ちなみに、let
で定義した場合、変数でも同様ですが、didSet
は持てません。これは当たり前ですが、値が書き換わらないため必要ないからです。また、セッターも持てません。セッターやゲッターを使いたい場合は、var
で定義することになります。
オーバーライドする時にはoverride var
で行うのですが、クラスサブクラスが親クラスから継承されている時にoverride var z
といった形でゲッターを書くことになります。以下に例を示します。
override var z: Int {
return 0
}
これでコンパイルが通るはずですが、let
で定義されているプロパティをoverride
することはできません。これはlet
がイミュータブルだからです。
let
は値が保証される必要があるため、オーバーライドすると値が変わる可能性が出てきます。これは意味的にもおかしいので、let
はfinal
プロパティと同じ感覚で捉えると良いかもしれません。
あと、final
キーワードの他にstatic
キーワードというものがありますが、それについてはまた別途触れることにしましょう。let
はfinal var
のゲッターと思ってもらえれば大丈夫です。
次回も引き続きオブジェクト指向に関する話を具体的に見ていこうと思います。お疲れ様でした。ありがとうございました。