コーディング時の型破りな技法やえげつないテクニックを紹介していきます。中には実利用を勧められないようなテクニックもありますが、こんな発想もあるんだな程度の気持ちで読んでみると面白いと思います。
目次
- for-ifイディオム
- else文のインライン化
- return文とカンマ演算子
- while文とカンマ演算子
- if文とカンマ演算子でPython風
- 引数とカンマ演算子
- 引数と代入演算子
- if文の簡略化
- return文でvoid型の戻り値を返す
- switchの波括弧省略
- switch文のdefault句を先頭に記述
- superメソッドコールを先頭に記述
- メンバ変数の更新処理を先頭に記述
- 実引数の両端に空白を記述する
- ビットOR演算子の空白無しスタイル
- ビットマスクを加算演算子で計算
- 演算子の優先順位を空白で判別
- switch文のcase節はワンラインで記述
- 列挙型の要素をカラム形式で記述
- switch文のcase節をカラム形式で記述
- 初期化処理をブロック文で行う
- 空コメントによるコードの区別
- アクセサの命名をプレフィックス基準で行う
ダークサイ度の高いテクニックがほとんどです。暗黒面に引きずり込まれないよう注意してください。
── 深淵を覗き込む時、深淵を覗いているのだ by ニーチャ・ン・ネラ ──
for-ifイディオム
for (var s : ["a", "", "c"]) if (s != "") {
print(s + "\n");
}
// 表示結果: "a\nc\n"
波括弧の省略記法を応用しています。
参考:波括弧の省略テクニック集 - ブレースの省略記法について
Swift等の現代的なプログラミング言語ではfor-in-where文等の正式な機能で上記と同等のイディオムを実現してします。
for var s in ["a", "", "c"] where !s.isEmpty {
print(s)
}
else文のインライン化
条件式やelse文のコードが短い場合は、else文のインライン化を行うとよいでしょう。
// Before
if (string) {
print(string);
print("\n");
} else {
print("Empty!");
}
// After
if (!string) { print("Empty!"); } else {
print(string);
print("");
}
可読性の低下や勘違いの原因にもあるため実利用はオススメしません。
return文とカンマ演算子
return文呼び出し前のちょっとした処理をreturn文内に埋め込んでしまいます。
return db.close(), results;
カンマで列挙された処理は左から順に処理されます。上記の例では、戻り値はカンマ演算子の最終評価値であるresults
変数となります。
while文とカンマ演算子
カンマ演算子を繰り返し文に応用した例です。
while (c = next(), c != '\n') {
print(c);
}
// こちらと同等の処理
for (c = next(); c != '\n'; c = next()) {
print(c);
}
if文とカンマ演算子でPython風
カンマ演算子で複数の文を一つの文にすることができます。if文で活用すると波括弧の省略が可能になります。
// 擬似オフサイドルール
if (true)
putchar('a'),
putchar('b');
else
printf("hello"),
fflush(stdout);
引数とカンマ演算子
関数fの引数には関数gの戻り値ではなく、enableの値が渡されます。
void setEnable(bool enable) {
f((g(), enable));
}
引数と代入演算子
setEnableメソッドの引数には代入後の値が渡されます。
void toggleEnable() {
view.setEnable(this.enable = !this.enable);
}
if文の簡略化
&&
演算子の短絡評価を活用することで、if文の実現が可能となります。
if (9 == 9) { puts("same"); }
↓
9 == 9 && puts("same");
&&
演算子には左辺の結果が真の場合にのみ右辺側の式が評価されるという性質があります。
カンマ演算子を用いると、複文の評価も可能となります。
if (9 == 9) {
printf("yes");
fflush(stdout);
}
↓
9 == 9 && (printf("yes"), fflush(stdout));
void型の戻り値を返す関数を呼び出す場合はカンマ演算子でダミーの値や真理値を返します。
9 == 9 && (free(NULL), false);
||
演算子を活用すると不一致時の処理を実現することができます。
if (!strlen("")) { puts("empty"); }
↓
strlen("") || puts("empty");
||
演算子には左辺が偽の場合にのみ右辺が評価されるという性質があります。
条件演算子を活用すると、if-else文の実現が可能となります。
9 == 9 ? puts("yes") : puts("no"); // "yes"
9 != 9 ? puts("yes") : puts("no"); // "no"
return文でvoid型の戻り値を返す
void型の戻り値はvoid型の関数の戻り値として返すことができます。
void close() {}
void write() {
if (true /* do something */) {
// Before //
close();
return;
// After //
return close();
}
/* do something */
close();
}
void型へのキャストで強制的にvoid型の戻り値を返すこともできます。
void write() {
if (true)
return (void)printf("error!");
printf("success!");
}
// int printf(const char *, ...);
switchの波括弧省略
switch (v) case 1: case 2: {
print("1 or 2");
}
switch文のブレース省略イディオム【波括弧省略の意外なテクニック】
switch文のdefault句を先頭に記述
行数の削減ができます。
switch (v) { default: return "(unknown)";
case TextAlignLeft: return "Left";
case TextAlignRight: return "Right";
case TextAlignCenter: return "Center";
}
superメソッドコールを先頭に記述
親クラスのメソッド呼び出しを、関数宣言の開始位置と同一の行に記述します。行数の削減ができます。またメインの処理を目立たせることができます。
func mouseDown(event: Event) { super.mouseDown(event)
print("mouseDown:", event)
}
func mouseUp(event: Event) { super.mouseUp(event)
print("mouseUp:", event)
}
メンバ変数の更新処理を先頭に記述
行数の削減ができます。またメインの処理を目立たせることができます。
void setName(String name) { this.name = name;
dothat();
}
同様にアサーションを先頭に記述することも有効です。
void f(String *s) { assert(*s != NULL && length(s) > 0):
print(s);
}
// なんちゃってnull安全
void g(void *a, void *b) { assert(a); assert(b);
print(a, b);
}
実引数の両端に空白を記述する
// Before
assert(9 == 9);
// After
assert( 9 == 9 );
肝心な式の内容が強調されます。また((
や))
等の二重括弧特有の読みづらさを回避することができます。
print((String)get(i)); // 読みづらい
print( (String)get(i) ); // 読みやすい
出力関数やログ関数、アサート・マクロなどに限定して用いるのがよいでしょう。
ビットOR演算子の空白無しスタイル
Javaの世界では割とよく見かけるスタイルです。
定数同士のビットマスク処理に限り、OR演算子の空白を省略します。
// Before
alert("hello", OK_MASK | CANCEL_MASK);
// After
alert("hello", OK_MASK|CANCEL_MASK);
// 一般的なOR演算では省略しない
boolean f = a | 0xFF;
// 変数を用いる場合も省略しない
Options opt = WidthSizable | mask;
// 定数同士の静的な演算であれば省略する
Options opt = WidthSizable|HeightSizable
演算子の空白省略はあまり見慣れないため可読性が悪く感じられたり、構文チェックツールに引っかかる可能性があるため、本来であればあまり良いスタイルではないかもしれません。またコードの一貫性も損なわれます。
ただ、今回のスタイルのように用途に合わせて記法を変えるという発想はとても大切なことだと思います。一貫性をあえて崩すことで別の新たな規則性や一貫性、意外性を生み出すことができる、というのがこのスタイルの教訓と言えます。
ビットマスクを加算演算子で計算
if (flags == (ControlKey | ShiftKey));
↓
if (flags == ControlKey + ShiftKey);
==
演算子よりも+
演算子のほうが優先順位が高くため、丸括弧の省略が可能となります。
ただしビットマスク用の定数同士の加算に限り有効なテクニックです。両定数のビットが重ならない場合にのみ活用することができます。ビット表現やビット演算の仕組みを理解していないと間違いを犯しやすい危険な技法となるため注意してください。
演算子の優先順位を空白で判別
以下の式は丸括弧を適切に用いることで<
演算子が&&
演算子よりも高い優先順位で処理されることを明示しています。こちらは一般的なテクニックです。
(0 < v) && (v < 9)
式を詰めることで優先順位を明示する型破りなテクニックもあります。以下の式は空白が省略されているほうが優先的に処理されるということを示唆しています。
0<v && v<9
他にも、空白を二つ用いることで、周りの式よりも優先順位の低い演算子であることを明示することもできます。文字数の削減もできます。
0 < v && v < 9
(0 < v) && (v < 9)
switch文のcase節はワンラインで記述
行数がもったいないのでcase節とbreak;文を同一の行に記述します。
switch (align) {
case Left: str = "Left"; break;
case Right: str = "Right"; break;
case Center: str = "Center"; break;
}
このようにcase内の処理が単純な場合はワンライン化します。
従来の書き方は冗長的で読みづらいと感じることがあります。
switch (align) {
case Left:
str = "Left";
break;
case Right:
str = "Right";
break;
}
switch文でのこの手の処理は、相当な量の要素数でもって記述されることが多く、下手をすればswitch文内の処理が30行を超えることもしばしばです。しかしこのテクニックを使えばそれを10行に短縮することができます。
ちなみに列挙型の内部値と配列の添字アクセスを応用したテクニックもあります。
if (Left <= align && align <= Center)
str = ["Left", "Right", "Center"][align];
列挙型の要素をカラム形式で記述
以下の記述はまだまだ改善できます。
enum Modifier {
Ctrl_L,
Ctrl_R,
Alt_L,
Alt_R,
Shift_L,
Shift_R,
};
enum Modifier {
Ctrl_L, // Control
Ctrl_R,
Alt_L, // Alternate
Alt_R,
Shift_L, // Shift
Shift_R,
};
横幅を上手く活用しましょう。行数の削減もできます。
enum Modifier {
Ctrl_L, Ctrl_R, // Control
Alt_L, Alt_R, // Alternate
Shift_L, Shift_R, // Shift
};
「関連性のある要素はできる限りインラインで記述する」という原則を徹底すると読みやすく理解のしやすいコードになります。
enum Tokens {
Add, Sub, Mul, Div, // +, -, *, /
Bracket_L, Bracket_R, // [, ]
Brace_L , Brace_R, // {, }
Paren_L , Paren_R, // (, )
};
まるでExcelの表のような見た目ですね。整ったコードでとても読みやすいです。私はこの手のコードを「ドキュメント的で読みやすいコード」「ドキュメントのように整理された綺麗なコード」と表現しています。
コード整列のススメ|読みやすいコードを意識するプログラミング作法
switch文のcase節をカラム形式で記述
列挙型の時と同じ発想です。こちらは読みやすさよりはどちらかというと行数の削減が大きな目的になってしまっています。
switch (modifier) {
case CTR_L: return "Ctr L"; case CTR_R: return "Ctr R";
case ALT_L: return "Alt L"; case ALT_R: return "Alt R";
case WIN_L: return "Win L"; case WIN_R: return "Win R";
case FUNCTION: return "Function";
case CAPSLOCK: return "Caps Lock";
case NUMLOCK: return "Num Lock";
};
初期化処理をブロック文で行う
どこからどこまでが特定のインスタンス変数の初期化処理なのかが明確になります。
void setup(Alert a) {
var field = new TextFeild();
{
field.text = "Hello";
field.textColor = RED;
field.align = RIGHT;
a.messageField = field;
}
var button = new Button();
{
button.text = "OK";
a.firstButton = button;
}
}
ちなみに、最近の言語ではこう書ける場合もあります。
var field = new TextFeild() {
.text = "Hello"
.textColor = RED
}
var field = new TextFeild(
text: "Hello",
textColor: RED
)
先程のテクニックをより現実的にしたイメージですね。このようにブロックやインデントを有効的に使うとコードは圧倒的に読みやすくなるので、もっと色々な言語に取り入れてほしい機能だと感じます。
ちなみに、次のような記法も考えられますが、記述が面倒なのと、IDEによってコードが再整形されてしまう問題があるためオススメはできません。対応策については次項のテクニックを参考にしてみてください。
var field = new TextFeild();
field.text = "Hello";
field.textColor = RED;
空コメントによるコードの区別
空のブロック・コメントを用いて通常の処理とアサーション/ロギング処理を区別させます。
function fn(s) {
/**/assert(s != NULL);
/**/assert(!s.empty());
alert(s);
alert(s.length);
}
void fn(String *s) {
/**/ assert(s != NULL);
/**/ assert(!s->empty());
auto it = s.begin();
auto end = s.end();
/**/ assert(it != end);
while (++it != end) {}
}
アサーションやロギングは特殊なコードであるため通常の処理と区別させるべきだ、という発想を元に考え出されたテクニックです。以前は大文字のASSERTを独自定義して区別していましたが、最近はこちらの方法を取ることが多くなりました。
参考:コメント・テクニック集 - 矯正インデント
いずれ削除する予定の一時的な処理を記述する際に活用するのも良いでしょう。また/
文字の挿入でコードの非活性化を行う事もできます。
void f($s) {
/**/var_dump($s);
↓
//**/var_dump($s);
}
またこのテクニックは、オブジェクトの初期化処理等、コードを揃えるための用途にも応用できます。
// Before
auto field = new TextFeild();
{
field->text = "Hello";
field->textColor = RED;
field->align = LEFT;
}
auto passwd = new TextFeild();
{
passwd->placeHolder = "****";
passwd->secureField = true;
}
// After
auto field = new TextFeild();
/**/ field.text = "Hello";
/**/ field.textColor = RED;
/**/ field.align = LEFT;
auto passwd = new TextFeild();
/**/ passwd->placeHolder = "****";
/**/ passwd->secureField = true;
アクセサの命名をプレフィックス基準で行う
// Before
double UIWindowGetAnimationSpeed();
void UIWindowSetAnimationSpeed(double v);
// After
double UIWindowAnimationSpeedGet();
void UIWindowAnimationSpeedSet(double v);
サフィックスに機能名を使っているような場合は、Get/Setのキーワードが中置されてしまい、印象が薄くなってしまいます。
プレフィックスにGet/Setを記述することで、重要なキーワードを目立たせる意図があります。
ヨーダ命名規則のススメ