NSString 連結を利用して heredoc 風に定数を記述する
これまで 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 @""
と定義した場合は後に定義されたものが置換対象となる。この動作は思わぬ事故を招くので ifdef
や ifndef
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 による連結を利用する手もあるかも?だけど。
というわけで調べてみたら
を見つけた。ここには 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 文のインデントを目的とするなら、
- Syntax とそれにぶら下がる単位で分割
- 各行の末尾に空白を入れる
とするのがよいだろう。空白の重複は無視されるので「行末へ空白を入れる」ぐらいの単純化されたルールでもよい。
そんなわけで今後 SQL 文を定義するときは heredoc 風で書くことにした。あと Objective-C で本物の heredoc がサポートされることを願っている。