ソースコードをインデントする際には、4文字幅のタブや空白が使われることが多いですが、2文字幅の人気も根強いように思います。特に熟練のプログラマであるほど、2文字幅による字下げを好む傾向にあるように感じます。今回はそんな2文字幅インデントの有効性や利点・欠点について解説していきます。
目次
2スペースの利点
コードの横幅が小さくなる
4スペースの場合はどうしてもテキストエディタの横幅を広く使ってしまいます。
function goo(foo, bar) {
return function (array) {
for (var i = 0; i < array.length; i++) {
if (foo(array[i])) {
return bar(i);
}
}
};
}
2スペースならこれがかなり縮みます。
function goo(foo, bar) {
return function (array) {
for (var i = 0; i < array.length; i++) {
if (foo(array[i])) {
return bar(i);
}
}
};
}
とてもコンパクトな見た目になりました。
最近のプログラミング言語では名前空間の利用やクラスの宣言、クロージャー/ラムダ式の利用によって、コードの階層が深くなる傾向にあるため、2スペース・インデントの必要性は昔よりも高まっているように感じます。
// 極端な例
namespace jp {
namespace marycore {
namespace partial {
class Math {
public: static auto max(int a) {
return ^(int b) {
return a < b ? b : a;
};
}
};
}
}
}
眼球運動が抑えられる
コードがコンパクトになる分、眼球の移動も最小限に抑えられます。長時間のコーディングやコードレビュー時に最適なインデント幅となります。
条件式とthen節が明確に区別される
2スペースを用いると、if文の条件式とthen節の表示位置が重なりません。
// 2スペース
if (file.isFileURL() && ← 条件部
file.exists()) {
file.remove(); ← 処理部
fclose(file);
}
// 4スペース(条件部と処理部の開始位置が揃ってしまう)
if (file.isFileURL() &&
file.exists()) {
file.remove();
fclose(file);
}
4スペースの字下げによって引き起こるこの垂直方向の“重なり”は、読み手に余計な解釈をさせてしまう恐れがあります。
人によっては以下のようなコードとして解釈してしまうかもしれません。
if (file.isFileURL() &&
file.exists() &&
file.remove()) {
fclose(file);
}
このような余計な解釈は、たとえ一瞬であったとしても行われてはならないものです。
4スペースのこの問題への対応策としては、空行で両処理を明確に分ける方法(改善策1)や&&
演算子を前置する方法(改善策3)、空白やインデントでコードを整える方法(改善策2・4・5)等が考えられますが、いずれも賛否両論があり、完璧な方法とは言い切れません。
// 改善策1(もっとも現実的)
if (file.isFileURL() &&
file.exists()) {
file.remove();
fclose(file);
}
// 改善策2
if (file.isFileURL() &&
file.exists()) {
file.remove();
fclose(file);
}
// 改善策3
if (file.isFileURL()
&& file.exists()) {
file.remove();
fclose(file);
}
// 改善策4
if ( file.isFileURL()
&& file.exists()) {
file.remove();
fclose(file);
}
// 改善策5
if ( file.isFileURL() &&
file.exists() ) {
file.remove();
fclose(file);
}
// 改善策6(オールマンのスタイル)
if (file.isFileURL() &&
file.exists())
{
file.remove();
fclose(file);
}
// 改善策7(オールマン絶対使いたくないマンのスタイル)
if (file.isFileURL() &&
file.exists()
) {
file.remove();
fclose(file);
}
// 改善策8
if (file.isFileURL() &&
file.exists()) {
file.remove();
fclose(file);
}
2スペースを用いれば、if文のこれらの問題を意識する必要はなくなります。また2スペースは丸括弧が省略可能なSwiftやGo, Rust言語等の次世代プログラミング言語でも同様の効力を発揮します。
if file.isFileURL() &&
file.exists() {
file.remove();
fclose(file);
}
文字数制限に強い
開発現場によってはソースコードの横幅に文字数制限(横幅最大80文字 等)を設ける所もありますが、これが非常に面倒な問題を引き起こします。中途半端な所で改行を行わなければならなくなるのです。
4スペース
....
....
var split = function (string) {
var array = string.separateBy(
"hello"); // 泣く泣く改行
return array;
}
特に深い階層でコードを書くほど、文字数超過のリスクが高まります。
また最近はメソッドチェーンやラムダ式の多用、略称を極力使わない明確な命名規則などによって、横幅を広く使うスタイルが主流となっているため、文字数の問題は非常に悩ましいものと言えます。
2スペース
2スペースの場合は4スペースと同じ深さの階層でも、インデント幅の累積は最小限に抑えられるため、文字数制限に引っかかる機会はとても少なくなります。
..
..
var split = function (string) {
var array = string.separateBy("hello");
return array;
}
コードの横幅や階層が深くなりがちな言語(C++, Objective-C, Swift, Java, JavaScript)では、2スペース・インデントの利点が活きやすい傾向にあります。
少しマニアックな利点
C言語やJavaではブロック文という特殊な文を利用できますが、2スペースの場合はこのブロックの書き出しに、コメント文や一般的な処理を書くこともできます。
int main() {
{ // 最大値
int val = 99;
printf("%d", max(0, val));
}
{ // 最小値
int val = 99;
printf("%d", min(0, val));
}
{ char *s = strdup("hello");
puts(s);
free(s);
}
}
もちろん4スペースでも記述自体は可能ですが、コメント部と処理部の書き出しが揃わないため、記法の一貫性が損なわれやすいという特徴があります。
{ // 最大値
int val = 99;
printf("%d", max(0, val));
}
{ // 最小値
int val = 99;
printf("%d", min(0, val));
}
{ char *s = strdup("hello");
puts(s);
free(s);
}
2スペースの欠点
階層を見失いやすい
しかし2文字インデントにも欠点はあります。階層構造が曖昧になってしまうのです。
ネストが浅くまた分量の少ないコードを書く分にはそれほど問題にはならないのですが、ネストが複雑でブロック内のコード行数が多く縦に長くなるようなコードでは、階層構造が視覚的に曖昧になります。
自身もよく2スペースを用いたコードを保守することがありますが、自分が今どこの階層のコードを追っているのかがわからなくなってしまうようなことがよく起こります。確認のためにコード上部に視線を移しますが、インデントが詰まっているため、なかなか複数ある階層の区別がつきにくく、モニターに定規を当てたくなるほどもどかしい思いをすることもしばしばです。
2スペースによるインデントはネストが深く複雑になるほど、階層感覚を失いやすくなるというデメリットがあるのです。
1スペースでの例
少し極端な例ですが、1スペース・インデントを利用すると、この階層構造の曖昧さが顕著に現れます。
........ {
Flip bar = ........;
if (maybe) { // A
if (isFoo) { // B
unless (_piyo == "foo") { // C
calbee("foobar", rg, Star(bar.pow + 4, 0), "Bar Foo")
} elsif (_piyo == "var" || _piyo == "end") {
calbee("varlet", bar, Star(bar.pow + 4, 0), "Opera Town")
} elif (bar.length) {
calbee("letbar", bar, Star(bar.pow + 2, 0), "stronger")
}
} elseif (~bar.flags) {
callee((self.isGoogle ? "..." : "---"), bar, Star(bar.pow + 3, 0), "")
// inside of B?
}
} elif (bar) {
const arrow = isHoge ? "varlet" : (self.isApple ? "VarLet" : "Varlet");
callbe(arrow, bar, Star(bar.pow + arrow.len, 0), "Var Let")
}
} // end of A?
インデント幅が短くなればなるほど、階層構造の把握が困難になり、コードが読みづらくなることが分かるかと思います。
階層構造の把握に余計な神経を使う
これはつまり、2スペースのようなインデント幅が極端に短いものほど、階層構造の把握に多大な神経を使うようになるということでもあります。階層間の関係性が見出しづらくなったり、気づきにくくなったりもします。
その苦労は、小さな針の穴に糸を通す作業と似ているように思います。インデント幅はまさにこの針の穴の大きさです。針穴が小さければ小さいほど、糸を通す際に余計な神経を使うでしょう。インデント幅もこれと同じです。インデント幅が小さくなればなるほど、階層間の状態や関係性が把握しづらくなります。人によってはこれらをストレスと感じてしまうかもしれません。
2文字幅インデントのススメ
普段4スペースに慣れているプログラマが開発で2スペースの利用を強いられてしまうことがよくありますが、ひょっとしたら彼らは無意識の内にストレスを抱え込んでしまっているのかもしれません。
2スペという名のハラスメントに耐えるためには、普段から2スペースに慣れ親しんで耐性をつけるほかありません。2文字幅インデントはもはや教養です。世のプログラマなら一度は2スペースを嗜んでおきたいものです。
3文字幅インデントのススメ
2スペース派と4スペース派の間を取って3スペース分のインデント幅を採用するという案も考えられます。
3スペース インデントのススメ
制御文やブロック間の関係性が理解しずらくなる
2文字幅インデントでは、字下げレベルという直感的な特性ではなく、波括弧やキーワードの名称というより論理的な部分に依存する形でコードの関係性を判断しなければならなくなるという性質があります。
実際に、2文字幅インデントは then句 と else文 が同一の列に表示されてしまうことによる、視覚的曖昧さの問題を引き起こします。
if (div == 0) {
print "hello"
throw "error"
} else if (div > 0) {
return function () {}
}
このようにthrow
文とelse
文が同一の列に並んでしまっています。これによってthrow
、else
の両処理が同一のブロックに記述されているように錯覚しまう恐れがあります。
ただ実際にはelse
キーワードの横に閉じ括弧(}
)が記述されているため、そのような誤った認知はすぐに解消されます。だだし我々は、これらの手順が余計な認知コストとなってしまっている点に注目しなければなりません。
3スペースインデント以上の字下げ表現やオフサイドルールによる字下げ表現では、字下げの深さという視覚的な特徴でもってコードの関係性を直感的に瞬時に理解することができますが、2文字幅インデントでは、波括弧の存在やキーワードの名称というより論理的な面でコードの関係性を判断しなければなりません。
常に具体性を持った特性に注目しなければならないという余計な負担を強いられているわけです。このような縛りや制限から逃れるためには、終わり波括弧(}
)や始め波括弧({
)をしっかりと意識するようにしたり、波括弧の記述方法を工夫したりする必要があります。
if (div == 0)
{
print "hello"
throw "error"
}
else if (div > 0)
{
return function () {}
}
2スペース活用時の注意点
2文字幅インデントの利用には、階層の深いコードや階層構造が複雑なコードを更に読みづらくしてしまうというデメリットがあります(# 2スペースの欠点)。そのため、2スペース利用時には可能な限り階層が深くなりすぎない簡潔なコードを意識する必要があります。
そのためには関数化やサブルーチン化によって余計な処理を分散させることが求められます。一つの関数内で記述できる行数を制限して関数化を促すのもよいでしょう。一つの関数内で使用できる階層の深さを制限することも有効です。
また関数の結果を即時に返すような工夫をすると、階層が浅くなります。余計な一時変数の利用も廃止できます。
// Before
boolean isNotEmpty(String s) {
boolean result = false;
if (s != null) {
result = s.length() != 0;
}
return result;
}
// After
boolean isNotEmpty(String s) {
if (s == null) return false; // 戻り値の即時返却
return s.length() != 0;
}
続いて、複数の条件式に改行を挟んで記述する際には、両式を縦方向に揃えるようにすることをオススメします。
// Before
if (s != NULL &&
strlen(s)) {
puts(s);
}
// After
if (s != NULL &&
strlen(s)) {
puts(s);
}
二行目の式のインデントはちょうど2倍の4スペースとなっているため、比較的揃えやすく大した労力にはなりません。この整列によって条件文の条件部と処理部の判別がしやすくなるという利点も得られます。
2スペース インデントは、コードの簡潔性や分量をきちんと管理できる優秀なプログラマが使わなければ、真の威力を発揮できないという性質があるように思います。2スペースはある意味、上級者向け・玄人向けのスタイルだとも言えそうです。