質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.50%
Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

Q&A

解決済

6回答

1837閲覧

ゼロ除算 - 例外を投げるか、先に弾くか?

Kaguya_2324

総合スコア9

Ruby

Rubyはプログラミング言語のひとつで、オープンソース、オブジェクト指向のプログラミング開発に対応しています。

0グッド

3クリップ

投稿2018/08/29 14:42

編集2018/08/31 14:11

発生している問題

Ruby(2.5.1)の勉強の一環として、計算機を作ってみようと思いました。空白区切りで数値と演算子を与え、答えを出力する簡単なものです。

in

11 + 1

out

12

足し算、引き算、掛け算に関しては特に詰まることなく実装できたのですが、第二数が0となった場合の割り算についてどのように実装するべきか戸惑いました。

考えたこと

特に別の処理をしない

最初に考えたのは、特に他の演算と別の処理を行わないことです。第二数に0が指定された場合、ZeroDivisionErrorが発生してプログラムは停止します。

calc.rb

1def calc(a, op, b) 2 case op 3 # 他演算子の処理、中略 4 when "/" 5 return a / b 6 end 7end

in

11 / 0

out

1divided by 0 (ZeroDivisionError)

先に判別し、nilを返す

次に考えたのは、先に第二数が0であるかどうか判別し、0であった場合はnilを返すことです。nilが返ってきた場合は呼び出し元でエラーメッセージを出力することを想定しています。

calc.rb

1 when "/" 2 if b == 0 then return nil end 3 return a / b 4 end

in

11 / 0

out

1ゼロで割ることは出来ません

質問

このような場合、特に触らずに例外を投げるのと先に弾くのだと、どちらのほうが良いのでしょうか。今までC++を書いていたので、予測でき回避できる例外はプログラム側で弾いたほうがいいのではと思っているのですが、言語側が詳細なエラーを投げてくれるならそのままにしたほうがいいのかなというような気もして迷っている次第です。Rubyの慣習としてこうするよ、だったり例外とreturn nilはこう使い分けるよ、だったりという意見をいただければ幸いです。

長文をお読みいただきありがとうございました。よろしくお願いします。

解決しました!

いくつもの回答を参考にさせていただいたので、質問への追記という形で回答の要約と私が取った解決法を共有させていただきます。今回、

  • Rubyでは、例外はそんなに怖がらなくて大丈夫
  • むしろ、nilを返すほうが何が起こったのかわからなくてまずい
  • 例外を投げる場合は、プログラムのバグなのかユーザー側の問題なのかわかるようにする (たとえば、例外クラスを作る)
  • 想定内の入力によって投げられた例外でプログラムが終了するのを避ける。例外をキャッチして、エラーメッセージを出す
  • 操作をする前に例外が発生することが予見できるなら、先に判別して例外を投げる
  • (異常な値を返すにせよ例外を投げるにせよ) 妥当性の確認は仕様に従い、穴がないように作る

というアドバイスをいただきました。

今回は期待する入力は数字二個の間に演算子、という仕様を想定していたので、

  • 入力が変なものじゃないかを正規表現で確認する。変なものだったら独自エラークラスを作成して投げる
  • ゼロ除算が発生するときにはreturn nilを避け、例外を投げる。計算前に、raise ZeroDivisionError if b == 0を行う
  • 例外を入出力を行っているメソッド (私の場合はトップレベルでした) で捕捉し、種別に合ったエラーメッセージを出す

といった実装にしようと思います。

最後に、回答いただいた皆様にお礼を申し上げて締めさせていただきます。皆様にご回答いただいたおかげで知見がとても広がりました。また機会があればよろしくお願いします。ありがとうございました!

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答6

0

ベストアンサー

計算機の目的・機能と「特に触らずに例外を投げるのと先に弾くのだと、どちらのほうが良いのでしょうか。」という二者択一とう制約下なら、「例外を投げる」という選択を私はします。
計算機が状態を持たないならば、case by caseを事前に検知して捌くことにあまりメリットを感じないからです。

二者択一の制約を外した場合は「事前に判断して、自分で例外を発行する」ということをします。
速度面の要件を満たせるなら、この策が防御力と対応力が高いからです。

