『Cプログラミング診断室』目次次(第12章 芸術的字下げ まとめ)

第12章 芸術的字下げ

単純に


このプログラムは何をするものか分かりましたか。では、リスト12−1と12−2を両方見比 べながら調べていきましょう。

■open/close■

まず、リスト12−2の16〜29行を見てください。複写先のファイルをオープンしてみて、-1で なければ、つまりオープンできればnotice_promptの方へ行きます。これは、複写先ファイルが存 在するときで、もし存在すれば上書きしても良いかユーザーに問い合わせるためです。

ファイルの存在の有無を調べるのに、openを使うのは賢明ではありません。openしたファイルを closeするのは面倒です。どのCにでも、ファイルのアクセス権を調べるaccess関数が存在するは ずです。これを使えば、ファイルをオープンすることなく、ファイルの状態を調べられます。もち ろん、openしていないので、closeは必要ありません。

これは、とても軽症で、そんなにとやかく言う程のものではありません。

では、リスト12−1で、複写元ファイルのopenとcloseの対応を調べましょう。openは32行に あります。closeは、165行と182行の2個所にあります。徹底的にネストにこだわるのなら、192行 の } の次にcloseを書くべきです。でも、けっして分かりやすくありません。これも、openと closeが「長距離」になっているからです。

■既存のものを使おう■

つぎに、リスト12−2の31〜95行を見てください。意図は何でしょう?

複写元、複写先の両ファイルをオープンし、コピー用のバッファ(実は1KB)をmallocしてい ます。次にforループに入り、複写元から複写先へ1KB単位でmallocで確保したバッファを経由し ながらファイルの中身をコピーしています。

ということは、どういうことでしょう。ファイルをまるまるコピーしているだけなのです。UNIX のcpコマンドがやるのと同じことを長々と書いているだけです。ですから、ここの「行数稼ぎ」は、 system関数を使い、unixのcpコマンドを実行するようにすれば一発ですんでしまいます。もちろん、 中途半端な1KBのバッファなど確保する必要もありません。確保していないのだから、freeの必要 もありません。それに、1KB程度のバッファがどうしても欲しいのなら、mallcするより、関数内 部に局所変数として確保すべきです。

この部分と同様のことが、例の有名なCの本(参考文献12−1)に、もう少しエレガントに書か れていますが、あれはcpコマンドの解説として出ているのです。でも、何の考えもなく、あの本の 一部をコピーすれば、あの本の著者並みの良いソフトウェアになる訳ではありません。

cpコマンドを使うと、複写元ファイルのopenが不要になり、ファイル記述子fhsも不用になりま す。どんどん単純になっていきますね。

■for■

リスト12−1では、42,43行のfor文はぐちゃぐちゃしています。とくに注意すべきは、for文 の第1式と第3式が同じになっていることです。
        for( exp_a ; exp_b ; exp_a )
今回の場合は比較的短いのですぐに理解できますが、それでも望ましくはありません。こういう場 合は、
	while( exp_a, exp_b )
とか、
	for(;;) {
		exp_a;
		if( ! (exp_b) )
			break;
	}
など、できるだけ同一式が出てこない方法に書き換えます。リスト12−2では、永久ループの for(;;)を使った書き方にしました。これは、readとwriteが対なので「同一レベル」に並ぶように し、エラー処理も読み込み、書き込み直後に行なうようにしたためです。readが1回、writeが1 回の方がバランスが取れていると思いませんか。

でも、この部分は、もっとちゃんと修正したリスト12−3ではなくなってしまいます。

■後始末■

ファイルをコピーし、必要な修正も終ると、後は終了処理です。リスト12−1の165〜178行が 正常終了時に2つのファイルをcloseし、エラー時には複写先ファイルを消去してしまいます。こ の部分は、複写元、複写先の両ファイルのオープンに成功後は、エラーが発生した場合にも通過し ます。

プログラムは、「準備」、「処理」、「後始末」の三段論法で、順に流れるのがベストです。だ から、「後始末」はプログラムの最後に位置しなければなりませんが、関数の途中にあるので分か りにくいのです。リスト12−2では、この後始末を関数の最後にしました。

でも、まだ分かりづらいのは、正常時にも、異常時にも通過するためです。

■ウィンド・システム■

このアプリケーションソフトは、Xウィンド上で動いています。Xウィンドに限りませんが、ほ ぼ全てのウィンドシステムでは、ウィンドに表示する文字列はもとより、多数の情報をプログラム 中に直接書かず、適当なテキストファイルを用意すれば、その内容に従って表示内容を変更してく れます。ということは、表示メッセージの変更、さらには動作状態や初期値の変更が、ソースプロ グラムの変更、コンパイル、リンクという一連の面倒で時間のかかるステップを踏むことなく、テ キストファイルの変更だけで済みます。即座にデバッグに入れることは、デバッグの効率を著しく 向上させ、コンピュータの資源をムダ遣いせず、かつ十分にテストする余裕をもたらすので品質が 向上します。

