今回は、これまで見てきた assertion
と precondition
の最後のところで軽く触れられている fatalError
まわりを眺めてみます。それの他にはこれまで見ていたところで出てきた Never
あたりもおさらいしてみようと思ってます。どうぞよろしくお願いしますね。
———————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #207
00:00 開始 00:20 今回の展望 01:25 fatalError の特徴 01:55 fatalError ばかりを使っているかもしれない 03:30 fatalError のメッセージもオートクロージャー? 04:45 リリースビルドで fatalError を使ってみる 07:40 API のつくりが不思議 10:41 OS 寄りの機能になるのかもしれない? 13:46 #line は ExpressibleByIntegerLiteral で受けられる 16:24 fatalError の利用場面 17:41 純粋仮想関数的なものを意味するときによく使うかも 19:53 仮想関数の実装に assertion を使うのはアリ? 21:22 オブジェクト指向とプロトコル指向 24:34 抽象クラスは極力避ける 28:48 Never という戻り値の型 30:00 戻り値が Never でもエラーは返せる 30:25 Void は空の戻り値を返す 32:23 Void は () の型エイリアス ————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #207
今日は最後の部分ですね。アサーションとプレコンディションの最後でもあり、「The Basics」の最後でもある、前提条件と最適化についてのお話をします。
前回もスライドを見ましたが、タイトルにあまり意識を向けずに話を進めていたので、改めて見ると最適化オプションによって実行されるアサーションやプレコンディション、そしてフェイタルエラーが突然現れたように感じます。私の意欲の問題かも分かりませんが、スライドが少し微妙に見えます。
最適化オプションによって無視されるアサーションやプレコンディションに関する話は、前回の内容を思い出してもらうとして、今回は突然出てきたフェイタルエラーについて見ていきましょう。フェイタルエラー関数は、最適化の設定に関係なく、既に実行を中断する、という風に書かれています。つまり、前提条件と最適化の最上位に位置するような感じになっています。
フェイタルエラーについて聞いて思い出したのですが、私自身はアサーションとプレコンディションをあまり使っていません。ただ、フェイタルエラーはよく使っていました。フェイタルエラーは完全にパラメーター3つ(メッセージ、ファイル、ライン)で構成され、コンディションなしで状況を判断し終えた上でエラーを発生させる、という感じです。
アサーションやプレコンディションと似ている点もありますが、使いどころが難しいと感じる部分もあります。自分のコードにアサーションやプレコンディションを使うことで、コードの雰囲気が変わって良いかもしれないと思いましたが、それも踏まえてフェイタルエラーを再度見てみましょう。
フェイタルエラー関数の定義を見てみると、オートクロージャーになっています。オートクロージャーの意味があるのか、少し疑問を感じますね。確実にエラーが発生する場合、クロージャーによる遅延実行の意味があるのかどうか。コンパイルオプションでメッセージが無視されることがあるのか、リリースビルドでメッセージが表示されないことがあるのか検討してみましたが、特に見当たりませんでした。
メッセージを表示してエラーを発生させる部分について試してみます。リリースビルドでもメッセージが表示されるかどうか確認しましたが、特に問題なく表示されました。オートクロージャーに意味があるのか、少し疑問ですが、インターフェースを統一するために揃えたのかもしれません。
フェイタルエラーは常にエラーを発生させますが、プレコンディションやアサーションは場合によって無視される点が異なります。インターフェースを統一した理由かもしれませんが、違和感を感じる部分でもあります。いずれにせよ、今後アサーションやプレコンディションの使いどころを意識してコードを書くと、より良いコードになるかもしれませんね。 統一感もそうですけど、ラインでUintを取っているところとか、何回も言っていますけど、これは何でしょうね。誰が作ったんだろう……なんとなくすっきりしないですね。このアサートという機能はモジュールと言わないかもしれないですけど、この空間の中に含まれて定義されていて、この中でアサートとプレコンディション、アサーションフェイラーとプレコンディションフェイラー、あとフェイタルエラー、これらが5つだけ定義されているところなんですが、なんとなくバランスが変な感じです。でもここにまとめられて、これを作った人が独特の価値と考えていたのかな、みたいな解釈もできなくはないです。
いずれにしても、僕個人的には気になる問題です。問題と言えば言い過ぎかもしれませんけど、なぜフェイタルエラーがオートクロージャーで受け取るのかという点です。さっきのコンパイルのさまざまなオプティマイズ設定にしても無視されないのに、メッセージがオートクロージャーである必要性を感じません。これについて何か思い浮かびますでしょうか……まあ、そうなってるならしょうがないですね。悪影響も特にないですし、不思議な存在感という感じです。このアサーション周りについては。
フェイタルエラーがオートクロージャーの理由は、元々はインターフェイスの共通化、つまりアサートとフェイタルエラーでインターフェイスを共通させたほうが分かりやすいのだと思います。やっぱりそんな感じのほうが理解しやすいのでしょうね。疑問を持たせるインターフェイスよりも統一化するほうが価値があるのかもしれません。シャープラインとかシャープファイルとかを見ると、これはどちらかというとXcode側の言語仕様に関連しているようです。プログラマブルインターフェイスでの設計になっている感じがします。他の言語とはちょっと違うエラー処理の特性が見られますが、これはXcodeの独自機能として設計されているのではないでしょうか。
ちなみに、シャープラインがUintで受け取られるというのはリテラルなわけですよね。リテラルのコンバージョンで変換できるのかどうか、ちょっと試してみますか。多分普通にストローグとかあって、前代わりにどうかある人とかいますかね。先回りでできるよとか言ってくれて大丈夫ですからね。とりあえずどうもありがとうと言いますか、このシャープラインがIntに向けないし、Int8とかに向けないし、その仕様が一応特殊ですよね。
全体的に見ると、すごく組み込まれたXcode独自の機能に強い感じがします。それにより設計がアプリケーションよりになっているのかもしれません。個人的な見解じゃ決めかねるところですが、実際にどう動作するか試してみるのも一つの方法です。 特定の型に変換できないというのは、明らかにその型に変換されているということです。ここで IntegerLiteralConvertible
に準拠してイニシャライザーでインドでも置いておきます。この状態でコンパイルが通るか試してみると、通りますね。次に、課題として lowValue
が String
としてあると仮定し、イニシャライザーで lowValue
を String
から 16進数の形で代入してみます。これで表示させると、「8」となるはずです。
具体的なコード例を示すと次のようになります。
let hexValue = String(16, radix: 16)
print(hexValue) // 出力: "10"
これでインテジャリテラルの標準的なバージョンで、とりあえず正常に動作しました。次の段階で、このような場面で使えるかについて考えます。プログラムの試作中にエラーが出た場合、例えば fatalError("Unimplemented")
を使うと良いでしょう。これは実行中断を確実に行えるためです。
fatalError
を使う場面として、抽象クラスを作成する時などが挙げられます。例えば、オブジェクト指向設計において抽象クラスを作り、メソッドの具体的な実装はサブクラスでオーバーライドする場合です。その際、未実装のメソッドが呼ばれると fatalError
で中断させることで、実装漏れを防ぎます。具体的には、次のように記述します。
class AbstractClass {
func someMethod() {
fatalError("This method must be overridden")
}
}
サブクラスで someMethod
をオーバーライドしないと、実行時にエラーになります。
最近では、安全性を重視し、プロトコルを使うことが増えました。プロトコルではメソッドの宣言のみが行われ、具象クラスで必ず実装することが求められるため、未実装のメソッドが実行されることはありません。この点で fatalError
の使用頻度は減少しています。
たとえば、次のようなコードが考えられます。
protocol SomeProtocol {
func someMethod()
}
class ConcreteClass: SomeProtocol {
func someMethod() {
// メソッドの実装
}
}
このように設計することで、未実装のメソッドが呼ばれる心配がなくなります。この方法のほうが安全性が高いため、推奨されることが多いです。
以上が、fatalError
を使う場面とその代替手段についてです。現在ではプロトコルを使った設計が一般的ですが、特定の状況では fatalError
が役立つ場合もあるため、適切に使い分けることが大切です。 こうやってツインして考えると、プロトコルとオブジェクトCは基本的に実装が必ず存在するという感覚で作られています。しかし、プロトコルは基本的に実装がないという感覚で作られており、必要に応じて実装をさせるよ、という感じがすごくいいですね。必要に応じてアクションを搭載してあげるという感じで、これを規定の実装として言うのを書かなくてもいいわけです。実装がなければ自分で書かなきゃいけないことが強制されますが、基本の振る舞い方があるという意味では、ベースクラスとサブクラスの話に似ています。
この点が非常に面白いです。アプローチが全然違うから、プロトコルに慣れるまでちょっと苦労するかもしれませんが、慣れるとやはり上手に作られています。もともと、プロトコルというかインターフェイスはオブジェクト指向の欠点を解消するために発展してきたものです。今現在のプロトコル指向という形に至っているのは、何というか感慨深いものがあります。非常に良いですね。
次に、フェイタルエラーについてですが、大した話すことはなかったですね。あとは、話の中で出てきたネパーですが、これも見ておきたいと思います。実際に以前にも何回か触れていますが、せっかくの機会なのでネパーとは何かというのを残りの時間で見ていこうと思います。
他に話しておきたいことや聞いておきたいことがあればお伝えください。アブストラクトクラスはオブジェクト指向が前提の言語環境でよく使われます。特にJavaはインターフェイスが出てくるのが若干遅かった言語ですから、そのためにアブストラクトクラスが前提になりがちですよね。確かにC#もアブストラクトクラスがあります。でも、今は完全に絶滅しているかと言うと、そこまでは行かずにまだ存在しています。確かに、オブジェクト指向に向いている分野だと、アブストラクトクラスは非常に便利です。
バーチャルといったキーワードで、C++にも似ていますが、Javaなどでも「abstract class」というキーワードを使いますね。こうした機能がある言語では、フェイタルエラーを書かなくても済むわけで、インターフェイスがあまり活用しづらくなることもあります。実装を完全に省略するという立ち位置が、中小クラスとかなり似ていますが、用途によってはそっちの方が便利です。
言語のサポートがある場合、中小クラスが非常に重要になります。中小クラスがサポートされている場合、かなり良い線で狙っていけます。また中小クラスがある限り、多重継承が欲しくなりますが、今のプロトコル指向に似たような感じで、継承関係の問題は非常に面白いですね。 いざ抽象クラスと多重継承が混ざってくると、「じゃあ具体的にどこが違うんだろう?」って説明するのが難しい気がしますね。でもまぁ、その話はまた別の機会にしましょう。Swiftにはこういった機能がないので、別の考え方で抽象クラスを実現しています。それに対する言語サポートがないため、さっき紹介したようなフェイタルエラーを使って抽象クラスを実現するのは極力避けるべき、できれば絶対に避けるべきです。使わなくても済む方法を考慮すべきという感じですね。
フェイタルエラーも使う機会はあると思いますけど、プレコンディションフェイラーやアサーションフェイラーの存在を意識すると、フェイタルエラーを使う場面は減ってくるかもしれないですね。
さて、ここで時間が足りないかもしれませんが、簡単に「Never型」について紹介します。Swiftの標準ライブラリには「Never」という型が存在します。この「Never型」は通常リターンタイプで使われ、何も返さないことを意味します。エラーを投げるかトラップするかのときに使われることが多いです。Never型は戻り値を返さない、つまり絶対に先へ進まないという特徴があります。エラーを投げると通常は復帰できますが、エラーが投げられた時点でその先のコードには進まないということを示します。
一方で、「Void型」は値を返さない関数の戻り値として使用されることがあり、通常の操作フローの中で使われます。どちらも「何も返さない」という点では共通していますが、Never型はコンパイラーに対して「ここには到達しない」と通知する特徴があります。
全ての関数が何らかの戻り値を持っていますが、Never型は例外的に戻り値を持たないことを示します。これは「Void型」が省略されている場合に混乱を招くかもしれませんが、実際はすべての関数がVoidもしくは具体的な型を返しています。
以上が簡単な紹介ですが、Never型に関する詳しい話題はまた別の機会に取り上げましょう。さて、時間にもなりましたので、これでNetafixの周りの話は終わりにします。次回は、さらに先の内容について見ていこうと思いますが、順番を変えてウィークや順次参照に関連する話題を取り上げる予定です。興味があれば次回もぜひ参加してください。
今日はこれで終わりにします。お疲れ様でした。ありがとうございました。