ポイントは「状態を持つか否か」です。(計算機でいうなら電卓のメモリ機能のような)
例外の良い点は、特段自分で実装しなくともオン・デマンド(on demand)で異常を自発的に通知してくれることですよね。自分が油断しててもプログラムがフォローしてくれる。防御力が高いです。
ただし、どこで何が起きたかお知らせしてくれるだけ。お知らせに応じた対応は自分でやらないといけないのですが、そこまで油断していると、後で問題が起きたりします。例外に応じて、適切な状態に戻したり、変更したりする必要がでてくるわけです。すると例外によってcase by caseで対応が必要なってきちゃいます。だったら、先にわかるものは先に片付けておこうと思うわけで、事前判断しようと考えます。そうすれば状態を戻すようなことを考えずに済むから。例外だけだとちょっと足りないんですよね。
(昔はJavaのDBにおけるConnectionのCloseし忘れで、DBの最大接続数食いつぶすとかありましたな。)

方や事前判断で戻り値を返すとすると、呼び出し元で異常の種類を知りたいなんてことになった場合、戻り値でしかお知らせできないので、0=正常、1は警告、2は…なんて話なり、情報が足りない文字情報も欲しいなんてなれば戻り値はクラスでなんて話にもなっちゃってインターフェイスの仕様変更で面倒です。影響範囲は全部修正・テストなんてことになってコストかかっちゃいます。この点は例外の機構が助かります。正常ケースには影響を及ぼさず、別途エラークラスを作る(takahasimさんのやつ)ことをすれば、case by caseを的確に必要かつ十分な情報を添えて通知することが可能です。なので戻り値返しじゃなくて、例外を使いたくなるんですよね。例外は通例ではない例外への対応力を上げるのにとても便利なんです。
(int型の戻り値の関数で、値ごとに意味はなんだ?定数か?なんだ直書きだし定数と意味ちげぇとか。)

ということで、「事前に判断して、自分で例外を発行する」という手を私は取ります。

質問文のcalcは状態を持たないので例外で一択ですが、
双方得意・不得意があるので、合わせ技で良いところを生かすのが旨い手だと思います。
計算機がメモリ機能を持つようなら、合わせ技の出番。そうなったら考えてみてください。

投稿2018/08/29 16:38

編集2018/08/29 16:46
Hiroshi-Aoki

総合スコア804

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/29 23:54

