web-dev-qa-db-ja.com

誰も自分のコードを理解できない場合の悪い兆候?

コーダーが他の誰も理解できないコードを記述し、コードレビューが常にレビュアーが頭を掻いたり、頭を手で握ったりして終了する場合、これは、プロのプログラミングのためにコーダーが単純に切り取られていないという明確な兆候ですか?これは転職を正当化するのに十分でしょうか?この業界では、わかりやすいコードがどれほど重要ですか?

これらのCの例を検討し、比較してください。

if (b)
  return 42;
else
  return 7;

対:

return (b * 42) | (~(b - 1) * 7);

後者の例を採用する言い訳はありますか?もしそうなら、いつ、そしてなぜ?

編集:検討のために元の後者のスニペットを残し、修正を追加します。

return (b * 42) | ((b - 1) & 7);

他の興味深い所見は、bがtrueの場合は1、falseの場合は0である必要があるということです。他の値を使用すると、奇妙な結果が得られます。

52

専門のソフトウェアエンジニアの最初のルールは、わかりやすいコードを記述することです。 2番目の例は、古くて最適化されていないコンパイラー、またはたまたまビット単位の演算子で自分自身を表現したい人のための最適化された例のように見えます。ビット単位の操作に慣れていれば、何が起こっているのかは明らかですが、それが当てはまるドメインにいない限り、2番目の例は避けてください。また、最初の例では中括弧が欠落していることも指摘しておきます。

コーダーは彼のコードが効率的であると主張するかもしれません、それは事実かもしれませんが、それが維持できない場合、それは途方もない技術的な負債を生み出すので、まったく書かれないかもしれません。

107
World Engineer

あなたが提起するいくつかの質問があります。

1)これは、プロのプログラミングのためにコーダーが切り取られていないという明確な兆候ですか?

  • いいえ。開発者は多くの場合、アイデアについて学び、それを適用したい段階を経ます。彼らは常にこれらのアイデアを効率的および/または効果的に適用していますか?いいえ。間違いはあり、それは学習プロセスの一部です。彼らが一貫して理解できないコードを書いている場合、より良いコミュニケーションが必要です。レビュー担当者は、受け入れ可能なコードの期待値を伝え、コーダーは、コードの動作(想定)と、必要に応じて特定の方法で行われた理由を伝える必要があります。

2)キャリアの変更を保証するのにこれで十分でしょうか?

  • これに触れないで。

3)この業界では、わかりやすいコードがどれほど重要ですか?

  • 極めて。コードが理解できない場合は、何が行われているか、何が行われていないかがわかりません。つまり、正常に動作しているかどうかを知る方法はありません。

4a)Cの例:

  • 引用されたCの例は同等ではありません。 2番目のケースは、bが0と1の値に制限されている場合にのみ、最初のケースと同じ結果になります。ただし、-bに対して173,406,926の値をスローすると、、結果は一致しません。

4b)後者の例を採用する言い訳はありますか?

  • はい。多くのプロセッサは分岐予測とパイプラインを採用しています。誤って予測された分岐のコストにより、許容できない遅延が発生する可能性があります。より確定的な応答を実現するために、ビットツウィドリングを使用して処理をスムーズにすることができます。

  • 2番目の例に対する私自身の見方は、それは不必要に複雑であり、明確にするために再加工する必要があるということです。さらに、乗算は(アーキテクチャによっては)他の演算と比較すると遅い場合があるため、私は好きではありません。最も可能性が高いのは、次のような形式のものが見たいです。

    return b ? 42 : 7;
    
  • ただし、状況に応じて(そして、重要なカテゴリで実質的により良い結果が得られたことが実証できた場合)、私はmightマクロを受け入れますコードをレビューするときに適切に説明する名前。例えば:

    /*
     * APPROPRIATE_MACRO_NAME - use (x) if (condition) is 1, (y) if (condition) is 0
     *
     * Parameter Restrictions:
     * (condition) - must be either 1 or 0
     * (x) and (y) are of the same integer type
     *
     * This macro generates code that avoids branching (and thus incorrectly
     * predicted branches).  Its use should be restricted to <such and such>
     * because <why and why>.
     */
    #define APPROPRIATE_MACRO_NAME(condition,x,y)  \
        (((condition - 1) & ((y) - (x))) + (x))
    

