これは何?
という記事で
int a;
a = 'c';
が問題なく動いている話や 素晴らしいコメント があって面白かったので、記事にしてみた。
それぞれの事情
C言語
C 言語の場合、そもそも 'c'
の型が int
である。
なので
int a;
a = 'c';
は、わりと面白くない。
Twitter 方面で、 'c'
の型が int
であることに疑問を抱いている方が散見されるので追記。
なぜそうなっているのかは知らないんだけど、C 言語では古来よりシングルクオートで囲まれたリテラルは int
型になっている。規格書にもそう書いてある。
古い C でも
printf( "sizeof('c') is %d\n", (int)sizeof('c'));
のようにすると「sizeof('c') is 1
」ではなく「sizeof('c') is 4
」など(4 じゃないかも)となることから、 'c'
が char
ではないことがわかる。'c'
が char
だと思いこんでいる方は是非試してほしい。なお、C++ では 'c'
は char
なので、C++ としてコンパイルしないように注意が必要。
それはともかく。実際問題、'c'
が char
ではなく int
だからといって sizeof
に入れる以外の方法で違いがわかるパターンがないので、知っていても実用上嬉しいことは特に無い感じだった。C11 が出るまでは。
C11 で _Generics
という新機能が入り、'c'
が int だと知らないと引っかかる罠が発生した ので、注意が必要。
C++ / C# / D / Java / Scala
C++ の場合、 'c'
は char
となっている。なので
int a;
a = 'c';
とした場合、char
から int
への暗黙の変換が発生する。この変換はほぼ必ず(sizeof(int)==sizeof(char)
で、なおかつ char
が unsigned
の場合は安全ではないが、そんな処理系に触る機会がある人は稀だろう)安全なので、警告は出ない。
C# と D のことはよく知らないんだけど、同じように char
のようなものから int
のようなものへの暗黙の変換が起きるんじゃないかな(調べてない)。Java も同様かな(よく知らない)。
Scala の場合「弱い適合性」というルールに従って Char
から Int
への変換が許容されるらしい。わかってないけど。
Go
Go は事情が違う。
var a int
a = 'c'
とした場合。a = 'c'
の右辺にある 'c'
は untyped rune。型は無いんだけど、強いて言えば rune
みたいなことだと思う。
左辺の a
は int
。型なしから int
への変換は、右辺の値が int
の範囲内の値であれば成功する。
以下のように
r := 'c'
var a int = r // cannot use r (type rune) as type int in assignment
untyped rune である 'c'
を型指定なしで変数に受けると、受けた変数は暗黙のうちに rune
になる。rune
から int
への暗黙の変換はできないのでエラーになる。
Groovy
Groovy の事情も面白い。
Integer a
a = 'c'
printf("[%c]\n", a)
で a = 'c'
で起こっていることはたぶん(自信ない)
-
'c'
は文字列 - 文字列を整数に代入しようとするので、暗黙の変換を試みる
- 文字列の長さが 1文字の場合に限って、先頭の文字の文字コードを整数型の値として取得できる
ということだと思う。
Integer a
s = 'c'
a = s
print("a=${a}/${a.class} s=${s}/${s.class}") // a=99/class java.lang.Integer s=c/class java.lang.String
のように、文字列から整数への変換ができる。また、 s
の値を ''
や 'hoge'
などにするとエラーになる。
それと。
上記の例では a
の型が確定するような書き方なので a = 'c'
などとすると 'c'
を整数に変換する必要が出てくるが、a
の型を宣言しないと
a = 50
printf("${a} ${a.class}\n") // 50 class java.lang.Integer
a = 'c'
printf("${a} ${a.class}\n") // c class java.lang.String
このように、 a
の型が変わる。
zig
zig の事情は go と似ている。
const stdout = @import("std").io.getStdOut().writer();
pub fn main() !void {
var a: c_int = 'c';
try stdout.print("a={}\n", .{a}); // a=99
}
'c'
の型は comptime_int
で、コンパイル時に値が確定しているけどビット長が確定していない整数型。
これを c_int
に代入すると、値の範囲を検査して OK なら代入できる。
と、ここまでは go と同じ感じ。
go との違いは、情報欠落がない場合は暗黙の変換ができるという点。
const stdout = @import("std").io.getStdOut().writer();
pub fn main() !void {
var s: u8 = 'c';
var a: c_int = s;
try stdout.print("a={}\n", .{a}); // a=99
}
コンパイル時に値がわかっていれば
const stdout = @import("std").io.getStdOut().writer();
pub fn main() !void {
const s: i8 = 'c';
var a: u32 = s; // 符号付きから符号なしへの変換
try stdout.print("a={}\n", .{a}); // a=99
}
情報の欠落がありそうな変換でも暗黙の変換が普通にできる点は珍しいと思う。
さらに。 comptime_int
に u8
などの値を代入することもできるので
const stdout = @import("std").io.getStdOut().writer();
pub fn main() !void {
comptime var a: comptime_int = 1;
comptime var z: u8 = 'z';
try stdout.print("{}\n", .{a}); //=> 1
a = 'c'; // 両辺 comptime_int
try stdout.print("{}\n", .{a}); //=> 99
a = z; // 左辺は comptime_int、右辺は u8
try stdout.print("{}\n", .{a}); //=> 122
}
こういうパターンもある。この辺りも go の「型なし」とは大きく異る。
Julia
ほぼ使ったことがないのでよくわからない。仕様書も見たけどよくわからなかったので試したら。
a16::Int16 = 0
a32::Int32 = 0
a16 = 'c' # 'c' は 16bit の範囲内なので OK
a16 = '😀' # '😀' は U+1F600 なので 16bit の範囲外。エラー。
a32 = '😀' # U+1F600 も 32bit なら範囲内なので OK
どうも
- 'c' は実質的に整数型。
-
a = 'c'
のような代入があると、a
が'c'
を表現できるかどうかをチェックし、表現できれば代入可能。
っぽい。
まとめ
整数型の変数 a
に対して a = 'c'
が合法である理由には各言語それぞれの事情がある。
上記をまとめると、推測も混じっているけどこんな感じ。
言語 | 事情 |
---|---|
C |
'c' は int なので、両辺 int 。 |
C++ / C# / D / Java / Scala |
'c' は int ではないが、整数型。暗黙の変換で代入成立。 |
Go |
'c' は untyped rune。異なる型への暗黙の変換はほぼ全部違法だが、untyped からの変換は、受け側の型が受け入れられる値ならOK。 |
Groovy |
'c' は文字列。一文字の文字列の場合のみ、整数への変換が可能。 |
Zig |
'c' は、comptime_int 。comptime_int に限らず、コンパイル時に値がわかっている場合、受け側の型が受け入れられる値ならOK。 |
Julia |
'c' はおそらく実質的に整数。代入先の整数型で表現できる場合には代入可能。たぶん。 |