当值类型从集合中移除时会发生什么?

5
假设我有一个像这样的简单结构体:struct
public struct WeightedInt {
    public int value;
    public double weight;
}

那么假设我有一个由这个结构的实例组成的集合:

List<WeightedInt> weightedInts = new List<WeightedInt>();

据我理解,值类型与引用类型的区别在于,值类型是分配在堆栈上的,因此一旦实例化该对象的函数终止,该值类型对象就会从内存中清除。这意味着在以下代码中:

void AddWeightedIntToList(int value, double weight) {
    WeightedInt wint = new WeightedInt();
    wint.value = value;
    wint.weight = weight;

    weightedInts.Add(wint);
}

在执行完AddWeightedIntToList函数后,本地变量wint的一个副本被添加到weightedInts中,而本地变量itself则被从内存中删除。

首先:这个理解是否正确?

其次,wint的这个副本存储在哪里?它不能在堆栈上,因为一旦函数完成,它就会消失(对吗?)。这是否意味着该副本与weightedInts一起存储在堆上?并且在被删除后是否被垃圾回收,就像引用类型的实例一样?

当然,这个问题很可能在某篇文章中得到了解答,如果是这样的话,提供链接也是可以接受的。我只是没有找到相关文章。


3
值类型并非在堆栈上分配。未在迭代器块中或被匿名函数所关闭的值类型的局部变量可以在堆栈上分配,并且通常会在堆栈上分配,但不一定如此。它们也可以分配在堆上,或者可以分配在寄存器中。 - Eric Lippert
1
如果你对“值类型被分配在栈上”的错误观点更深入的分析感兴趣,这里有两篇我写过的文章:http://blogs.msdn.com/ericlippert/archive/2009/04/27/the-stack-is-an-implementation-detail.aspx 和 http://blogs.msdn.com/ericlippert/archive/2009/05/04/the-stack-is-an-implementation-detail-part-two.aspx - Eric Lippert
2个回答

7
首先:这个说法正确吗?
是的。一旦作用域结束,原始数据就会“消失”。
其次,这个wint的副本存储在哪里?它不能在堆栈上,因为一旦函数完成,它就会消失了(对吧?)。这是否意味着该副本与weightedInts一起存储在堆上?并且在被移除后是否像引用类型的实例一样进行垃圾回收?
您的List<WeightedInt>实例会在堆上创建一个数组。当您将它添加到列表中时,您正在为该数组的一部分分配值类型的副本。该值保存在堆上,作为List类内部的数组的一部分。
当weightedInts成员超出范围时,它将变得未根据,并有资格进行垃圾回收。在此之后的某个时间点,GC将运行,并释放与其内部数组相关联的内存,从而释放与wint副本相关联的内存。
编辑:
另外,当您调用:
weightedInts.Remove(wint);

一些事情会发生(与List<T>有关)。

首先,列表会找到第一个等于wint的值类型的实例的索引。然后它调用RemoteAt(index)。

RemoveAt(index)方法基本上标记内部大小减小了一个,然后检查要删除的索引。如果它在列表中间,它实际上会使用Array.Copy将所有值类型实例向上复制一个元素,以"缩小"列表。然后它会清空数组末尾的内存。

数组本身不会缩小,因此删除元素不会释放任何内存。如果您想要回收这个内存(或使其有资格通过GC释放),您需要调用List<T>TrimExcess()。


从你所说的,我可以得出结论,添加到weightedInts中的值将在weightedInts超出范围时被垃圾回收。但对我来说仍不清楚的是,如果我调用weightedInts.Remove(wint);会发生什么 - 它是否一直保留在那里,直到weightedInts被垃圾回收? - Dan Tao
那么一个int列表将被存储在堆中,堆栈上的唯一分配将是指向数组的堆指针? - Ty.
实际上,它是指向List <T>的指针,其中包含指向数组和用于跟踪的整数值的指针,但是是的,这是一个常见的想法。该数组在堆上分配,List <T>内部的数组引用也一样。 - Reed Copsey
@Reed 首先非常好的回答。但严格来说,我认为存储在堆上的不是指向列表的指针,而是引用(在MS实现中,它们基于指针,但不需要,并且没有必要)。 - Rune FS
很有趣;在我发布这个问题时似乎非常令人困惑的事情,现在在经历了一个多月的经验后变得清晰起来。你的答案非常有帮助。 - Dan Tao
显示剩余4条评论

1

有一个常见的误解,即值类型总是分配在堆栈上。

你刚刚展示的例子是值类型分配在堆上的完美例子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接