リスト12−1またはリスト12−2の関数notice_promptのNOTICE_MESSAGE_STRINGSに続く大 文字の名前はマクロ名で、

  #define   SYS_MESSAGE_FILECOPYE          "複写できません。"
というように文字列に定義されています。これらのマクロ定義は、1つのファイルに書かれている ので、良くまとまっていると思うのは軽率です。

それどころか、このメッセージ定義ファイルの内容を変更しようものなら、makeしたとたん、全 ファイルのコンパイルが始まります。短いプログラムなら不平もありませんが、このプログラムは 十万行を越えていて、20分程度待たされてしまいます。だから、とても気楽にメッセージの変更は できません。本当に気合いを入れて、「やるぞー」という意気込みで開始し、散歩か買いものにで も出かけるのでなければしてはいけません。

これだけの長さになると、まず一人で開発することはありません。複数人での開発ですから、全 プログラムをコンパイルしている間は、全員の作業がその間停止してしまいます。もちろん、その ようにならなくする一時のがれの手段もありますが、いやらしい逃げ道にすぎません。これでは、 もう作業になりません。

ウィンドシステム以外でも、起動時などにユーザ毎の環境設定ファイルを読み取り、ユーザ毎に 異なった動作をするようにプログラムを作るのは今や常識ですよね。ウィンドシステムでは、始め からそのような機構が存在します。ぜひ使って楽をしましょう。

オリジナルはOpenWindows上で動作するように、XViewライブラリの関数notice_promptを使って 作られていました。アプリケーションは、できることならどこのメーカーのコンピュータでも動作 することが理想です。OpenWindowsでは特定メーカーのコンピュータでしか動作しなくなり、他の メーカーがより優れたコンピュータを出しても移植が大変です。それで、特定コンピュータメーカー に拘束されないように、よりオープンなMotifへ移植しました。

リスト12−3を見てください。このリストはMotif版のものです。ここでは、XViewの関数 notice_promptの代わりに、Xウィンドのリソースデータベースの機能をちゃんと利用した関数 my_notice_promptを作成し、利用しました。この関数は、第2引数にエラー種別を文字列で指定す ることで、この文字列から表示すべき文字列などを取り出し、エラー表示をします。

リスト12−3 修正版(その2) エラー処理を統合化(Motif版)

     1  int     file_copyfile( filed, files, job, page, comment )
     2    char  *filed;         /* 複写先ファイル名 */
     3    char  *files;         /* 複写元ファイル名 */
     4    char  *job;           /* 複写先JOB名 */
     5    char  *page;          /* 複写先頁番号 */
     6    char  *comment;       /* 複写先コメント */
     7  {
     8          int             ret;
     9          int             fhd = -1;
    10          SYS_PAGE_HEAD   head;
    11          BASE_INFO       destbase;
    12          char            command[1000];
    13                                                  /* 複写先ファイルの存在確認 */
    14          if( access( filed, O_WRONLY ) == 0 ) {
    15                  ret = my_notice_prompt( master_menu_panel, "filecopy_check" );
    16                  if ( ret != NOTICE_YES )
    17                          return -2;
    18          }
    19                                                          /* ファイルのコピー */
    20          sprintf( command, "cp %s %s 2> /dev/null", files, filed );
    21          if( system(command) != 0 ) {
    22                  my_notice_prompt( master_menu_panel, "filecopy_cp_error" );
    23                  return  ERROR;
    24          }
    25                                                          /* 複写先ファイルのオープン */
    26          if( (fhd = open( filed, O_RDWR ) ) == -1 )
    27                  goto error_finish;
    28                                                          /* 複写先ファイルの先頭へ */
    29          if( lseek( fhd, 0, SEEK_SET ) != 0 )
    30                  goto error_finish;
    31                                                          /* ヘッダー部読み込み */
    32          if( read( fhd, &head, sizeof(SYS_PAGE_HEAD)) != sizeof(SYS_PAGE_HEAD) )
    33                  goto error_finish;
    34                                                          /* 変更部分に位置合わせ */
    35          if ( lseek( fhd, head.base_pos, SEEK_SET ) != head.base_pos )
    36                  goto error_finish;
    37                                                          /* 変更部分の読み込み */
    38          if( read( fhd, &destbase, head.base_size )!= head.base_size )
    39                  goto error_finish;
    40                                                          /* 変更 */
    41          strncpy( destbase.job_name, job, JOBNAME_LEN );
    42          strncpy( destbase.page_no, page, PAGENO_LEN );
    43          strncpy( destbase.comment, comment,COMMENT_LEN);
    44                                                          /* 変更部分に位置合わせ */
    45          if ( lseek( fhd, head.base_pos, SEEK_SET ) != head.base_pos )
    46                  goto error_finish;
    47                                                          /* 変更部分の書き込み */
    48          if( write( fhd, &destbase,head.base_size) != head.base_size )
    49                  goto error_finish;
    50  
    51          if ( close( fhd ) == -1 )
    52                  goto error_finish;
    53                                                          /* 正常終了 */
    54          return  NORMAL;
    55  
    56    error_finish:
    57          unlink( filed );
    58          my_notice_prompt( master_menu_panel, "filecopy_check_error" );
    59  
    60          return  ERROR;
    61  }

