代码改变世界

相同类型的每个对象大小都是一样的吗?

2009-11-30 14:09  Jeffrey Zhao  阅读(5562)  评论(41编辑  收藏  举报

快速回答:“相同(引用)类型的每个对象大小都是一样的吗?”其实个问题对于大多数情况下来说应该正确的,不过的确也有些类型受到CLR的特殊照顾,因而有那么些例外。我现在尝试使用一些简单的小实验来进行验证,当然它是不严谨的,只能算是一个简单尝试而已。

比如,我们有这么一个类型:

public class SomeClass
{
    public int Field;
}

然后编写这样的代码:

var c1 = new SomeClass();
var c2 = new SomeClass();
var c3 = new SomeClass();
var c4 = new SomeClass();
var c5 = new SomeClass();

GC.Collect(2);

unsafe
{
    fixed (int* ptr1 = &c1.Field,
        ptr2 = &c2.Field,
        ptr3 = &c3.Field,
        ptr4 = &c4.Field,
        ptr5 = &c5.Field)
    {
        Console.WriteLine("Size of c1: " + ((int)ptr2 - (int)ptr1));
        Console.WriteLine("Size of c2: " + ((int)ptr3 - (int)ptr2));
        Console.WriteLine("Size of c3: " + ((int)ptr4 - (int)ptr3));
        Console.WriteLine("Size of c4: " + ((int)ptr5 - (int)ptr4));
    }
}

运行这段代码的结果是:

Size of c1: 12
Size of c2: 12
Size of c3: 12
Size of c4: 12

当然,如果我们要得出“每个SomeClass对象大小是12字节”,那么还必须有如下两个前提

  • 在堆中分配对象时是连续的。
  • 相同类型的实例,内部字段地布局(或者说“顺序”)是相同的。

但是,有一个类型可能是个特例,那就是我们随处可见的String类型。为此,我们再写一段代码进行实验:

static void Main()
{
    var c1 = new SomeClass();
    var s10 = new String('a', 10);
    var c2 = new SomeClass();
    var s20 = new String('a', 20);
    var c3 = new SomeClass();
    var s30 = new String('a', 30);
    var c4 = new SomeClass();
    var s40 = new String('a', 40);
    var c5 = new SomeClass();

    GC.Collect(2);

    unsafe
    {
        fixed (int* ptr1 = &c1.Field,
            ptr2 = &c2.Field,
            ptr3 = &c3.Field,
            ptr4 = &c4.Field,
            ptr5 = &c5.Field)
        {
            Console.WriteLine("Size of s10: " + ((int)ptr2 - (int)ptr1 - 12));
            Console.WriteLine("Size of s20: " + ((int)ptr3 - (int)ptr2 - 12));
            Console.WriteLine("Size of s30: " + ((int)ptr4 - (int)ptr3 - 12));
            Console.WriteLine("Size of s40: " + ((int)ptr5 - (int)ptr4 - 12));
        }
    }

    DoSomething(s10, s20, s30, s40);

    Console.ReadLine();
}

private static void DoSomething(params object[] args)
{
    // nothing
}

DoSomething的作用仅仅是为了避免s10-40几个字符串直接被当作垃圾而释放掉。上面这段代码中可以得出:

Size of s10: 40
Size of s20: 60
Size of s30: 80
Size of s40: 100

这个结果似乎是说:长度为10的字符串占40字节,长度为20的字符串占60字节……以此类推,长度为n的字符串占20 + 2n个字节(这也说明CLR中的字符是使用双字节的Unicode进行存储)。

真的是这样吗?事实上这个实验中并不能严格得出“不同长度String对象大小不同”,因为,万一String类型也只是直接创建了一个字符数组呢?这样,其实每个String对象的大小还是相同的,大小不同的只是字符数组而已。那么究竟事实是怎么样的呢?这只能靠WinDBG + SOS来一探究竟了,有空我再试试看。

而现在,我也只能靠猜的。我猜String对象是在自身内部包含了一长串字符,并非引用了一个字符数组,因为您看它的构造函数其实都是extern的,要靠外部调用的东东其中一定有猫腻:

[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char[] value);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(char* value);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value);
[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char c, int count);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(char* value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall)]
public extern String(char[] value, int startIndex, int length);
[MethodImpl(MethodImplOptions.InternalCall), CLSCompliant(false)]
public extern unsafe String(sbyte* value, int startIndex, int length, Encoding enc);

最后回到标题:“相同(引用)类型的每个对象大小都是一样的吗?”答案肯定是否定的咯。最简单的例子便是int数组,不同长度的int数组类型相同,但大小明显不一样。当然,无论什么类型的数组都这样。