uint[] は IList<int> に変換できる
こんなポストが流れてきました。
C#の挙動についてのちょっとしたクイズです。
以下のコードの出力結果は何になるでしょう?
public static class Program
{
public static void Main()
{
object array = new uint[] { 1,2 };
AnalyzeType(array);
}
public static void AnalyzeType(object o)
{
switch(o)
{
case IEnumerable<int> value:
Console.WriteLine("Int array"); break;
case IEnumerable<uint> value:
Console.WriteLine("UInt array"); break;
default:
Console.WriteLine("Unknown type"); break;
}
}
}
最初の自分の考えはこうでした。
- いちど
uint[]
をobject
に変換しているけど、uint[]
は参照型だから、boxingは関係しないはず -
uint[]
はIList<uint>
を実装するのでIEnumerable<uint>
を実装するはず - 一方で、
uint[]
はIEnumerable<int>
を実装しないはず - 要素が参照型の
IEnumerable<T>
の場合はT
をアップキャストしたIEnumerable<T>
にキャストできる「共変性」があるが、これは値型には適用されない
UInt array
だ!
答えは違いました。正解はInt array
です。
細かい挙動を見てみる
実験してわかった挙動は以下の通りです。
-
uint[]
をIEnumerable<int>
に直接キャストすることはできないが、いちどobject
を経由することでキャストできる -
IEnumerable<int>
以外にも、IList<int>
やIReadOnlyList<T>
、果てはint[]
へのキャストも可能 - この方法で作成した
IEnumerable<int>
は普通に使え、int
の範囲外の値はuint
に再解釈される。配列の内容はコピーされず、直接参照される - リフレクションを使用して
uint[]
の実装するインターフェースを列挙すると、IEnumerable<int>
等は実装されていない - これが起こるのはサイズの等しい整数型どうし(
byte
とsbyte
、ushort
とshort
、uint
とint
、ulong
とlong
)だけで、浮動小数点数(int
とfloat
など)では起こらない
総合して、uint[]
をint[]
に自動的に再解釈するような挙動であるようです。
ILを見てみる
is
演算子による判定や型のパターンマッチングでは、isinst
というIL命令が使われます。最初のコードのILには次のような部分があります。
IL_0000: ldarg.0
IL_0001: isinst class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>
IL_0006: brtrue.s IL_0012
このisinst
がuint[]
のインスタンスについて、IEnumerable<int>
に対してtrue
を返すことを意味しています。
CoreCLRのソースを見てみる
次に私が探したのはランタイム実装です。正確なところは自信ありませんが、このisinst
ではIsInstanceOfAny_NoCacheLookup
という関数が呼び出されるようです。
この先を掘っていくと……
-
ObjIsInstanceOfCore()
https://github.com/dotnet/runtime/blob/9893d145ebb61ae592afa1a64baa2f6f094b1f6b/src/coreclr/vm/jithelpers.cpp#L979-L1036 -
MethodTable::CanCastTo()
https://github.com/dotnet/runtime/blob/9893d145ebb61ae592afa1a64baa2f6f094b1f6b/src/coreclr/vm/methodtable.cpp#L1373-L1418 -
MethodTable::ArraySupportsBizarreInterface()
https://github.com/dotnet/runtime/blob/9893d145ebb61ae592afa1a64baa2f6f094b1f6b/src/coreclr/vm/methodtable.cpp#L1421-L1444
ArraySupportsBizarreInterface
といういかにもな名前の関数がいました。
BOOL MethodTable::ArraySupportsBizarreInterface(MethodTable * pInterfaceMT, TypeHandlePairList* pVisited)
{
/* 略 */
// IList<T> & IReadOnlyList<T> only supported for SZ_ARRAYS
if (this->IsMultiDimArray() ||
!IsImplicitInterfaceOfSZArray(pInterfaceMT))
{
CastCache::TryAddToCache(this, pInterfaceMT, FALSE);
return FALSE;
}
BOOL result = TypeDesc::CanCastParam(this->GetArrayElementTypeHandle(), pInterfaceMT->GetInstantiation()[0], pVisited);
CastCache::TryAddToCache(this, pInterfaceMT, (BOOL)result);
return result;
}
さらに、配列の要素型についてCanCastParam()
という判定が入っています。
-
TypeDesc::CanCastParam()
https://github.com/dotnet/runtime/blob/9893d145ebb61ae592afa1a64baa2f6f094b1f6b/src/coreclr/vm/typedesc.cpp#L386-L433
BOOL TypeDesc::CanCastParam(TypeHandle fromParam, TypeHandle toParam, TypeHandlePairList *pVisited)
{
/* 略 */
else if(CorTypeInfo::IsPrimitiveType(fromParamCorType))
{
CorElementType toParamCorType = toParam.GetVerifierCorElementType();
if(CorTypeInfo::IsPrimitiveType(toParamCorType))
{
if (GetNormalizedIntegralArrayElementType(toParamCorType) == GetNormalizedIntegralArrayElementType(fromParamCorType))
return TRUE;
} // end if(CorTypeInfo::IsPrimitiveType(toParamCorType))
} // end if(CorTypeInfo::IsPrimitiveType(fromParamCorType))
// Anything else is not a match.
return FALSE;
}
GetNormalizedIntegralArrayElementType()
はこちらです。
-
GetNormalizedIntegralArrayElementType()
https://github.com/dotnet/runtime/blob/main/src/coreclr/vm/array.cpp#L1003-L1026
CorElementType GetNormalizedIntegralArrayElementType(CorElementType elementType)
{
/* 略 */
// Array Primitive types such as E_T_I4 and E_T_U4 are interchangeable
// Enums with interchangeable underlying types are interchangeable
// BOOL is NOT interchangeable with I1/U1, neither CHAR -- with I2/U2
switch (elementType)
{
case ELEMENT_TYPE_U1:
case ELEMENT_TYPE_U2:
case ELEMENT_TYPE_U4:
case ELEMENT_TYPE_U8:
case ELEMENT_TYPE_U:
return (CorElementType)(elementType - 1); // normalize to signed type
default:
break;
}
return elementType;
}
なんだか覚えのある整数型の対応関係が見えてきました!
さらにコメントによるとenum
の場合もunderlying typeとの変換が行えるようです。こちらも試してみました。
using System;
using System.Collections.Generic;
var array = new E[]{ E.A, E.B, E.C };
var list = (IList<int>)(object)array;
list[0] = 2;
Console.WriteLine(array[0]); // C
enum E { A, B, C }
この挙動の背景は?
dotnet/runtime
のコミット履歴を遡ってみましたが、.NET Coreの初期リリースから存在しているため、おそらく.NET Framework時代からの挙動なのでしょう。
次に私は、これが.NET の仕様書に書かれていないかを調べました。
すると次のような項目がありました。
I.8.7
- compatible-with – this is the relation used by castclass (§III.4.3) and isinst
(§III.4.6), and in determining the validity of variant generic arguments
isinst
やcastclass
命令における型の関係は、compatible-with
という言葉で定義されるとのことです。
I.8.7.1
A signature type T is compatible-with a signature type U if and only if at least one of the following holds.
(略)
5. T is a zero-based rank-1 array V[], and U is a zero-based rank-1 array W[], and V is array-element-compatible-with W.
(略)
7. T is a zero-based rank-1 array V[], and U is IList<W>, and V is array-elementcompatible-with W.
(略)
型T
とarray-element-compatible-with
な型W
に対して、T[]
とW[]
、T[]
とIList<W>
はcompatible-with
であるようです。
array-element-compatible-with
の定義はこちらです:
I.8.7.1
A signature type T is array-element-compatible-with a signature type U if and only if T has underlying type V and U has underlying type W and either:
- V is compatible-with W; or
- V and W have the same reduced type.
reduced type
はこちらです:
The reduced type of a type T is the following:
- If the underlying type of T is:
a. int8, or unsigned int8, then its reduced type is int8.
b. int16, or unsigned int16, then its reduced type is int16.
c. int32, or unsigned int32, then its reduced type is int32.
d. int64, or unsigned int64, then its reduced type is int64.
e. native int, or unsigned native int, then its reduced type is native int.- Otherwise, the reduced type is itself.
ありました!仕様で定義されていたんですね。
ところで、array-element-compatible-with
の定義はcompatible-with
の定義を拡張したものなので、今回の挙動はジャグ配列でも成立しそうです。試してみました。
using System;
var array = new int[1][];
array[0] = new int[1];
var arrayUInt = (uint[][])(object)array;
arrayUInt[0][0] = uint.MaxValue;
Console.WriteLine(array[0][0]); // -1
感想
配列型の符号あり・符号なしのキャストを、コピーなしに実現したいモチベーションは理解できます。ただ、最新のC#でこの類の操作をするならSpan<T>
にしてからMemoryMarshal.Cast()
を使ったり、あるいはUnsafe.As<T>()
を使ったりすることでしょう(こちらなら整数以外にも変換できますし)。これが安全な、仕様に定義された手段として提供されていたとは驚きました。
Discussion