お役に立てれば。

84
Sparky

2番目のコードは42または7を返しません。

for b = 1:
  (1 * 42) | (~(1 - 1) * 7)
  42 | (~(0) * 7) 
  42 | (-1 * 7) 
  42 | -7
  -5

for b = 0:
  (0 * 42) | (~(0 - 1) * 7)
  0 | (~(-1) * 7) 
  0 | (0 * 7) 
  0 | 0
  0

それでも、投稿したときにはそうであると思っていました。そのため、複雑なコードは避けなければなりません。

ただし、次のような「正しい」コードを例にとります。

return ((b - 1) & (7 ^ 42)) ^ 42;

それが役に立つかもしれないと私が考えることができる2つの理由があります。 -作成中のアーキテクチャは、分岐命令または述語命令をサポートしていません。 -あなたが書いているアーキテクチャには、分岐操作を通過しても機能しないか、分岐予測ミスに関連する法外なコストがあるパイプラインがあります。

この場合、次のように記述します。

/* 
   This code is equivalent to:

   if (b)
      return 42;
   else
      return 7;

   when b=0 or b=1

   But does not include a branch instruction since a branch prediction
   miss here would cause an unacceptable impact to performance. 
*/

return ((b - 1) & (7 ^ 42)) ^ 42;

ただし、説明や根拠のないそのようなコードが表示される場合は、ソースコードが難読化されている可能性があります。ソースコードの難読化(バイナリコードの難読化とは対照的に、多くの場合正当な目的があると見なされます)は悪意のある傾向があります。一部のプログラマーは、さまざまな理由で、時には仕事の安全性のために、時には何が行われるか、そして自我、不安感または不信のために物事がどのように行われるかをより詳細に制御するために、領土を持つことができます。しかし、ほとんどの場合、同僚や雇用主の利益に反します。リーダーの管理スタイルと個々のプログラマーが対応する方法に応じて、相互信頼を築くか、恐れを植え付けるかに関係なく、そのような行動を後からではなく早期に根絶することは、チームの責任者の責任です。

38
Donscarletti

奇妙なことに、(b * 42) | (~(b - 1) * 7)を書いている人は、経験や知識がありそうなふりをしようとするプログラミングについてほとんど何も知らない人、またはプロジェクトを妨害しようとしている人です(つまり、経験が豊富で知識が豊富で知性があり、雇用保障が欲しい)。

最初のタイプの人は、NOT、OR、操作の順序などの使い方を知っていることを示したいと思っています。知識を見せびらかしていますが、悲しいことに、2つの乗算が必要なため、はるかに効率の悪いコードを書いています。 、減算、not、およびorは、比較、カップルジャンプ、およびリターンよりも明らかに非効率的です。

その場合、彼らは(まだ)業界に参加するに値しませんが、彼らのデモンストレーションは、彼らが基本的なコンピュータロジックを知っており、いつかショーボートを通り過ぎて効率的なコードを書き始めたら、貴重なリソースになる可能性があることを証明します。また、bが必ずしも0または1ではない可能性があり、まったく異なる値が返されます。これは、見つけるのが難しいかもしれない微妙なバグをもたらす可能性があります。

2番目のタイプの人は、このようなコード(または他の多くの不正なタイプのコード)を挿入して、人々がコードについて質問し続けるので、価値がありすぎて失うと考えています。このタイプのサボタージュは最終的にプロジェクトに悪影響を及ぼします。彼らは彼らの教訓を学び、最適化された読みやすいコードを書くまで、すぐに放たれるべきです。以前のようにbが1または0にならない可能性もあります。つまり、予期しない値(42または7)が返され、不幸なプログラマーがif(b) ... else ...に戻すまで正しく機能する可能性があります。そして、コードは突然動作を停止します。たとえば、多分これは非常に高価なifステートメントに偽装した疑似番号ジェネレーターです。