オリジナルプログラムでは、notice_promptがプログラム全体に400個ほど散在しています。だか ら、notice_promptを同値の関数に置き換えるのではなく、頑張って便利な関数を作ることには大 きな意味がありますが、my_notice_promotの詳細はMotifやXウィンドの詳細を理解する必要があ るのでリストは省略します。

■エラーメッセージ■

リスト12−2までは、読み込み時(read,lseek)か、書き込み時(write,close)のエラーかを区 別しています。リスト12−2の101行以降は、ファイルのコピーが完了し、複写先ファイルの内 容の一部を変更する処理なので、意味的には、たとえreadやlseekを使っていても複写先ファイル に対する操作です。使用関数ではなく、意味で区別すべきです。

リスト12−3には、my_notice_promptは3つしかありません。それらの第2引数は、

        "filecopy_check"
        "filecopy_cp_error"
        "filecopy_check_error"
になっています。readとwriteに対応してエラーメッセージを書く人が大勢いますが、それはデバッ グしている人には意味があるかも知れませんが、アプリケーション利用者にはどうでも良いことで、 自分のやろうとしたことが拒否された本質的理由が分かれば良いのです。以上の3つの第2引数は エラーメッセージそのものではなく、エラーメッセージを引き出す「鍵」です。Motifでは、
    ColorSystem*filecopy_check:		ファイルコピー/ファイル名エラー
という感じに、: の右側に自由に文字列を書けば、その文字列が直接画面上の指定した適当な個所 に出るようにすることができます。

実際のエラーメッセージは、テキストファイルとして用意されるので、ユーザーにとって一番分 かりやすいメッセージを後でじっくり推敲してもらうことができます。

■国際化対応■

XウィンドもX11R5になり、国際化、つまり、一つのソフトで、英語版にもなり、日本語版にも なるということが叫ばれています。その中心的役割を果たしている書物が、「X11Release5増補版」 というXのマニュアルです。国際化のための章が2章もあり、アメリカでも日本語対応を含めた国 際化対応に力を入れていると言いたげです。私には、「アメリカでも、やっと他国のことを考える ようになったのかなあ」という感じです。

でも、このマニュアルに載っているサンプルプログラムを見ると笑ってしまいます。そのコメン トには、

    /* このプログラムのエラーメッセージはすべて英語であるが、
     * 真の国際化プログラムではソースコードに書き込むべきではない。
     * ある種のデータベースからエラーメッセージを検索すべきである。
     */
と書かれています

そして、その後に、英語のエラーメッセージがfprintfの引数に書かれているのがゴロゴロ出て きます。まあ、コメントで一言断わっているのはよいのですが、「ある種の」というのは、どの種 のことなのか全然わかりません。このマニュアルの他の部分を見ても、具体的にどうすればよいか は書かれていません。

こういうのを、「分かる人にだけ分かる書き方」と言うのです。アホみたいに短い例でよいから、 そのような具体例が欲しいですね。せめて、「ある種」ではなく、たった一言、「リソースファイ ルなどのある種のデータベース」と言って欲しいものです。

実はこのプログラム、日本語のエラーメッセージなどがプログラム中にベタベタと書かれていた のですが、外国から引合いが来てしまい、英語化しなければならなくなったのです。

ウィンドシステムを使い、輸出もする可能性のある場合には、絶対にプログラム中に日本語文字 列を直接書くのは止めましょう。エラーメッセージだけでなく、ボタンなどあらゆる個所にメッセー ジがありますが、それらは全部プログラム中から消し、メッセージ用のデータファイルに移しましょ う。そして、プログラムに書くのは、それらのメッセージを引く「鍵」だけにするのです。

このシステムは、残念ながら、悲鳴がでるほど莫大な量の漢字文字列がプログラム中にばらまか れていました。国際化を叫んでいる人が多いのに、正しい国際化の実現方法を例示しているものが 少ないのは嘆かわしいことです。

■書き直し■

どんどん単純に、私にも理解できるように直してしまったのがリスト12−3です。オリジナル の1/4になってしまいました。短くなりましたが、オリジナルと同じ動きをします。エラー処理を まとめ直したので、オリジナルよりもカスタマイズが容易なソフトウェアに変身してしまいました。


Copyright1996 Hirofumi Fujiwara. No reproduction or republication without written permission
『Cプログラミング診断室』目次次(第12章 芸術的字下げ まとめ)