在讨论.NET的类型系统的时候,我们经常提到“基元类型(Primitive Type)”的概念,我发现很多人并没有真正理解基元类型就究竟包含哪些(比如很多人觉得字符串是基元类型)。除了明确界定基元类型外,本篇文章还会简单介绍额外两种关于类型的概念——Unmanaged类型和Blittable类型。
.NET下的基元类型(Primitive Type)如下14个。我们可以这样来记:长度(字节数)分别为1、2、4、8的有/无符号的整数;外加两个基于指针宽度(下x86=4; x64=8)的整数,计10个。长度(字节数)分别为4和8的单精度和双精度浮点数,计2个。外加布尔类型和字符类型, 计2个。所以我们熟悉的String(string)和Decimal(decimal)并不是基元类型。
对于某个指定的Type对象,我们可以利用它的IsPrimitive属性确定它是否为基元类型。
public abstract class Type
{
public bool IsPrimitive { get; }
}
Type对象的IsPrimitive属性值最终来源于RuntimeTypeHandle类型如下这个内部静态方法IsPrimitive。从该方法的实现和CorElementType的枚举成员也可以看出,枚举值2-13,外加CorElementType.I(IntPtr)和CorElementType.U(UIntPtr)这14个类型属于基元类型的范畴,这与上面的列表是一致的。
public struct RuntimeTypeHandle
{
[SecuritySafeCritical]
internal static bool IsPrimitive(RuntimeType type)
{
CorElementType corElementType = GetCorElementType(type);
if (((int)corElementType < 2 || (int)corElementType > 13) && corElementType != CorElementType.I)
{
return corElementType == CorElementType.U;
}
return true;
}
}
[Serializable]
internal enum CorElementType : byte
{
End = 0,
Void = 1,
Boolean = 2,
Char = 3,
I1 = 4,
U1 = 5,
I2 = 6,
U2 = 7,
I4 = 8,
U4 = 9,
I8 = 10,
U8 = 11,
R4 = 12,
R8 = 13,
String = 14,
Ptr = 15,
ByRef = 16,
ValueType = 17,
Class = 18,
Var = 19,
Array = 20,
GenericInst = 21,
TypedByRef = 22,
I = 24,
U = 25,
FnPtr = 27,
Object = 28,
SzArray = 29,
MVar = 30,
CModReqd = 31,
CModOpt = 32,
Internal = 33,
Max = 34,
Modifier = 64,
Sentinel = 65,
Pinned = 69
}
顾名思义,Unmanaged类型可以理解不涉及托管对象引用的值类型。如下的类型属于Unmanaged 类型的范畴:
如果要求泛型类型是一个Unmananged类型,我们可以按照如下的方式使用unmanaged泛型约束。我在《如何计算一个实例占用多少内存?》提到过,只有Unmananged类型采用使用sizeof操作符计算大小。
public static unsafe int SizeOf<T>() where T : unmanaged
{
return sizeof(T);
}
Blittable是站在基于P/Invoke的互操作(InterOp)角度对传递的值是否需要进行转换(Marshaling)而作的分类。Blittable类型要求在托管内存和非托管内存具有完全一致的表示。如果某个参数为Blittable类型,在一个P/Invoke方法调用非托管方法的时候,该参数就无需要作任何的转换。与之类似,如果调用方法的返回值是Blittable类型,在回到托管世界后也无需转换。如下的类型属于Blittable类型范畴:
顺便强调一下,DateTime/DateTimeOffset都采用Auto布局(如下所示),Guid虽然是一个默认采用Sequential布局的结构体,但是最终映射在内存种的字节依赖于字节序(Endianness),所以具有这三种类型字段的结构体或者类都不是Blittable类型。
[Serializable]
[StructLayout(LayoutKind.Auto)]
public struct DateTime
{ }
[Serializable]
[StructLayout(LayoutKind.Auto)]
public struct DateTimeOffset
{ }
只有Blittable类型的实例才能调用GCHandle的静态方法Alloc为其创建一个Pinned类型的GC句柄。以如下的代码为例,类Foobar的两个属性都是Blittable类型,我们通过标注在类型上的StructLayoutAttribute将布局类型显式设置成Sequential使其称为了一个Blittable类型。
GCHandle.Alloc(new Foobar(), GCHandleType.Pinned);
[StructLayout(LayoutKind.Sequential)]
public class Foobar
{
public int Foo { get; set; }
public double Bar { get; set; }
}
如果Foobar类定义成如下的形式,都不能使其称为一个Blittable类型。前者默认采用Auto布局,后者的Bar属性并不是Blittable类型。如果将这样Foobar对象作为参数按照上面的方式调用GCHandle. Alloc方法,会直接抛出ArgumentException异常,并提示“Object contains non-primitive or non-blittable data. (Parameter 'value')”。
public class Foobar
{
public int Foo { get; set; }
public double Bar { get; set; }
}
[StructLayout(LayoutKind.Sequential)]
public class Foobar
{
public int Foo { get; set; }
public DateTime Bar { get; set; }
}