読みやすいコードが重要であるだけでなく、このようなロジックバグのないコード(実用的な限り)も重要です。しばらく真剣にコードを書いている人なら誰でも、これがいかに重要であるかを知っています。 Tic Tac Toeの完全に機能するゲームを作成します。ここで、このコードを1年間取っておき、それから戻ってコードを読んでみます。コーディング標準、コメント、ドキュメンテーションなどを考慮せずに手短にそれを書いた場合、おそらくあなたが書いたコードがあなたが入力したものであることにさえ気付かないでしょう。更新する必要がありました。

複数の開発者が共同で作業する場合は、コードを読みやすくすることがさらに重要です。オッズはそうなので、そのコードを維持することはできません。あなたは他のことに移るでしょう、そして誰かがあなたのコードを維持しなければならないでしょう。逆に、別のプロジェクトを継承する場合、コメントやクリーンなコードを残す前に、開発者が作業できるようにしてください。信頼性の高いコードに取り組んでいるプログラマーは、読みやすさやコメントを含め、コードを保守しやすいように記述します。

パフォーマンスも重要ですが、読みやすさを上回ってはなりません。少なくとも、ここで2番目のコードブロックを使用した場合、従来の方法ではなく、なぜこのようにしたのかを明確に説明する長いコメントブロックが必要です。その時点で、正当な理由がない場合は、より一般的なコードに置き換えることをおそらく決定します。実際、それが論理爆弾である場合、私はそれをより長い方法で書き直します。そのため、バグを回避するために、その機能は実際に何が行われているのかを明確に文書化する必要があります。

結局のところ、偶然にもこの正確なアルゴリズムが必要であることが判明しているいくつかの特殊な問題の正当な使用法があると確信しています。もしそうなら、私はこのロジックを使用するアルゴリズムを説明するコメントを期待します、そしてそれはif-elseブロックより良い何かのためにあった方が良いでしょう。特定の例に適切なif-elseブロックは、if(b) return 42; return 7;(elseはオプション)とreturn b? 42: 7;(コーディング基準で完全に禁止されていない限り、3項演算子は問題ありません)の2つだけです。他のものは非常に複雑であり、より単純なステートメントを削減する必要があります。

私は時々、後輩がすぐに理解できない「トリッキーな」コードを書いていることに気づきますが、私は通常そのコードにコメントを付けて、なぜコードがそのように書かれたのか理解できるようにします。最適化されたコードは読みにくい場合がありますが、それでも必要な場合もありますが、これらは規則ではなく例外である必要があります。

偶然にも、このようなコードの完全に受け入れられる場所があります。難読化コンテスト。その場合、コードが本当に賢く、CPUを浪費する分岐であるか、または疑似乱数を生成するためのより不正なデバイスである場合は、PI(または他の魔法の数)、またはおそらく弱い暗号化アルゴリズムです。

6
phyrfox

if/then/elseのブランチに問題がある場合は、次のようなものに切り替えるのがおそらく最も簡単です。

static const int values[] = {6, 42};

return values[b!=0];

これは実際に機能し、if/then/elseよりも読みにくい場合がありますが、Cを知っている人や、まったくC++。

これは汚いトリックであると考える必要がある人、または特定の言語で特に緩い型チェックに依存している人のために:はいといいえ。記述されているように、コードはCおよびC++で「false」が0に変換され、「true」が1に変換されることに依存しています。

ただし、基本的な考え方は、型システムが大幅に厳格な言語を含め、他の言語にも同様に適用できます。たとえば、Pascalでは、同じ基本的な考え方は次のようになります。

var
    values : array [boolean] of Integer;

