今回は、前回の最後で試しきれなかった オプショナルチェイニング
を伴う 演算子
の定義についてからお話ししていきますね。その次に、一般公開されている iOS 研修課題
を眺めていたら、 Equatable
適用の判断で感覚的に難しく感じたところがあったので、みんなと意見交換してみたくなったのでそれを。それらを見た後で時間があれば、引き続き 循環参照
に見られる問題点のあたりを詳しく眺めていく見込みです。どうぞよろしくお願いしますね。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #217
00:00 開始 00:09 今回の展望 00:39 代入演算子が話題に挙がった経緯 02:14 オプショナルに対応した代入演算子を自作したい 04:54 代入演算子であることの明記が必要 08:23 nil に独自の代入演算子を適用してみる 08:55 代入演算子を使わないとコードが複雑化 12:03 assignment を知らなかったとすると? 13:10 独自演算子は名前で説明できない 14:57 演算子の定義は辿りにくそう 15:42 周知の演算記号であれば独自定義も問題なさそう 18:06 独自演算子を導入してみた体験談 19:25 独自演算子を使わない方法の模索も有意義かも 21:17 Result 型とエラー処理との変換機構 22:28 独自演算子は考えるほどに避ける印象 23:21 演算子のオーバーロードは効果的 23:52 演算子はコード補完されない 24:12 既存演算子の優先順位を変更できる 25:50 独自演算子は懸念材料が多くて縁遠い印象 28:29 クロージングと次回の展望 —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #217
では、始めていきますね。今日はちょっと寄り道をしようかなと思いまして、というのも前回の最後のほうで演算子、オプショナルチェイニングに関連した演算子の話がありました。いくつかの特徴を説明しようとしたのですが、時間内にすべて話せませんでした。そこで、今日はその話から始めようと思います。
まず、前回の内容を整理してみましょう。ある型 Person
を作り、その中にオプショナル型のプロパティ apartment
を持っています。この apartment
も、別のオプショナル型のプロパティ unit
を持っています。この時点でのさまざまなインスタンスの状態について説明していきます。
まず、インスタンスを作成し、オプショナル型のプロパティにはまだ値を代入していません。この状態で、オプショナル型のプロパティに値を設定しようとする場合、インスタンスの存在確認を行う必要があります。この確認に使えるのが、!
と ?
の2つの演算子です。
例として、次のようなコードで説明します。
var john: Person? = Person()
john?.apartment = Apartment(unit: "4A")
このコードでは、john
が nil
でなければ apartment
に「4A」を代入します。john
が nil
であれば、代入処理は行わずに次の処理に進みます。
次に、自分で演算子を定義することができるという話題に移ります。まず、以下のように演算子を定義します。
infix operator <~ : AssignmentPrecedence
func <~ (left: inout Int, right: Int) {
left = right
print("Assigned \\(right) to left")
}
この演算子を使って値を代入しようとすると、次のように記述できます。
var value: Int? = 5
value <~ 10
このコードでは、value
が nil
でなければ 10
を代入し、「Assigned 10 to left」と表示されます。しかし、もし value
がオプショナル型で初期化されていない場合、これはエラーになります。
さらに、この演算子に属性を付けて特定の条件下で動作するようにすることができます。AssignmentPrecedence
グループに属している演算子には特定の意味を持たせたり、右結合にしたりできます。
precedencegroup CustomAssignmentPrecedence {
associativity: right
assignment: true
}
infix operator <~ : CustomAssignmentPrecedence
func <~ (left: inout Int?, right: Int) {
if left != nil {
left = right
}
}
これで、もし left
が nil
の場合には代入は行われず、エラーも発生しません。
以上が今日の説明でしたが、次回はさらに詳細に踏み込んでいきたいと思います。質問があれば、ぜひどうぞ。 オプショナルの話題に戻りましょう。オプショナルの武器が登場しましたね。これを使用すると、コードは正常に動くでしょう。この定義の方が良いので、変更してみましょう。今、パスが出なくなり、ここだけがプリントされるようになります。
オプショナルチェーニングと演算子をうまく組み合わせることで、コードを書く際の効率が向上します。わざわざオプショナルチェーン風のことをしなくても済むので、もっと簡単にコードを書けます。例えば、このアサインメントのやり方を知らないと、少し難しく感じるかもしれません。
オプショナルバインディングでも、リフレットバリュー(例:if let value = value
)では、演算をかけることができません。なぜなら、その値は読み取り専用だからです。また、コピーされていることも重要なポイントです。この状況だとどうにもならないです。
もしリフバード(例:if var value != nil
)を使った場合は、なおさら違う動きをするコードになります。もともとのバリューと全く別のバリューになってしまうため、いくら演算しても意味がありません。これを解消するためには、純粋に次のような方法を使うことが考えられます。if value != nil
だったら強制アンラップして演算を行う方法です。
もう少し賢い方法としては、スマートなアサインメントの仕方も考えられますが、よくわかりませんね。コピーしている場合は、そのコピーに演算を施しても、実際に望んでいる結果にはならないからです。
例えば、次のようにしてオプショナルを扱うことができます:
var x: Int? = 10
if let x = x {
print(x + 10)
}
オプショナルに対して?
演算子を使うと、中身にアクセスする構文になるため、この方法はうまくいかないことがあります。ですから、もしアサインメントを知らなければ、適切な書き方ができません。それが分かっていれば、コピーに対して結論を出すことができます。つまり、エミュータブルでコピーしてレッドで打つ(例:let value = value
)という方法だと、失敗します。
この場合、次のように書かざるを得ません:
guard let value = value else { return }
value += 10
このコードなら間違いなく動作するはずです。何か簡単な手法があればそれでも良いですが、基本的にはこれくらいしかないと思います。アサインメントを知っていれば、17行目のコードだけで済むようになるということです。
次に、オペレーターに関して考えてみたいと思います。オペレーターを使うことについてどう思うでしょうか?関数であれば、英語や日本語の文字列で説明できますが、特殊なオペレーターを使うと、記号だけでは何が行われているか分かりにくいことがあります。
そのため、一見分かりづらい部分を超えてでもオペレーターを定義する意味があるかどうかを考えなければなりません。関数のほうが分かりやすい場合もあります。特に関数型言語の普及が進む中で、初期のSwiftでは多くの複雑な演算子が提案され、その結果として、非常に読みにくいコードが増えました。
現代のSwiftでは、こうした難解なコードを避けるために、シンプルで理解しやすい方法を推奨する方向に向かっています。一部の特殊オペレーターは依然として便利であるものの、それらを使う際には注意が必要です。 確かに、パソコンのトレーニングではうまくいかないことがあるかもしれませんね。具体的に何を言っているのか見えない部分もあるかもしれませんが、一緒に話すことの大切さも感じます。また、コメントでいただいたように、特定の分野で意味がしっかり定まっている演算子であれば、混乱しにくいです。そのため、名前付けに関するAPIデザインガイドラインを考慮することが重要ですね。「6文字の演算子だからダメ」とかではなく、「この名前だから演算子より関数の方が良い」といった具体的な理由があるかもしれません。
例えば、通常の人が if is less or equal
といった演算子を定義するとき、これをファンクションとして定義する方法も考えられます。この方が、実際に使う際にはいろいろと省略できます。ただし、より丁寧に書く必要はありますね。is less or equal
というと意味が保管されますが、こちらのほうが名前が良いと感じるかもしれません。
たしかに、見慣れていないと分かりづらい部分はありますが、慣れていくと理解が深まるものです。また、エラーをスローするオプショナルの使い方についても検討が必要です。セッションプログラムから派生する新しい試みもあり、興味深いと思います。任意の演算子を使うことが適している場合も多々あるでしょう。
例えば try
を使ってエラーをスローする方法や、リザルト型をうまく活用する方法もあります。リザルト型にはいくつかのイニシャライザーがあり、エラー制御のためのスローを含むことも可能です。制御を一気に倒すためには適した手法ですね。リザルトを用いることでコードがエlegantになり演算子や関数の選択が鍵となります。
最終的には、演算子を定義して使う方法がスマートでシンプルに書けることがわかりました。右辺にエラーが含まれている場合も、これに対応できるコードを書くことが可能です。全体として、説明を受けることで演算子の利点が明確になり、次第に納得できるようになります。 それを見ると、このエラーが返りそうだと感じるんですよね。単なる感覚に過ぎないですけど、Nilだったらそれっぽいエラーが出そうな気がします。下手をすると、ランタイムエラーが投げられそうな気もします。
言われてみると、確かにその通りですね。右辺にfatalError
のようなものが来るイメージでしょうか。そんなイメージが浮かびます。それも一つの解釈としてはありえるかもしれませんね。右に添えたエラー型が示す内容で、ランタイムエラーが発生するという解釈も、完全にないわけではないです。
演算子は本当に難しいですね。いろいろと問題を含んでいるので、考えると使わなくなることが多いです。私も昔は独自に演算子を定義して遊んでいたことがありましたが、次第に全然やらなくなりました。C++
の頃はひたすら演算子を独自定義していましたが、あれはどちらかというとオーバーロードですね。
演算子って並びが固定されるので書きにくくもあります。普通に使える記号は多いですが、一発で入力できない記号もありますね。
例えば、演算子を使うとき、一つの例として1 + 1 * 0
は1
になりますが、順番を変更できるインフィックスオペレーターの場合、グループ化が行われて((1 + 1) * 0)
となってしまい、結果は0
になってしまいます。これは非常にややこしく、あまり使いにくいですよね。普通のライブラリをインポートして使用する際にも、意図しない動作を引き起こすことがあります。
たとえば、あるライブラリが独自の優先順位を持つ演算子を定義していた場合、こちらが意図していない順序で実行されてしまう可能性があります。こういうことが起きると、実装が非常に複雑になり、予期しないバグが発生する可能性があります。
さらに、型の違いやオーバーロードによっても問題が生まれます。異なる型に対して同一の演算子をオーバーロードしている場合、その意図する動作が複雑になることがあります。そのため、個人的には演算子を使うのはあまり好ましくないと感じています。
今日の話では、「アサインメント」というキーワードにも触れました。ただ、演算子を適切に使う場面が見つかれば、非常に有用なキーワードとなります。ですので、頭の片隅に置いておいて、将来使いどころが出てくるかもしれません。
今回の勉強会では、iOS開発における演算子の使い方についても話しましたが、次回に持ち越しても良いかもしれません。それでは今日はこの辺で終わりにします。お疲れさまでした。ありがとうございました。