15

少し前 (おそらく何年も前) に Stackoverflow で、if テストをできるだけ少なくするプログラミングの魅力について読んだことを覚えています。この質問は多少関係がありますが、受け取ったパラメーターに応じてテストによって決定された値を返す多くの小さな関数を使用することにストレスがあったと思います。非常に単純な例は、これを使用することです:

int i = 5; 
bool iIsSmall = isSmall(i);

次のisSmall()ようになります。

private bool isSmall(int number)
{
    return (i < 10);
}

これを行う代わりに:

int i = 5;
bool isSmall;
if (i < 10) {
    isSmall = true;
} else {
    isSmall = false;
}

(論理的には、このコードは単なるサンプル コードです。これは、私が作成しているプログラムの一部ではありません。)

これを行う理由は、見栄えが良くなり、プログラマーが論理エラーを起こしにくくなるためだと私は信じています。このコーディング規則が正しく適用されている場合、そのテストを実行することのみを目的とする関数を除いて、if テストは事実上どこにもありません。

さて、私の質問は次のとおりです。この規則に関するドキュメントはありますか? このスタイルの支持者と反対者の間で激しい議論が見られる場所はありますか? これを紹介した Stackoverflow の投稿を検索してみましたが、もう見つかりません。

最後に、私は問題の解決策を求めているわけではないので、この質問が却下されないことを願っています。このコーディング スタイルについてもっと聞いて、将来行うすべてのコーディングの品質を向上させたいと思っています。

4

3 に答える 3

10

この「if」と「no if」の全体から、 Expression Problem 1が思い浮かびます。基本的に、if ステートメントを使用するプログラミングと、if ステートメントを使用しないプログラミングはカプセル化と拡張性の問題であり、if ステートメント2を使用する方が良い場合もあれば、メソッド/関数ポインターを使用した動的ディスパッチを使用する方が良い場合もあります。

何かをモデル化する場合、2 つの軸について考慮する必要があります。

  1. 対処する必要がある入力のさまざまなケース (またはタイプ)。
  2. これらの入力に対して実行したいさまざまな操作。

この種のものを実装する 1 つの方法は、if ステートメント / パターン マッチング / ビジター パターンを使用することです。

data List = Nil | Cons Int List

length xs = case xs of
  Nil -> 0
  Cons a as -> 1 + length x

concat xs ys = case ii of
  Nil -> jj
  Cons a as -> Cons a (concat as ys)

もう 1 つの方法は、オブジェクト指向を使用することです。

data List = {
    length :: Int
    concat :: (List -> List)
}

nil = List {
    length = 0,
    concat = (\ys -> ys)
}

cons x xs = List {
    length = 1 + length xs,
    concat = (\ys -> cons x (concat xs ys))
}

最初のバージョンで if ステートメントを使用すると、データ型に新しい操作を簡単に追加できることがわかります。新しい関数を作成し、その中でケース分析を行うだけです。一方で、これにより、データ型に新しいケースを追加することが難しくなります。これは、プログラムに戻ってすべての分岐ステートメントを変更することを意味するためです。

2番目のバージョンは一種の反対です。新しいケースをデータ型に追加するのは非常に簡単です。新しい「クラス」を作成し、実装する必要がある各メソッドに対して何をすべきかを指示するだけです。ただし、インターフェイスに新しい操作を追加することは、インターフェイスを実装したすべての古いクラスに新しいメソッドを追加することを意味するため、現在は困難です。

Expression Problem を解決し、新しいケースと新しい操作の両方をモデルに簡単に追加できるようにするために、言語が使用するさまざまなアプローチがあります。ただし、これらのソリューションには長所と短所があります3。したがって、一般的には、拡張を容易にしたい軸に応じて、OO ステートメントと if ステートメントのどちらかを選択するのが良い経験則だと思います。

とにかく、あなたの質問に戻ると、指摘したいことがいくつかあります。

1 つ目は、すべての if ステートメントを取り除き、それらをメソッド ディスパッチに置き換えるという OO の「マントラ」は、ほとんどの OO 言語が型安全な代数データ型を持たないことに関係していると思います。 statesnts" はカプセル化に適していません。タイプ セーフにする唯一の方法はメソッド呼び出しを使用することなので、if ステートメントを使用するプログラムをVisitor パターン4を使用するプログラムに変換することをお勧めします。さらに悪いことに、visitor パターンを使用する必要があるプログラムを単純なメソッド ディスパッチを使用するプログラムに変換します。間違った方向に簡単に拡張できます。

2 番目のことは、私は、できるという理由だけで物事を関数に分割することはあまり好きではないということです。特に、すべての関数がわずか 5 行であり、他の関数を大量に呼び出すスタイルは非常に読みにくいと思います。

最後に、あなたの例は実際には if ステートメントを取り除いていないと思います。基本的に、あなたがしていることは、Integers から新しいデータ型への関数を持っていることです (Big 用と Small 用の 2 つのケースがあります)。その後、データ型を操作するときに if ステートメントを使用する必要があります。

data Size = Big | Small

toSize :: Int -> Size
toSize n = if n < 10 then Small else Big