(* ... *)
values[false] = 6;
values[true] = 42;

(* ... *)
f = values[b<>0];

Pascalユーザーマニュアルとレポート(2nd キャスリーンイェンセンとニクラウスワースによる版)は、§6.2.1や§7など、多くの場所で配列へのインデックスとしてブール値を使用することを示しています。 Pascal(最初に定義されたとおり)には初期化された変数の概念が含まれていませんが、含まれている場合、初期化子はCおよびC++で行われるのと同じ順序になります(false < true)。

Adaは少し異なる構文を使用しますが、同じ基本機能を提供します:

Temp    : array(Boolean) of Integer := (false => 7, true=>42);

-- ...

Return_Value = Temp( 0 /= i);

したがって、特に緩い型チェックを使用している1つの言語の偶然に過ぎないものは扱っていません。 Pascalと(特に)Adaは、特に強く型付けされていることでよく知られていますが、どちらもCやC++と同じ基本構成をサポートしており、基本的に同じ方法でサポートしています。

4
Jerry Coffin

次のようなものは、コードのINTENTをより明確にするための方法になります。

manifoldPressureFloor = (b * 42) | (~(b - 1) * 7);

return manifoldPressureFloor;

manifoldPressureFloorは完全に構成されています。もちろん、元のコードが実際に何であるかはわかりません。

しかし、難読化されたコードに対する何らかの説明や正当化がなければ、コードレビューアーやメンテナンスプログラマーは元のプログラマーが実際に達成することを意図していた明確なアイデアはありませんとなります。コードが実際に機能していることを証明することはほとんど不可能です(実際に、コードが実行することがサポートされていることを実行します)。また、メンテナンスプログラミングが面倒になり、バグが発生しやすくなります。

私は(資格なしで)コードが完全に自己コメント化できるとは思いません。しかしながら;私はその方向に大きく傾くことが可能だと完全に信じています。

いくつかの一般的でない(またはEdge、パフォーマンス、またはこれまで指定されていなかった)理由がある場合、その理由は(b * 42) | (~(b - 1) * 7)構文は正当化されますが、コードのINTENTが簡単に解読されず、なぜそれが行われたのかを説明するコメントがなかったため、コードレビューアはその事実を簡単に把握できませんでした。

賢いためだけに賢いコードは避けてください。明確なコードを作成する主な目的の1つは、人間が理解できるようにし、将来バグが発生するのを予防的に防ぐことです。明確に理解できない賢いコードは時限爆弾です。今日は機能しても、明日、または1年後には、コードの保守後に機能しなくなります。そして、バグは難解で、見つけて修正するのが難しいでしょう。

元のプログラマー以外の誰かが、意図と構文が明確でない場合にコードが機能することをどのように証明できますか?元のプログラマでも、その場合にコードが機能することをどのように証明できますか

コメントは、ねじれた、巧妙なEdgeケースコードで必要です。しかし、コードがコメントを必要としないほど単純であれば、それはより良いことです。これらのコメントは、コードが更新され、メンテナンスプログラマがコメントを更新または削除しなかった場合も、古くなったり無関係になったりします。プログラマは、コメントを更新せずに定期的にコードを更新します。彼らはすべきではない、彼らは単に行う

これらすべての要因により結論が導き出されますこのプログラマーは、コードが機能することを証明し、コードを他の人間が理解できるようにし、コードを堅牢にするという問題に対処する必要があります。メンテナンス中に新しいバグが発生する可能性が非常に高いコードは脆弱です。壊れやすい、バグが発生しやすいコードの開発に多額のお金を払いたくない雇用主はいない。

脆弱なコードは、その動作を証明できませんが、クリーンで明確、証明可能(テスト可能)、理解しやすく、保守しやすいコードよりもはるかに高価です。プログラマーは、他の誰かから報酬を得て、プロフェッショナルになり、妥当なコストで、適度にクリーンで、堅牢で、保守可能な製品を製造します。正しい?

