小言_互联网的博客

C# List<T>的使用

273人阅读  评论(0)

C# List<T>的使用

List是在日常开发中使用异常频繁的一个集合类。List是一个线性的容器,也就是存在List中的数据必须是连续的,一个挨着一个。那么就会有一些疑问,为什么List是线性的呢?创建的时候应该要一个多大的容器呢?太大了肯定是浪费空间,太小了装不下怎么办?集合中的单个元素又占有多大的内存空间呢?

①这个容器到底该设置为多大呢?

看一个例子。假设我们需要一个宠物容器。

 


  
  1. class Program {
  2. private static List<Pet> pets;
  3. internal class Pet
  4. {
  5. }
  6. static void Main(string[] args)
  7. {
  8. pets = new List<Pet>();
  9. Console.WriteLine( "Count : " + pets.Count);
  10. Console.WriteLine( "Capacity : " + pets.Capacity);
  11. while ( true) ;
  12. }
  13. }
  14. }

因为还没有添加数据,所以此时的容量和树木都是0.现在我们向其中添加一个元素


  
  1. static void Main(string[] args)
  2. {
  3. pets = new List<Pet>( 0);
  4. Console.WriteLine( "Count : " + pets.Count);
  5. Console.WriteLine( "Capacity : " + pets.Capacity);
  6. Console.WriteLine( "添加一个元素后:");
  7. Pet pet = new Pet();
  8. pets.Add(pet);
  9. Console.WriteLine( "Count : " + pets.Count);
  10. Console.WriteLine( "Capacity : " + pets.Capacity);
  11. while ( true) ;
  12. }

这里就说明List容器的容量只有在对其进行操作之后才会被设置,而且有个默认值的规则。实际上List提供了设置容器容量的构造方法,就是你可以设置这个容器的初始大小。之所以设置默认值为4,实在长期的实践过程中统计出设置这个默认值会得到最优解。

②再来看看第二个问题,每个单位元素所占的内存空间是多大?


  
  1. internal class Pet
  2. {
  3. }
  4. static void Main(string[] args)
  5. {
  6. pets = new List<Pet>( 0);
  7. Console.WriteLine( "Count : " + pets.Count);
  8. Console.WriteLine( "Capacity : " + pets.Capacity);
  9. for ( int i = 0; i < 4; i++)
  10. {
  11. Pet pet = new Pet();
  12. pets.Add(pet);
  13. }
  14. pets.ForEach(PrintAddress);
  15. while ( true) ;
  16. }
  17. /// <summary>
  18. /// 打印对象在内存中的位置
  19. /// </summary>
  20. /// <param name="obj"></param>
  21. private static void PrintAddress(object obj)
  22. {
  23. GCHandle hander = GCHandle.Alloc(obj);
  24. var pin = GCHandle.ToIntPtr(hander);
  25. Console.WriteLine(pin);
  26. }

添加了一个打印内存的方法,并且多添加了几个数据,以便通过元素地址的差值来看出每个元素在内存中所占的内存大小(这里注意Pet是一个空的类型,什么数据都没有)。打印结果如下:

可以看出,每个元素所占的内存大小为8个字节。因为是容器,我们容易臆想我们的数据在容器中是一个叠一个的,就想可比克那样,那么现在单位元素(一片薯片)是因为pet类是空的原因吗,

那我们在Pet上添加一些数据。修改代码如下


  
  1. internal class Pet
  2. {
  3. public string id;
  4. public string name;
  5. public Type type;
  6. public int age;
  7. }

然后我们再次执行修改后的代码,发现每个单位元素的所占的地址空间还是8个字节。所以List中的数据并不是和可比克那样叠加保存的。实际上是每个八位字节只保存了每个单位元素在堆内存中的地址,因为通过new出来的对象都是保存在堆内存中的,因为你无法保证每个单位元素实际需要多少内存空间。而在堆内存里的每个对象就像在池塘里的小鱼一样,但是有一根线连着这一条条小鱼,线的另一端就是List中对应的地址。

③现在的容量是4,如果装满了之后我们再向其中添加元素怎么办?


  
  1. static void Main(string[] args)
  2. {
  3. pets = new List<Pet>( 0);
  4. Console.WriteLine( "Count : " + pets.Count);
  5. Console.WriteLine( "Capacity : " + pets.Capacity);
  6. Console.WriteLine( "添加四个元素 ");
  7. for ( int i = 0; i < 4; i++)
  8. {
  9. Pet pet = new Pet();
  10. pet.age = i;
  11. pets.Add(pet);
  12. }
  13. Console.WriteLine( "Count : " + pets.Count);
  14. Console.WriteLine( "Capacity : " + pets.Capacity);
  15. pets.ForEach(PrintAddress);
  16. Console.WriteLine( "再添加四个元素 ");
  17. for ( int i = 0; i < 4; i++)
  18. {
  19. Pet pet = new Pet();
  20. pet.age = i + 4;
  21. pets.Add(pet);
  22. }
  23. Console.WriteLine( "Count : " + pets.Count);
  24. Console.WriteLine( "Capacity : " + pets.Capacity);
  25. pets.ForEach(PrintAddress);
  26. while ( true) ;
  27. }

我们发现再向其中添加元素的时候,容量被自动拓展了。这是因为在向List中Add元素的时候会先检测是否满了,如果满了就会根据拓展规则来拓展。

List自动拓展规则:当前容量X2(当然<Int最大值),但是我们能够看到在长度由4变为8的时候,整个List的地址其实是发生了变化的,将List之前的数据全部复制到一个容量更大的新的容器中,当然我们的pets的地址会更改为新的地址。

