なぜカップルとタプルがしばしば悪いのか



多くのプログラマーは、ペアとタプル(ペアとタプル)の概念に精通しています-それらの実装は、STL、Boost(および他の場所)で行われます。 それが何であるかを知らない人のために、私は簡単に説明します-これらは、複数の値(ペア-のみ2、タプル-たくさん)をグループ化して、それらを一緒に保存\送信\受信できるテンプレートです。

MSDNの例:

pair <int, double> p1 ( 10, 1.1e-2 ); pair <int, double> p2 = make_pair ( 10, 2.22e-1 ); cout << "The pair p1 is: ( " << p1.first << ", " << p1.second << " )." << endl; cout << "The pair p2 is: ( " << p2.first << ", " << p2.second << " )." << endl;
      
      





最初は、このアイデアは魅力的だと思われます。

  1. 同じ次元の複数のベクトルを関数に渡す代わりに、ペアとタプルのベクトルを1つだけ渡すことができます。それらの対応をチェックする心配はありません。
  2. outパラメーターのポインターやリンクにだまされることなく、関数から値のセットを簡単に返すことができます(多くの場合、これは困難です)
  3. 2〜3フィールドの小さな構造の束を作成しないようにすることができます(コードが少ないほど良い)。
しかし、この力には暗い側面があります。



sayingにもあるように、単純さは盗難よりも​​悪いです。 ペアとタプルはまさにその場合です。 これらは、上記のすべての利点を提供します。 しかし、価格について考えてみましょう。



ペアまたはタプルの内容は謎です



そのような関数の宣言を見てみましょう:

 pair<string, string> GetInfo( int personId );
      
      





彼女は何を返すと思いますか? 姓と名? そして、どこで手に入れましたか? たぶん-パスポート番号と税コード? または、フルネームと電話番号。 または本名とニックネーム。 アイデアは、関数宣言のどこにも、返されるペアに含まれるものを記述するものではないということです。 もちろん、あなたは今これを覚えています。 そして、1年で覚えていますか? さらに、この関数を使用することを決定した別のプログラマーは、コードをクロールし、それが返すものを探す必要があります-これにより、OOPアプローチ全体、モジュール性、カプセル化、その他の重要なことが完全に終わります。

上記のコードと次のコードを比較してください。

 struct Person { string Name; string Surname; } Person GetInfo( int personId );
      
      





すべてが明確であるため、戻り値を理解するために関数コードを読む必要はありません。



ペアまたはタプルのデータの順序は謎です



さて、前の例をよく考えて、次のように関数を書き直しました。

 pair<string, string> GetNameAndSurname( int personId );
      
      





これで、彼女がその人の名前と姓を返すことが明らかになりました。 しかし、ここに質問があります-どの順番で? これらの行の特定の順序がペアになっていることは非常に明白なようですが、悪い知らせがあります。 あなたは、人々が名前にさまざまな単語の順序、日付と時刻のさまざまな形式を使用し、左から右へ、またはその逆に書き、多様な交通のある道路を運転するなどの世界に住んでいます それはあなたに唯一の可能なものであるという事実、ペアの値の順序のこのバリアントのみが何も証明しません。 マーフィーの法律の1つにあるように、「 何かがいくつかの方法で解釈できる場合、それらは最も不正確なものによって解釈されます 。」

戻り値に別の構造(クラス)を使用する場合、コードの解釈は常に明確です。





拡張性が低い



さらに先に進む-関数にさらにデータを追加したい場合はどうなりますか? はい、ペアをタプルに置き換えて、ばかげたサイズに成長させることができます。

 tuple<string, string, int> GetNameAndSurnameAndBirthday( int personId );
      
      





しかし、この機能を使用するすべての人にとって、それはなんと悪夢でしょう! 変更のたびに、すべての関数呼び出しを確認し、正しいフィールドにアクセスしているかどうかを確認する必要があることがわかります。 ホラー



値の1つが存在しないことを示すことができない



値のセットでは、1つ以上のフィールドが設定されない場合があります。 構造体またはクラスに表示するのは非常に簡単です(isSet変数を作成するか、フィールドチェックメソッドを記述します)が、ペアまたはタプルに表示することは完全に不可能です。この場合、セットにすべての値が含まれ、有効であると想定されます。 その結果、「2番目のパラメーターが-1の場合、情報がまったくない」という精神で合意に煩わされる必要がありますが、これは明らかではなく、忘れられていて不快です。





検証チェックを挿入する場所がありません



デバイスの動作温度範囲を返すこの関数を見てみましょう。

 pair<int, int> SomeDevice::GetCelsiusTemperatureRange() { ... return make_pair( -300, +30 ); }
      
      





1文字の入力ミスの結果、関数は(過度の負担をかけずに)物理的現実の境界を拡大し、デバイスが-300℃で動作できることを示しました。 ペアオブジェクトの作成時または関数からこの値を返すときのいずれかで、そのような温度の妥当性のチェックはまったくありません。 そして、それを書く場所はまったくありません。

それが事実であるかどうか、温度範囲オブジェクトが作成されたときに返された場合、何らかの方法で無効な値をキャッチし、それに反応することが可能になります(アサート、ログ、例外、有効な値での置換など)

 struct TemperatureRange { int minTemp; int maxTemp; TemperatureRange( int min, int max ) { assert( min <= max ); assert( min >= -273 ); minTemp = min; maxTemp = max; } } TemperatureRange SomeDevice::GetCelsiusTemperatureRange() { ... return TemperatureRange( -300, +30 ); //   assert! }
      
      







反例



記事のタイトルで「 最も頻繁に悪い 」とはどういう意味ですか? 私は時々カップルが使用できることを認めなければなりません。 たとえば、2人のプレーヤーのゲームメカニクスの過程で、ランダムな値(intの範囲の数値)を捨てる必要があるゲームがあります。 これは、次の形式の関数によって実行できます。

 pair<int, int> GetTwoRandomNumbers();
      
      





この機能が悪くないのはなぜですか? すべてが非常に簡単です:





さらに、この例では、ペアは個別のクラス(コードが少ない)よりも優れており、ポインターの形式のより優れた出力パラメーター(有効性を確認する必要はありません)および配列またはベクトルよりも優れています(混乱する任意のサイズにすることができます)。

一般に、例には生命に対する権利があります。



おわりに



ペアやタプルの使用は、明確で読みやすく、十分に拡張可能なコードを作成しようとしている場合、ほとんど正当化されていないようです。 小さなクラスまたは構造体を使用すると、非常に単純な場合を除き、ほとんど常に読みやすさが向上します。



All Articles