したがって、結論として、そのコードを思いついたプログラマーは明らかに賢く、したがって可能性に満ちています。しかし、コードの正当性を証明し、コードを堅牢でわかりやすいものにするという問題は、真剣に受け止める必要があります。その変更が発生する可能性がある場合、プログラマはおそらく続ける価値があります。そうでない場合、プログラマがこの方法で作業を続けることを主張すると、チームを維持することを正当化するのが難しくなる可能性があります。それが本当の問題ですよね。

私見では。

3
Craig

このようなコードがコードレビューで提示された場合、提起する2つの質問があります。

  • なぜ私たちはそれをこのように書くことを選んだのですか?このようなビット操作は、ある種のパフォーマンスのボトルネックを回避するために使用されるため、このアプローチを代わりに使用すると、修正されるボトルネックがあると推測されます。すぐにフォローアップする質問は、「これが重大なボトルネックであることをどのようにして証明したのか」です。

  • 最適化されたアプローチが最適化されていないアプローチと同等であることを証明する何らかの承認フレームワーク(単体テストなど)がありますか?私たちがしないでくださいの場合、それは警報ベルが鳴るときですが、それらは考えられるほど大きくはありません。

最終的に、作成するコードはmaintainableにする必要があります。高速ですが、バグが発生したときにブローチするのが難しいコードは、良いコードではありません。彼らが上記の私の懸念の両方を満たすことができれば、おそらく正当化とそれに相当する非ビット形式の両方を追加する要求でそれを手放すでしょう。

一般に、コードレビューでは、これらの欠点を特定して明確にする必要があります。その方法でコードを記述する正当な理由がない場合、またはコードが単に正しくない場合(他にもいくつか指摘されているように)、そもそもその方法でコードを記述した理由はなく、修正する必要があります。 。これが継続的な傾向である場合。つまり、開発者は効率的でありながらひどく難読化されたコードを引き続き作成します。その時点で、リーダーまたは上級管理職が介入して、明確なコードの重要性を繰り返します。

3
Makoto

If-sを算術/論理式に置き換える必要がある場合は、プロセッサの長いパイプが必要です。これにより、コードは条件に関係なく常に同じ命令を実行するようになり、並列化により適しています。

つまり、2つのサンプルは同等ではないため、提供されたサンプルは間違っています。

_if (b)
  return 42;
else
  return 7;
_

return ((b&1)*42)|((1-(b&1)))*7にすることができます

OPサンプルのプログラマーが誤ったif除去を行ったか、OP自体がifの従来のスイッチを誤った方法で推測したときに、プログラマーの意図を誤って解釈した可能性があります。 2つのうちどちらが正しいかを言うことは、要件が何であるかを知ることは難しい。

式は「あいまい」ではないことに注意してください。これは、P*(t)+Q*(1-t)(t = 0..1の形式)の単なる線形結合です。これは、ほとんどの線形代数。

私が理解できる唯一のことは、コーダーとレビュアーの間に、並列処理と線形代数に関する異なる文化的基盤があるということです。このIMHOは、正当性が証明されている場合、一方を他方よりも優れたものにしません。

2

Sparky(アイテム1と3)とDonscarlettiによって作成された優れた点に加えて、この投稿は、私がずっと前に回答した別の記事に私を導きます: コードをどのように文書化しますか?

多くの人がプログラマーである、または自分自身をプログラマーと呼んでいます。他の多くの人生の歩みと同じように。あなたが自分より劣っている、または期待しているよりも劣っているように見える人(あまり役に立たない、時間の浪費)を判断することができ、彼らを助けようとする(素晴らしい)か、彼らをより良くするように強制することができます(あなたは選択の余地がないかもしれません)、またはそれらを無視して単に最善を尽くすかもしれません(選択の余地がないかもしれません、これは時々最良のルートです)。一般に、どのアクションを選択するかは、単純な技術的能力をはるかに超える多くの要因に依存します。

1
asoundmove