C# の定義済み型キーワード、そして… Int32 やめます

長らく bool でなく Booleanstring ではなく String、そして int ではなく Int32 と書いてきた (いわゆる) CLR 型名派の私。

世間の声にもめげず、自分を曲げずにここまでやってきました。Visual Studio や ReSharper のコード スタイル設定での公認機能拡充等々で遂に我々 (?) の時代が来た!と思っていた…のですが…

C#, 謎の挙動

ある日の夜、まあ、こういうコード書いてたわけです。

1
2
3
4
5
6
7
8
public enum SomeType : Byte
{
Foo = 1,
Bar = 2,
Baz = 3,
// ...
Invalid = unchecked((Byte) -1),
}

(経緯は割愛するとして) Int32 値 -1 を Byte に無理やり押し込めると 255 になって、表現上もしっくりくるので、意図込みで上のように書いたんです。

さりげなーく書かれてる : Byte。これ、Roslyn 以前は byte などのキーワードな型名しか受け付けなかったので、CLR 型名派が全力でスルーしたい C# 言語仕様トップ 3 に入りそうなくらい残念な制限だったのです。これが遂に「きちんとした」 (と勝手に主張する) 型名でも指定できるようになって、nameof(int) とは書けないけど nameof(Int32) とは書けるみたいな超どうでもいいネタと併せて、この世の春を (勝手に) 謳歌していたんですね。

ですが、なんと上のコード、

1
2
3
Invalid = unchecked((Byte) - 1),
// ^^^^
// CS0119: 'byte' is a type, which is not valid in the given context

なんということでしょう、無様な整形まで施された上に何故かエラー扱いされてしまいました。Invalid なのは値ではなく、コードだったなんて…

色々試してみると、

1
unchecked((byte) -1); // OK!

なんと Byte じゃなくてキーワードな byte だと問題ないというショッキングな結果に…

また、

1
2
unchecked((Byte) 1); // OK!
unchecked((Byte) (-1)); // OK!

これでもコンパイルが通ります。かなり謎な挙動です。Byte じゃなくて byte だとコンパイルが通るだけでも十分に謎なのに、括弧を補ってやれば Byte でも正しいコードになるあたり、すごく謎な挙動ですし、私は今度こそ C# コンパイラの秘孔を突いたのではないかと快哉を叫び、すかさずバグ レポートを仕上げました

C#, 謎の仕様

圧倒的な達成感を得たまま床に就いた私でしたが、ああ、なんということでしょう。42 分という圧倒的な早さで飛んできた reply は、C# 言語仕様の引用でした…

7.7.6 キャスト式

(中略) cast-expression の文法のために、構文にはあいまいな部分があります。たとえば、(x)-y という式は、cast-expression (x 型に対する -y のキャスト) または parenthesized-expression と組み合わされた additive-expression (値 x - y を計算する) のいずれにも解釈できます。

cast-expression のあいまいさを解決するために、次の規則が設けられています。かっこの中に 1 つ以上の token (2.3.3 を参照) のシーケンスがある場合は、以下の条件の少なくとも 1 つが満たされている場合に限り、cast-expression の開始と見なします。

  • トークンのシーケンスが、type については正しい文法になっているが、expression については正しくない。
  • トークンのシーケンスが type の正しい文法になっており、右かっこのすぐ後のトークンが、”~“ トークン、”!“ トークン、”(“ トークン、identifier (2.4.1 を参照)、
  • literal (2.4.4 を参照)、または as および is 以外の任意の keyword (2.4.3 を参照) である。

上の規則で使われている “正しい文法” という表現は、トークンのシーケンスが特定の文法生成規則に準拠している必要がある、ということだけを意味します。シーケンスを構成する識別子の実際の意味については考慮しません。たとえば、xy が識別子の場合、x.y は型に関して正しい文法です。x.y が実際に型を表しているかどうかには関係ありません。
あいまいさを排除するための規則に従うと、x および y が識別子である場合は、(x)y(x)(y)、および (x)(-y)cast-expressions であり、(x)-yx が型を示していても cast-expressions ではありません。ただし、x が定義済みの型 (int など) を示すキーワードである場合は、このようなキーワードがそれ自体で式になることはないため、先に示した 4 つの形式はすべて cast-expressions になります。

― Microsoft, “C# 言語仕様, Version 5.0” (2012).

まさか、言語仕様にそのままドンピシャな形で記載があったなんて…またしても完敗でした。というより、こんな細かい例外規定が用意されていて、しかもそれを 1 時間足らずで引き出せる人がいるという事実に、似非とはいえ曲がりなりにも言語処理系の作者である私としては、ただ打ちのめされるしかありませんでした。まあ当然だけど。

Int32 forever;

今まで、C# に用意されている int なり byte なりの定義済みの型名は、今まで単なる (外観上の) C 言語の系譜との親和性のために明示的に用意されたデザインだと推測していました。

しかし、上に引用した仕様を鑑みるに、どうやらこれらのキーワードは C# コンパイラの構文解析上の都合による、ある種の妥協の産物であると考えたほうがむしろ筋が通るように見えました。

どうも C# は識別子の解釈において、型かどうかの判定がかなり保守的であるように見えます。こと複雑さから逃れられない数値リテラルの解釈の都合上、なるべく曖昧さが持ち込まれないように、それら数値の型が大半を占める、いわゆるプリミティブ型について言語側でキーワードを用意しておくというのは、なるほど合理性があるように思われます。

そして、現に曖昧さの排除という効果を持つ言語キーワードを用いないという選択は、現実に ((Byte) (-1)) のように括弧を補う必要がある、という構文上の不利益を発見してしまった以上、CLR 型名を使うことは、もはや単なる字句的な見栄えの問題ではない、構文的に非合理な選択であると私は結論付けざるを得ませんでした。

転向宣言

これは屈従ではない、自己選択である

さようなら、Int32。私が Int32 と書くのは、もはや型やメンバの名前の一部か、あるいは nameof 式の中だけでしょう。

しかし、int でも Int32 でも、その本質は System.Int32―れっきとした、一級市民の型であるという事実は変わりありません。

Int32 よ、永遠なれ!

転んでも、ただでは起きぬ

…と、こうして今回も C# のバグではなく自らの無知を発見してしまったわけですが、結果的に新たな知識を手にすることができましたし、ただいま読んでいただいておりますように blog の記事のネタにもなりました。

また、今回はさらに ReSharper の issue を作ることもできました―最終的な結果はまだ不明ではありますが、恐らくバグで間違いないでしょう―今回はまあ、ちょっとした恥を晒してしまったとはいえ、結果的には結構な収穫になったかなと考えています。

これに挫けることなく、これからも言語上の怪しげな挙動はバグレポをこつこつと書き上げて、いつの日か真のバグを踏み抜く日が来るのを期待してコードを書いていきます。