这里就有一个小的细节值得注意,频繁扩容一定是消耗性能的,毕竟要将数据完全复制到新的内存中,所以如果能够粗略估计数据的规模,在初始化的时候可以给出一个合理的初始容量,以避免频繁扩容。以下是List的扩容的源码,能够看到其规则。


  
  1. // Adds the given object to the end of this list. The size of the list is
  2. // increased by one. If required, the capacity of the list is doubled
  3. // before adding the new element.
  4. //
  5. public void Add(T item) {
  6. if (_size == _items.Length) EnsureCapacity(_size + 1);
  7. _items[_size++] = item;
  8. _version++;
  9. }
  10. // Ensures that the capacity of this list is at least the given minimum
  11. // value. If the currect capacity of the list is less than min, the
  12. // capacity is increased to twice the current capacity or to min,
  13. // whichever is larger.
  14. private void EnsureCapacity(int min) {
  15. if (_items.Length < min) {
  16. int newCapacity = _items.Length == 0? _defaultCapacity : _items.Length * 2;
  17. // Allow the list to grow to maximum possible capacity (~2G elements) before encountering overflow.
  18. // Note that this check works even when _items.Length overflowed thanks to the (uint) cast
  19. if (( uint)newCapacity > Array.MaxArrayLength) newCapacity = Array.MaxArrayLength;
  20. if (newCapacity < min) newCapacity = min;
  21. Capacity = newCapacity;
  22. }
  23. }

 

List源码里还有一些值得学习的小技巧:

比如我们想删除掉pets表中age为偶数的元素,我们可能会这样去写。


  
  1. private static void RemovePets() {
  2. for ( int i = 0; i <pets.Count; i++)
  3. {
  4. if (pets[i].age % 2 == 0) {
  5. pets.RemoveAt(i);
  6. Console.WriteLine( "------------------------ " + i);
  7. pets.ForEach(PrintAddress);
  8. }
  9. }
  10. }

从打印的结果可以看出,再没删除一次,pets中的所有数据的地址都发生了改变,也就是说每次删除一个元素都对所有的元素进行了一次集体搬家,这样不需要删除的元素愣生生搬家了四次。这显然是不合理的,合理的办法应该是先找出所有的需要删除的元素,

再对剩余的元素搬家一次。List提供了RemoveAll方法,就是干这事的。所以代码可以修改为


  
  1. private static void RemovePets() {
  2. pets.RemoveAll((pet)=> { return pet.age % 2 == 0; });
  3. }

多次搬家显然是耗时的,我们这里测试一下两种方法耗时的时间上到底有多大的差别:


  
  1. private static List<Pet> pets;
  2. public void Use() {
  3. pets = new List<Pet>( 0);
  4. for ( int i = 0; i < 100000; i++)
  5. {
  6. Pet pet = new Pet();
  7. pet.age = i;
  8. pets.Add(pet);
  9. }
  10. Console.WriteLine( "pets.count : " + pets.Count);
  11. DateTime beforeDT = System.DateTime.Now;
  12. //耗时代码//
  13. RemovePets1();
  14. DateTime afterDT = System.DateTime.Now;
  15. TimeSpan ts = afterDT.Subtract(beforeDT);
  16. Console.WriteLine( "need : " + ts.TotalMilliseconds);
  17. Console.WriteLine( "pets.count : " + pets.Count);
  18. }
  19. private void RemovePets2()
  20. {
  21. for ( int i = pets.Count - 1; i > 0; i--)
  22. {
  23. if (pets[i].age % 2 == 0)
  24. {
  25. pets.RemoveAt(i);
  26. }
  27. }
  28. }
  29. private void RemovePets1() {
  30. for ( int i = 0; i < pets.Count; i++)
  31. {
  32. if (pets[i].age % 2 == 0) {
  33. pets.RemoveAt(i);
  34. }
  35. }
  36. }
  37. private static void RemovePets()
  38. {
  39. pets.RemoveAll((pet) => { return pet.age % 2 == 0; });
  40. }

为了看出差别,我们将数据量扩展为100000,使用RemovePets1删除偶数的耗时如下图:多次测试都在750ms左右,也可以用RemovePets2测试一下,大概在360ms左右

我们将上述代码中的RemovePets1改用RemovePets() ,结果如下:多次测试都在3ms左右。相差甚远。

 

当然更重要的是去学会人家怎么实现的,那就去看看源码,这个过程就慢慢嚼吧,是一个耐人寻味的过程,说难吧,也不难,但是真的是很细致。


  
  1. // This method removes all items which matches the predicate.
  2. // The complexity is O(n).
  3. public int RemoveAll(Predicate<T> match) {
  4. if( match == null) {
  5. ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  6. }
  7. Contract.Ensures(Contract.Result< int>() >= 0);
  8. Contract.Ensures(Contract.Result< int>() <= Contract.OldValue(Count));
  9. Contract.EndContractBlock();
  10. int freeIndex = 0; // the first free slot in items array
  11. // Find the first item which needs to be removed.
  12. while( freeIndex < _size && !match(_items[freeIndex])) freeIndex++;
  13. if( freeIndex >= _size) return 0;
  14. int current = freeIndex + 1;
  15. while( current < _size) {
  16. // Find the first item which needs to be kept.
  17. while( current < _size && match(_items[current])) current++;
  18. if( current < _size) {
  19. // copy item to the free slot.
  20. _items[freeIndex++] = _items[current++];
  21. }
  22. }
  23. Array.Clear(_items, freeIndex, _size - freeIndex);
  24. int result = _size - freeIndex;
  25. _size = freeIndex;
  26. _version++;
  27. return result;
  28. }

源码:https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,49c519bce0cdbd82


转载:https://blog.csdn.net/Struugle_Guy/article/details/116200936
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场