昨日、友達と呑んでいて、「C の switch、do-while、union を使ったことがない。どうしてこんなものがあるのか?」と聞かれました。その場で説明したんですが、「あどけない話にも書いて」と言われたので、書いておきます。(あまり、乗る気ではないのですが。。。)
switch
確かに if-else があれば不要です。でも、if-else の使い方の中で、頻繁に出てくるパターンがあり、それを取り出したのが switch です。すなわち、ある*一つの*スカラー変数の値に応じて、挙動を変えることです。
典型例は、コマンドオプションの処理でしょう。
while ((ch = getopt(argc, argv, "abc")) != -1) { switch (ch) { case 'a': /* -a の場合 */ break; case 'b': /* -b の場合 */ break; case '?': /* fall through */ default: /* -? と知らないオプション */ usage(); } }
上記のように、fall through をうまく使うと、処理をまとめられて便利です。
蛇足ですが、default: は下にある必要はありません。人生で一回だけ、上の方にあるコードを見たことがあります。まぁ、上の方に書くのはお勧めしませんけどね。
switch に関する議論はいくつかあります。一つは、デフォルトは break にすべきだという主張。「DのFAQ」には、こんなことが書かれています。
switch文がfall throughなのは何故ですか?
沢山の人に、switch 文のcaseとcaseの間には必ずbreakが入る、 という仕様にならないかと要求されました。C の、いわゆる "fall through" は沢山のバグの原因となってきたからです。
Dでこれを変えなかった理由は、整数の昇格や演算子の優先順位を同じにしたのと、 同様の理由 - Cとして読めるコードはCと同様に振る舞うべき - です。 潜在的に違う意味をもっていたりしたら、 余計わかりにくいバグを生むでしょうから。
デフォルトは break で、fall through のときに、新たなキーワード 'through' を使うというのはいいアイディアですね。
それから、「オブジェクト指向のこころ」の175ページには以下のようなことが書かれています。
switch は抽象化の必要を示す赤信号となり得る
switchは、(1)ボリモーフィズムの導入、あるいは、(2)責務の見直しを検討すべき赤信号となり得ます。この場合、抽象化を行ったり、他のオブジェクトに責務を委譲したりするといった、より一般的な解決策を考慮して下さい。
デザインパターンとともに学ぶオブジェクト指向のこころ (Software patterns series)
- 作者: アラン・シャロウェイ,ジェームズ・R・トロット,村上雅章
- 出版社/メーカー: ピアソン・エデュケーション
- 発売日: 2005/09/16
- メディア: 大型本
- 購入: 51人 クリック: 615回
- この商品を含むブログ (125件) を見る
C だと多相性(異なる種類のオブジェクトを同じ関数名で仕事をさせること)は実現できないと思っている人もいるかもしれませんが、関数へのポインタの配列を用意すれば、同じようなことはできますね。
do-while
これは簡単です。while は 0 回以上繰り返しなのに対し、do-while は 1 回以上の繰り返しなのです。
必ず1回は実行したいことの典型例は、入力です。
do { /* 入力 */ } while (/* 正しい入力でない間 */)
do-while が存在するのは、'*' で十分な正規表現に、'+" も定義されている理由と同じです。便利でスマートに見えるからです。
上記のコードを do-while を使わないと、こうなってしまいます。
/* 入力 */ while (/* 正しい入力でない間 */) { /* 入力 */ }
同じコードが 2 回現れるのが、汚いと感じる人もいるってことですね。
なお、for と while は等価なので、どちらか一方でいいはずです。do-while がいらないという人は、同時に for と while のどちらかはいらないと言わないとフェアではない気がします。。。
union
気持ちは分ります。関数へのポインタと union を使いこなせるようになれば、一人前の C プログラマーです。:) この二つが分らなければ、カーネルのコードは書けないでしょうね。
論より証拠ということで、ICMPv6 のヘッダの定義を見てみましょう。(質問した友達は、ネットワークのことが分るので、例はこれで十分でしょう。)
struct icmp6_hdr { u_int8_t icmp6_type; /* type field */ u_int8_t icmp6_code; /* code field */ u_int16_t icmp6_cksum; /* checksum field */ union { u_int32_t icmp6_un_data32[1]; /* type-specific field */ u_int16_t icmp6_un_data16[2]; /* type-specific field */ u_int8_t icmp6_un_data8[4]; /* type-specific field */ } icmp6_dataun; }; #define icmp6_data32 icmp6_dataun.icmp6_un_data32 #define icmp6_data16 icmp6_dataun.icmp6_un_data16 #define icmp6_data8 icmp6_dataun.icmp6_un_data8 #define icmp6_pptr icmp6_data32[0] /* parameter prob */ #define icmp6_mtu icmp6_data32[0] /* packet too big */ #define icmp6_id icmp6_data16[0] /* echo request/reply */ #define icmp6_seq icmp6_data16[1] /* echo request/reply */ #define icmp6_maxdelay icmp6_data16[0] /* mcast group membership */
つまり、異なるデータ型がメモリの同じ位置に来る場合は、union を使うべきなのです。
へなちょこプログラマーは、union を使わずに、キャストを利用してバグの多いコードを書きがちです。
ちゃぶ台をひっくり返すようなまとめ
「なぜ Haskell は重要か」の一部を翻訳して、この記事のまとめとします。
(関数型言語と)同じ程度とは言えないが、命令を並べる方式でも抽象化していくとこはできる。命令型言語では、新しいキーワードと標準ライブラリを導入することで抽象化する。たとえば、多くの命令型言語は、プログラマーがループを実現する仕事から解放されるように、少しずつ動作が異なる複数のキーワードを用意している。しかし、命令型言語は、命令を並べる方式に基づいているため、そこから完全に逃れることはできない。命令型言語では、命令の並びに対し、抽象化のレベルを上げる唯一の方法は、さらなるキーワードや標準関数の導入である。そして、言語はゴチャゴチャになる。
来れ!純粋関数型言語の世界へ。(ああ、無理矢理だ。:)