someOp :: Size -> String
someOp Small = "Wow, its small"
someOp Big   = "Wow, its big"

式の問題の観点に戻ると、 toSize / isSmall 関数を定義する利点は、数値がどのケースに収まるかを選択するロジックを 1 つの場所に配置し、関数がその後のケースでのみ動作できることです。ただし、これはコードから if ステートメントを削除したという意味ではありません。toSize がファクトリ関数であり、Big と Small がインターフェイスを共有するクラスである場合は、コードから if ステートメントが削除されます。ただし、 isSmall がブール値または列挙型を返すだけの場合は、以前と同じ数の if ステートメントが存在します。(そして、将来、新しいメソッドや新しいケースを簡単に追加できるようにするかどうかに応じて、使用する実装を選択する必要があります。Medium など)。


1 - 問題の名前は、「式」データ型 (数値、変数、部分式の加算/乗算など) があり、評価関数などを実装したいという問題に由来します。

2 - または、代数データ型に対するパターン マッチング、より型を安全にしたい場合...

3 - たとえば、「ディスパッチャー」がそれらを見ることができる「最上位」ですべてのマルチメソッドを定義する必要がある場合があります。他のコード内に深くネストされた if ステートメント (およびラムダ) を使用できるため、これは一般的なケースと比較した場合の制限です。

4 - 本質的に代数データ型の「チャーチ エンコーディング」

于 2013-04-21T07:11:36.833 に答える
1

それは本当にコンベンションですか?フラストレーションが生じる可能性があるという理由だけで、最小限の if-constructs を削除する必要がありますか?

特に時間の経過とともに多くの特殊なケースが追加された場合は特に、ステートメントが制御不能になる傾向があります。ブランチが次から次へと追加され、最終的には、スパゲッティ コードのこの成長したインスタンスに何時間も費やし、コーヒーを数杯飲まなければ、すべてが何をするのかを理解することはできません。

しかし、すべてを別々の関数に入れるのは本当に良い考えですか? コードは再利用可能であるべきです。コードは読み取り可能でなければなりません。しかし、関数呼び出しは、ソース ファイルのさらに上を参照する必要性を生み出すだけです。すべての if がこのように片付けられると、ソース ファイルを常にスキップすることになります。これは読みやすさをサポートしていますか?

または、どこにも再利用されていない if ステートメントを検討してください。慣例のためだけに、本当に別の関数に入るべきですか?ここでもオーバーヘッドが発生します。パフォーマンスの問題も、このコンテキストに関連している可能性があります。

私が言おうとしているのは、次のコーディング規約は良いことです。スタイルは重要です。しかし、例外があります。プロジェクトに適合する優れたコードを作成し、将来を念頭に置いてください。結局のところ、コーディング規約は、何も強制せずに良いコードを作成するのに役立つガイドラインにすぎません。

于 2013-04-19T08:31:40.100 に答える
1

こんな対流は聞いたことがありません。とにかく、私はそれがどのように機能するかわかりません。確かに a を持つことの唯一のポイントiIsSmallは、後でそれに分岐することです (おそらく他の値と組み合わせて)?

が聞いたのは、 iIsSmall at all のような変数を持つことを避けるための議論です。iIsSmall行ったテストの結果を保存するだけなので、後でその結果を使用して何らかの決定を下すことができます。iでは、決定を下す必要がある時点での値をテストしてみませんか? つまり、代わりに:

int i = 5; 
bool iIsSmall = isSmall(i);
...
<code>
...
if (iIsSmall) {
    <do something because i is small>
} else {
    <do something different because i is not small>
}

書くだけ:

int i = 5
...
<code>
...
if (isSmall(i)) {
    <do something because i is small>
} else {
    <do something different because i is not small>
}

そうすれば、分岐点がすぐそこにあるので、実際に分岐しているものを分岐点で知ることができます。とにかく、この例では難しいことではありませんが、テストが複雑な場合は、変数名ですべてをエンコードできない可能性があります。

また、より安全です。iIsSmall他の何かをテストするようにコードを変更したため、またはi呼び出し後に実際に変更されたisSmallため、必ずしも小さくなったわけではないため、または誰かが愚かな変数名を選んだなどの理由で、名前が誤解を招く危険はありません。等

明らかに、これは常に機能するとは限りません。テストのコストが高く、その結果に基づいて何度も分岐する必要がある場合は、isSmall何度も実行したくありません。また、簡単でない限り、その呼び出しのコードを何度も複製したくない場合もあります。または、知らない呼び出し元が使用するフラグを返したい場合があります (ただし、変数に格納してから変数を返すのではなく、i単に返すこともできます)。isSmall(i)


ところで、別の関数はあなたの例では何も保存しません。関数の return ステートメントと同じくらい簡単に(i < 10)、変数への割り当てに含めることができます。つまり、同じように簡単に書くことができます-別の関数ではなく、ifステートメントを回避するのはこれです。またはフォームのコードは常にばかげています。またはを使用するだけです。boolboolbool isSmall = i < 10;if (test) { x = true; } else { x = false; }if (test) { return true; } else { return false; }x = testreturn test

于 2013-04-20T02:07:51.357 に答える