Dictionary や object のキーに複数の型を指定した場合、どういった扱いになるのかが非常に気になったので、自分の知っている範囲の言語で試してみました!
Python の場合
a = {}
a[1] = "int"
a[1.0] = "float"
a["1.0"] = "str"
# => {1: 'float', '1.0': 'str'}
int と float は同一視されるようです。Python では 1 == 1.0
が成立するので、これは合理的な気もしますね。使う側は非常に安心できます。他にも、True, False, Fraction, Decimal なども同一視されるようです。
Numeric values that compare equal have the same hash value (even if they are of different types, as is the case for 1 and 1.0).
なお、ハッシュ値は以下のようになっています。小数部分が 0 の場合だけ特殊処理されているようですね。
(123).__hash__() # => 123
(123.0).__hash__() # => 123
(123.456).__hash__() # => 1051464412201451643
Ruby の場合
Ruby の場合は異なる型のキーは区別されるようでした。シンプルで分かりやすいですね。
a = {}
a[1] = "i"
a[1.0] = "f"
a["1"] = "s"
a["1".to_sym] = "sym"
# {1=>"i", 1.0=>"f", "1"=>"s", :"1"=>"sym"}
JavaScript の場合 (>= ES6)
JavaScript はある意味では非常にシンプルです。オブジェクトのキーは(1つの例外を除いて)全て文字列に変換されるため、1
も 1.0
も "1"
も BigInt(1)
も全て同一視されます。
JavaScript のオブジェクトのプロパティ名 (キー) は文字列かシンボルしか扱えないので、各括弧表記の中のキーはすべて、シンボルを除いて文字列に変換されます
しかし、全てを文字列に変換してしまうため、undefined
と "undefined"
や、null
と "null"
が同一視されるような問題が発生します。キーにこういった値を使うのは避けたほうが良さそうです。
a = {};
a[1] = "number (1)";
a[1.0] = "number (1.0)";
a["1"] = "string (1)";
a[BigInt(1)] = "bigint";
console.log(a); // => {1: 'bigint'}
Object.keys(a); // => ['1']
a = {};
a[undefined] = "undefined";
a["undefined"] = "string (undefined)";
console.log(a); // => {undefined: 'string (undefined)'}
Object.keys(a); // => ['undefined']
なお、JavaScript においては 1.0
と 1
はそれ自体が同じものです。
console.log(1 === 1.0); // => true
PHP の場合
PHP の場合はややカオスで、配列のキーは string または integer となります。文字列が指定された場合は、その内容によって、integer に変換されるかどうかが決まるようです。また、小数は整数に変換されますが、バージョンによっては警告が出たりエラーになったりするかもしれません。
$a[1] = 100;
$a[1.0] = 200;
$a["1"] = 300;
$a["1+2"] = 400;
$a["3.14"] = 500;
$a[3.14] = 600;
// Deprecated: Implicit conversion from float 3.14 to int loses precision
// array(4) {
// [1] => int(300)
// ["1+2"] => int(400)
// ["3.14"] => int(500)
// [3] => int(600)
// }
ドキュメントには次のように記載があります。
- 10 進数の int として妥当な形式の String は、 数値の前に + 記号がついていない限り、 int 型にキャストされます。 つまり、キーに "8" を指定すると、実際には 8 として格納されるということです。一方 "08" はキャストされません。これは十進数として妥当な形式ではないからです。
- floats もまた int にキャストされます。つまり、 小数部分は切り捨てられるということです。たとえばキーに 8.7 を指定すると、実際には 8 として格納されます。
- bool も int にキャストされます。つまり、 キーに true を指定すると実際には 1 に格納され、 同様にキーを false とすると実際には 0 となります。
- Null は空文字列にキャストされます。つまり、キーに null を指定すると、実際には "" として格納されます。
- array や object は、キーとして使えません。 キーとして使おうとすると Illegal offset type という警告が発生します。
C# の場合
C# には(TypeScript で言うところの)union 型がないためこういった問題は普通は発生しません。そのためか、小数でも GetHashCode() は特殊処理されておらず、1.0
と 1
では異なるハッシュ値を返します。
// 以下参考:
Console.WriteLine((1).GetHashCode()); // => 1
Console.WriteLine((1.0).GetHashCode()); // => 1072693248
Console.WriteLine(("1").GetHashCode()); // => 1029607131
object 型をキーにして無理やり試すことは出来ますが、普通はそんなことは行わないですし、GetHashCode()
の結果を見れば分かる通り、そんなに面白い結果にもなりませんでした。
var a = new Dictionary<object, string>();
a[1] = "int";
a[1.0] = "double";
a["1"] = "string";
foreach (var (key, value) in a) {
Console.WriteLine($"{key} => {value}");
}
// 1 => int
// 1 => double
// 1 => string
object.Equals()
についてはボックス化される場合とされない場合で結果が変わるようでした。GetHashCode()
の値と矛盾しないよう注意深く実装されているようにも見えますが、どうなんでしょうか。
Console.WriteLine(((object)1).Equals((object)1.0)); // => False
Console.WriteLine(((object)1.0).Equals((object)1)); // => False
Console.WriteLine(((object)1.0) == ((object)1)); // => False
Console.WriteLine(((object)1) == ((object)1.0)); // => False
Console.WriteLine((1).Equals(1.0)); // => False
Console.WriteLine((1.0).Equals(1)); // => True
Console.WriteLine(1.0 == 1); // => True
Console.WriteLine(1 == 1.0); // => True
C++ (STL) の場合
ハッシュ値を見る限り、恐らく C# と似たような仕様かと思います。
std::cout << std::hash<int>()(1) << std::endl; // => 1
std::cout << std::hash<double>()(1.0) << std::endl; // => 8386164645967068059
std::cout << std::hash<std::string>()("1") << std::endl; // => 10159970873491820195
感想
言語によって仕様がバラバラでとても面白かったです。歴史的経緯や処理速度の都合などもあるとは思いますが、個人的には int と float を決して仲間外れにさせないという Python の強い絆が非常に輝いているように見えました。