Hiroshi-Aokiさん、回答ありがとうございます。 「状態」という言葉がよくわからずstackexchangeの質問(https://softwareengineering.stackexchange.com/q/235558)を読んだのですが、ここでは大雑把に「内部の変数」と読み替えてしまっても問題ないでしょうか? 「例外の処理は自分で行わないといけないので、事前に分かるなら例外が発生する前に弾いておく」というのは質問時の心情にとてもマッチしている解決策な気がします。事前に例外を投げるという発想にまで至れていなかったので、nilを返すという選択肢を書いたのですが、確かに事前に例外を投げてやれれば先に弾ける上に詳細なエラーを投げられて安心ですね。 戻り値でエラーを通知するようにすると収拾が付かなくなってしまうというのはまさにその通りな気がします。自前でエラークラスを作れば呼び出し元で戻り値をチェックしたりせずに詳細な出力を提供できるわけですね。 > 合わせ技で良いところを生かすのが旨い手だと思います。 詳細な回答をありがとうございます!今回は事前判別で例外を投げるという方法を取り、より機能を追加できるよう頑張ってみます。
Hiroshi-Aoki

2018/08/30 17:55

コメントありがとうございます。 「内部の変数」と読み替えてしまって問題ありません。捉え方で良い悪いといわれますが結局は変数の値ですしね。シンプルで良いと思います。 読まれた記事ですが、面白い方向で予想外のものを見つけてきましたね。 エンプラ系(企業系)だと特にですが状態遷移・管理っていうのが避けられない場合があって、大抵の人が状態遷移苦手でバグの温床になっちゃうんですよね。 だったら状態なんてもたない(不変:immutable)にしちゃうとか、これに時制を取り込んで状態遷移をプログラム自身に管理させちゃう(StreamやFilter)とか、そこまできたらロールバックの発想加えちゃえばと(Stack使って状態の保存と巻き戻し)いうことをやってきました。結果、手間がかかった分お客様の資産であるデータが破損・不整合する障害を抑制できましたし、データバグが少なさからかトラブル発生率も下がってデータ連携やシステムリプレースでの移行も計画通りスムーズに進んだりしたことを思い出しました。 記事の内容はその取り組みを始めたころを思い出す内容で、なつかしく嬉しい気分になりました。
Kaguya_2324

2018/08/31 13:34

追加の質問にも回答頂きありがとうございます。手間を払ってトラブルを抑制できる喜びが文面から伝わってきました。何か実際にトラブルが起こらないと他の人にはわからないけど、自分(自分たち)だけが裏でやったことを知ってる、というのがこっそり世界を救ってる影の実力者っぽくて高まりますね。経験談もお聞かせいただけて楽しかったです。また機会があればよろしくお願いします!
guest

0

計算機のプログラムであれば、0の入力はあり得ることなので、ゼロ除算は想定内であると言う仕様にするのが妥当でしょう。つまり、例外でプログラムが終了することを避ける。

ゼロ除算である旨のメッセージを出す方法としては、
案1:除数がゼロであるかチェックしてから除算を行う
案2:そのまま計算して、ゼロ除算の例外を投げてもらい、ユーザインタフェースを行う階層でそれをキャッチして、メッセージを出す

案1は、質問のようにメソッドの階層がある場合に、役割分担が難しくなります。
案2が良いと思います。
例外のキャッチ方法は、リファレンスの例外処理 を参照。

投稿2018/08/29 16:22

otn

総合スコア84421

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/29 23:28

otnさん、回答ありがとうございます。 > 例外でプログラムが終了することを避ける。 なるほど…!例外を投げておしまいにするか、return nilしてエラーメッセージを出すかという二択に勝手に囚われてしまっていたので、例外を投げた上で自前でエラーメッセージを出すというのは盲点でした。
guest

0

一文で

コーディングの勉強ならどっちでもいいですが、普通に電卓の実装を書くのなら例外をキャッチすべきです。


言い訳を考えてみる

自前でバリデーションをかけるのは例外を自前で実装することと同じで、ライブラリに実装済みのものがあるのに自分でもう一回実装していることに相当します。

今はゼロだけを考えていますが、文字を入力されたらどんなバリデーションを書きますか?
例えば、
1 / a
1 / 1 1
上のものはどうバリデーションしますか?
考えうるパターンを網羅しますか?
1 / ( 1 - 1 )
1 / (
はどうします?

逆に、上の考慮が必要ないケース、数字2つで真ん中に記号と確定していて、拡張は絶対にありえないのならバリデーションをかけても良いことになります。
さらに、「入力された数字同士の演算をすること」が目的ではなく、「特定のフォーマットの入力に対する加工」として捉えるのなら入力された時点で正規表現による正当性確認をすべきです。これなら例えば4桁までの数字同士の計算を簡単に強制させることができます。


一般にバリデーションを事前にかけるべきパターンとしては、例えばインジェクション対策のエスケープとかに思い至ります。
ただ、電卓にそれが必要かは不明ですし、やるにしても、ライブラリを使いますね。

投稿2018/08/29 22:39

mkgrei

総合スコア8560

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/30 00:38

mkgreiさん、回答ありがとうございます。 > 1 / ( 1 - 1 ) な、なるほど… 数字2つで真ん中に記号と思い込んでしまっていたのですが、確かにそのような入力が来たらb == 0では弾ききれませんね…。 > 正規表現による正当性確認 自分にはなかった視点です。a, bをInteger()して確認したつもりになっていました。 自前で確認するのは例外を実装し直しているのと同じ、というご意見が突き刺さりました。例外が発生しうる状況を自分で網羅出来ない限りは例外をキャッチしたほうが確実ですね。自分で仕様をちゃんと決めて、その穴を塞げるように頑張ります。
guest

0

いろんな意味で「例外」はあるわけで、それは足し算、引き算、掛け算の実装に置いてもあったはずです。

  • 記号やアルファベット、漢字が入力として使われていた場合
  • * 1 + + 3など、普通に演算できない順序で入力が行なわれた場合

たぶんこのあたりはcalcの呼び出し前に対応していたかと思いますが、この辺をどのように処理するかによって、0で除算した場合の処理も変わってくるかと思います。

また、例外を投げるにしても、ZeroDivisionErrorをそのまま投げるか、あるいは別途エラークラスを作ってそれを投げるか、という選択もあります。

結局は、アプリケーションまたはライブラリ全体の、例外の扱いを設計する必要があるのでした。

投稿2018/08/29 15:33

takahashim

総合スコア1877

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/29 23:19

takahashimさん、回答ありがとうございます。 ご指摘頂いたとおり、引数が不適な場合は呼び出し元で対処していたのですが、これも「例外」となりうるのですね。失念していました。 引数を呼び出し元でチェックせずそのままcalcに渡し、「別途エラークラスを作らずに、Ruby標準の例外を投げる」というように定めれば、ZeroDivisionErrorやArgumentErrorを投げることで例外の扱いは統一できたと言えるでしょうか?
takahashim

2018/08/30 05:35

「ZeroDivisionErrorやArgumentErrorを投げる」でもいいですが、とりわけArgumentErrorについては、コードのバグなのか、入力値のエラーなのかが判別できなくなる可能性があるのが問題になりそうです。 入力値のエラーについては、適切に入力し直すことを促すメッセージを出したいのですが、バグであれば開発者に報告することを促したい、ということになるかもしれません。そのためには、入力値のエラーについては、 class InputError < StandardError; end みたいなエラークラスを導入して、rescue InputError, ZeroDivisionErrorで入力値のエラー処理を行う、みたいなことが必要になります。 とはいえ、細かいエラーメッセージを出し分けることも面倒ではあるので、rescue StandardErrorでまるっとエラーにする、ということであれば、「ZeroDivisionErrorやArgumentErrorを投げることで例外の扱いは統一できた」ということも言えます。 このへんは目的とこだわり次第ではあるかと思います。
Kaguya_2324

2018/08/31 13:39

追加でご回答いただきありがとうございます。詳細度と実装量のトレードオフといった感じですね…。バグなのか実行中のエラーなのかわからなくなってしまうのは致命的という感触を受けるので、独自クラスを作ってやったほうがよさそうと感じました。
guest

0

C++において、例外を用いる場合の問題点として**「遅い」ということが挙げられると思います。
(他にもメモリ管理が煩雑になるなどありますが)
Rubyの場合はもともと
「速くない」言語**なので例外のデメリットが然程問題にならないように個人的には思います。
なので、Rubyでは例外を避ける理由は希薄です。

逆にnilを返す場合の問題点としては

  • 返り値のチェックを行わないと結局例外が発生する箇所が変わるだけになる。

最悪、どこでエラーになったのか分からなくなりデバッグ時に苦労する。

  • なぜnilが返ってきたのかが、外から判別できない。

例外投げた方が無難に思います。


rb

1if b == 0 then return nil end

後置if(if修飾子)を用いて

rb

1return nil if b == 0

と書くことができます。

投稿2018/08/29 15:15

asm

総合スコア15147

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/29 23:01

asmさん、回答ありがとうございます。 Rubyでは例外を避ける必要性は薄いのですね。確かにreturn nilだと例外に比べて外から得られる情報量が少なすぎますね… 後置ifはこのような場合に使えるんですね…!目から鱗でした、ありがとうございます!
guest

0

エラー種類を分類します。

A. a, op. b の妥当性チェック
B. 計算処理のチェック

A については、事前にチェックします。 a, b は数字であること、 op は +, - , * / であることなどです。

B については、例外で処理します。 0 では割り算だけでなく、 オーバーフロー、アンダーフローなどが発生する可能性があります。

投稿2018/08/29 21:52

katoy

総合スコア22324

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Kaguya_2324

2018/08/30 00:24

katoyさん、回答ありがとうございます。 計算処理は例外で処理してやるのが良いのですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.50%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問