NSString 連結を利用して heredoc 風に定数を記述する

2016年12月6日 0 開発 , , ,

これまで Objective-C による iOS アプリで FMDB を使用するとき、SQL 文は define directive で定義していた。

#define SQL_READ @"SELECT user_id, name, age FROM users WHERE 20 <= age;"

この方法による定数は pre-process で単純なコードに対する置換として動作する。そのため内容が構文エラーであっても置換された結果が構文として正しいなら許容される。C 言語や C++ ではこれを利用して関数名の一部を pre-process で書き換えるなどのハックが横行していたものだ。

define directive のもう一つの特徴として、複数定義された場合は後勝ちになる。例えば

#define SQL_READ @"SELECT user_id, name, age FROM users WHERE 20 <= age;"
#define SQL_READ @""

と定義した場合、後に定義されたものが置換対象となる。この動作は思わぬ事故を招くので ifdefifndef directive により定義状態で条件分岐するものだが、いかにも冗長である。

なお、このような定義を見つけたら Xcode 8.1 は

'SQL_READ' macro redefined

と警告してくれるのだが、それでも後勝ちの定数はそのまま使用されてしまう。これを防ぐためにコンパイラーの警告レベルをあげてエラーにする手もあるが、そうした運用による対策なしに標準でエラーにしたいところ。

また Xcode 4 時代に Cocoa が提供する標準型の定数書式が改善されて NSArray や NSDictionary などを定数化しやすくなった。これらと一緒に定義するとき、文字列だけ define directive なのは違和感がある。

というわけで前述の定数を

static NSString * const kSQLRead = @"SELECT user_id, name, age FROM users WHERE 20 <= age;";

と書き換える。グローバル定数にするなら .h で以下のように宣言し、

extern NSString * const kSQLRead";

.m で値を定義する。

NSString * const kSQLRead = @"SELECT user_id, name, age FROM users WHERE 20 <= age;";

ただし NSObject 系を定数として宣言する際はポインターの示すものに注意すること。この辺の話は objective c – "sending ‘const NSString *’ to parameter of type ‘NSString *’ discards qualifiers" warning – Stack Overflow の議論が分かりやすい。Win32 API でプログラミングしていた頃はこうした宣言で事故らないように typedef で抽象化していたが、Objective-C だとこの方法を見かけないので素で書いている。

さて NSString 定数を宣言したところまではよかったが、SQL 文のように長くて可読性を求められるものを単一行に定義するのはつらい。構文に基づいてインデントしたくなる。こうしたとき heredoc を使えると便利。

ただ、はたして Objective-C は heredoc をサポートしているのだろうか。C 言語と見なして back slash による連結を利用する手もあるかも?だけど。

というわけで調べてみたら How to split a string literal across multiple lines in C / Objective-C? – Stack Overflow を見つけた。ここには C 言語と Objective-C 独自の方法が紹介されている。できれば一次情報にあたりたかったのだけど Stack Overflow にもリンクはなく、 Objective-C の言語仕様の日本語訳を見ると

「コンパイラのディレクティブ」に、定数文字列を連結するための 言語サポートについて文書化しました。

こうという更新履歴はあるものの当該項目は見当たらず。残念。この記事を読まれた方で情報をお持ちの方はコメ欄や Twitter などで指摘していただけると助かります。

とはいえ紹介されている Objective-C の方法はちゃんとコンパイルできて期待どおり動く。試しに

static NSString * const kSQLRead = @""
"SELECT "
  "user_id, name, age "
"FROM "
  "users "
"WHERE 20 <= age;";

と定義した SQL 文が FMDatabase – executeQuery で動作することを確認できた
。NSLog に渡すと結合された結果がちゃんと出力される。

この機能により文字列の定義における自由度は格段に向上した。好きなようにインデントできて嬉しい。ただし

  • 改行位置へ改行コードが自動挿入されない
  • 変数の展開がない

ことから heredoc と呼ぶのは語弊があるだろう。そのため heredoc 風としておく。それともうひとつ注意点がある。この機能は単に複数の文字列を連結しているだけなので、

static NSString * const kSQLRead = @""
"SELECT"
  "user_id, name, age"
"FROM"
  "users"
"WHERE 20 <= age;";

のように各行末から空白を取り除いてしまうとそのまま

static NSString * const kSQLRead = @"SELECTuser_id, name, ageFROMusersWHERE 20 <= age;";

こんな感じに結合され、SQL 文として成立しなくなる。そのため結果を意識しながら定義すること。SQL 文のインデントを目的とするなら、

  • Syntax とそれにぶら下がる単位で分割
  • 各行の末尾に空白を入れる

とするのがよいだろう。空白の重複は無視されるので「行末へ空白を入れる」ぐらいの単純化されたルールでもよい。

そんなわけで今後 SQL 文を定義するときは heredoc 風で書くことにした。あと Objective-C で本物の heredoc がサポートされることを願っている。


REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です