条件を重複してチェックするのは悪いスタイルですか?


10

コードの中で、特定の条件を何度もチェックしていることがよくあります。

簡単な例を挙げましょう。「a」で始まる行、「b」で始まる行、およびその他の行を含むテキストファイルがあり、実際には最初の2種類の行のみを処理したいとします。私のコードは次のようになります(Pythonを使用しますが、擬似コードとして読み取ります)。

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a")):
        # do stuff
    elif (line.startsWith("b")):
        # magic
    else:
        # this else is redundant, I already made sure there is no else-case
        # by using clear_lines()
# ...

ここでこの状態をチェックするだけでなく、他の関数などでもチェックすることを想像できます。

それをノイズと考えますか、それとも私のコードに何らかの価値を加えますか?


5
基本的には、防御的にコーディングしているかどうかです。このコードが頻繁に編集されているのを見ますか?これは非常に信頼性が必要なシステムの一部になる可能性がありますか?私はassert()テストを助けるためにそこに押し込むことにそれほど害はないと思いますが、それ以上はおそらく過剰です。とはいえ、状況によって異なります。
Latty

あなたの「else」ケースは本質的に死んだ/到達できないコードです。これを禁止するシステム全体の要件がないことを確認してください。
NWS

@NWS:私は他のケースを維持するべきだと言っていますか?すみません、よくわかりません。
marktani

2
特に質問とは関係ありません-しかし、私はその「アサーション」を不変式にします-行を文字列として扱い、それらに何を伝えるのではなく、(おそらくA&Bの派生クラスで)新しい「Line」クラスが必要になります彼らは外側から表しています。CodeReview
MattDavey

もしかしてelif (line.startsWith("b"))?ちなみに、これらの条件のかっこは安全に削除できます。Pythonでは慣用的なものではありません。
tokland

回答:


14

これは非常に一般的な方法であり、これに対処する方法は、高次のフィルターを使用することです。

基本的に、フィルター処理するリスト/シーケンスと共に関数をフィルターメソッドに渡し、結果のリスト/シーケンスには必要な要素のみが含まれます。

私はpython構文に慣れていません(ただし、上記のリンクにあるような関数が含まれています)が、c#/ f#では次のようになります。

c#:

var linesWithAB = lines.Where(l => l.StartsWith("a") || l.StartsWith("b"));
foreach (var line in linesWithAB)
{
    /* line is guaranteed to ONLY start with a or b */
}

f#(ienumerableを想定、そうでない場合はList.filterが使用されます):

let linesWithAB = lines
    |> Seq.filter (fun l -> l.StartsWith("a") || l.StartsWith("b"))

for line in linesWithAB do
    /* line is guaranteed to ONLY start with a or b */

したがって、明確にするために、テスト済みのテスト済みのコード/パターンを使用すると、スタイルが悪くなります。つまり、clear_lines()を介して表示される方法でメモリ内のリストを変更すると、スレッドの安全性と、並列処理の可能性が失われます。


3
注意として、このためのpython構文はジェネレータ式になります(line for line in lines if line.startswith("a") or line.startswith("b"))
12

1
+1は、(不要な)命令型の実装clear_linesが本当に悪い考えであることを指摘します。Pythonでは、おそらくジェネレーターを使用して、完全なファイルをメモリーにロードしないようにします。
tokland

入力ファイルが使用可能なメモリよりも大きい場合はどうなりますか?
Blrfl

@Blrfl:まあ、用語ジェネレーターがc#/ f#/ python間で一貫している場合、@ toklandと@Lattywareがc#/ f#の歩留まりまたは歩留まり、あるいはその両方に変換します!ステートメント。Seq.filterはIEnumerable <T>のコレクションにのみ適用できるため、f#の例では少しわかりやすくなっていますlinesが、生成されたコレクションの場合、両方のコード例が機能します。
Steven Evers

@mcwise:このように動作する他のすべての使用可能な関数を調べ始めると、それらをすべて連鎖させて構成できるため、非常にセクシーで信じられないほど表現力が高まります。見てskiptakereduceaggregate、.NETで)mapselect.NET)で、そこよりますが、それは本当に固体スタートです。
Steven Evers

14

最近、Motorola Sレコード形式を使用してファームウェアプログラマーを実装する必要がありました。時間的なプレッシャーがあったため、最初のドラフトでは冗長性を無視し、アプリケーションで実際に使用する必要があるサブセットに基づいて簡略化しました。それは私のテストに簡単に合格しましたが、他の誰かがそれを試みたとたんに激しく失敗しました。問題が何であるか手掛かりはありませんでした。それは完全に通りましたが、最後に失敗しました。

そのため、問題がどこにあるかを絞り込むために、すべての冗長なチェックを実装するしかありませんでした。その後、問題を見つけるのに約2秒かかりました。

正しい方法を実行するには、おそらく2時間余分にかかりましたが、トラブルシューティングのために他の人の時間を1日も無駄にしました。いくつかのプロセッササイクルが1日の無駄なトラブルシューティングに値することは非常にまれです。

そうは言っても、ファイルの読み取りが関係している場合、ファイル全体をメモリに読み込んでメモリで処理するのではなく、ファイルを読み込んで一度に1行ずつ処理するようにソフトウェアを設計する方が多くの場合有益です。そうすれば、非常に大きなファイルでも機能します。


「数プロセッササイクルが1日の無駄なトラブルシューティングに値することは非常にまれです。」答えをありがとう、あなたは良い点を持っています。
marktani

5

else場合に応じて例外を発生させることができます。この方法は冗長ではありません。例外は発生するはずのないことですが、とにかくチェックされます。

clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if (line.startsWith("a)):
        # do stuff
    if (line.startsWith("b")):
        # magic
    else:
        throw BadLineException
# ...

後者は明示的ではないため、後者は悪い考えです。後でを追加することを決定した場合"c"、明確性が低下する可能性があります。
Latty、

最初の提案にはメリットがあります... 2番目の提案( "b"を想定)は悪い考えです
Andrew

@Lattyware答えを改善しました。コメントしてくれてありがとう。
TulainsCórdova12年

1
@Andrew私は答えを改善しました。コメントしてくれてありがとう。
TulainsCórdova12年

3

契約による設計、一つはそのマニュアルに記載されている各機能は、その仕事をする必要があります推測します。したがって、各関数には事前条件、つまり関数の入力の条件と、事後条件、つまり関数の出力の条件のリストがあります。

関数は、入力が事前条件を尊重している場合、出力は事後条件で記述されているとおりであることをクライアントに保証する必要があります。少なくとも1つの前提条件が守られていない場合、関数は必要なことを実行できます(クラッシュ、結果を返すなど)。したがって、事前条件と事後条件は、関数のセマンティックな説明です。

契約のおかげで、関数はクライアントが正しく使用していることを確認し、クライアントは関数が正しく機能していることを確認しています。

一部の言語では、ネイティブまたは専用のフレームワークを通じてコン​​トラクトを処理します。他の人にとっては、@ Lattywareが言ったように、アサートのおかげで事前条件と事後条件をチェックするのが最善です。しかし、私の考えでは、この概念は(人間の)ユーザーの入力に対する保護に重点を置いているため、私はその防御的プログラミングとは呼びません。

コントラクトを悪用すると、呼び出された関数が完全に機能し、二重チェックが不要になるか、呼び出された関数が機能しなくなり、呼び出した関数が意図したとおりに動作するため、冗長なチェック状態を回避できます。

難しいのは、どの機能が何に責任があるかを定義し、これらの役割を厳密に文書化することです。


1

最初は、clear_lines()は実際には必要ありません。行が "a"でも "b"でもない場合、条件文はトリガーされません。これらの行を削除したい場合は、elseをclear_line()にしてください。現状では、ドキュメントを2回パスしています。最初にclear_lines()をスキップして、foreachループの一部として実行すると、処理時間が半分になります。

スタイルが悪いだけでなく、計算も悪い。


2
これらの行が別の目的で使用されている可能性があり、"a"/ "b"行を処理する前に処理する必要があります。それが必要である可能性があるというだけの可能性はありません(明確な名前はそれらが破棄されていることを意味します)。その行のセットが将来繰り返し繰り返される場合、多くの無意味な反復を避けるために、それらを事前に削除することも価値があります。
Latty、

0

無効な文字列(たとえば、出力デバッグテキスト)を見つけて実際に何かを実行したい場合は、それで問題ありません。いくつかの追加の行と、何らかの未知の理由で機能が停止した数か月後の出力を見て、理由を確認できます。

ただし、無視しても安全である場合、または無効な文字列が取得されないことが確実な場合は、追加のブランチは必要ありません。

個人的には、私は常に予期しない状況に対して少なくともトレース出力を行うことを心がけています。出力に添付されたバグがあり、何が問題だったかを正確に伝えるときに、非常に簡単です。


0

... "a"で始まる行、 "b"で始まる行、およびその他の行を含むテキストファイルがあり、実際には最初の2種類の行のみを処理したいとします。私のコードは次のようになります(Pythonを使用しますが、擬似コードとして読み取ります)。

# ...
clear_lines() # removes every other line than those starting with "a" or "b"
for line in lines:
    if ...

構造が嫌いif...then...elseです。私は問題全体を回避します:

process_lines_by_first_character (lines,  
                                  'a' => { |line| ... a code ... },
                                  'b' => { |line| ... b code ... } )
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.