C#中List<T>底层原理剖析

本文详细解析了C#中List类的底层工作原理,包括其构造方法、容量管理、添加、移除、插入、清除、查找、排序等功能,并指出其内存分配和线程安全问题。
摘要由CSDN通过智能技术生成


人物

1. 基础用法

list.Max() 取最大元素

list.Avgage() 取平均值

static void Main()
{
    List<string> names = new List<string>();
    names.Add("一号元素");
    names.Add("二号元素");
    names.Add("狗");
    names[2] = "三号元素";//修改第三个元素

    string[] str = new string[] { "四号元素", "五号元素", "六号元素" };
    names.AddRange(str);//为集合增加数组

    Console.WriteLine("当前集合元素个数为{0}",names.Count);//返回集合元素个数

    Console.WriteLine("当前集合的容量为{0}", names.Capacity);//返回当前集合的容量
    //当添加元素的时候集合的容量不足以容纳所有元素就会自动增加目前元素数一倍的容量。

    Console.WriteLine(names.Contains("三号元素"));//返回集合中是否存在某元素,bool类型

    names.IndexOf("三号元素");//返回元素的索引值

    names.Clear();//清空所有元素,元素个数为0,但是容量不变
}

2. List的Capacity与Count:

  • Count 属性表示 List 中实际包含的元素数量。它是一个只读属性。
  • Capacity 属性表示 List 内部数组的容量,即它可以容纳的元素的数量。容量是指分配给列表的内部数组的大小,而不是列表中实际包含的元素数量。
  • 当添加元素时,如果内部数组的容量不够,List 会自动调整容量以容纳更多的元素。
List<int> list1= new List<int>();
WriteLine($"List的初始容量:{list1.Capacity}"); 
WriteLine($"List的初始数量:{list1.Count}");
list1.Add(0);
WriteLine($"通过Add()添加一个元素后的容量:{list1.Capacity}");
WriteLine($"通过Add()添加一个元素后的数量:{list1.Count}");

以上代码输出:

List的初始容量:0
List的初始数量:0
通过Add()添加一个元素后的容量:4
通过Add()添加一个元素后的数量:1

3.List的底层原理

3.1. 构造方法

public class List<T> : IList<T>, System.Collections.IList, IReadOnlyList<T>
{
    private const int _defaultCapacity = 4;

    private T[] _items;
    [ContractPublicPropertyName("Count")]
    private int _size;
    private int _version;
    [NonSerialized]
    private Object _syncRoot;
    
    static readonly T[]  _emptyArray = new T[0];        

    public List() 
    {
        _items = _emptyArray;
    }

    public List(int capacity) 
    {
        if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity, ExceptionResource.ArgumentOutOfRange_NeedNonNegNum);
        Contract.EndContractBlock();

        if (capacity == 0)
            _items = _emptyArray;
        else
            _items = new T[capacity];
    }
    //其他部分……
}

仔细阅读上面的源码可以看出:

  • List内部是由数组实现的,也就是代码中的_items;
  • 当没有指定额定容量时,将调用第一个构造方法,并得到一个容量为0的数组: _items = _emptyArray;。
  • 如果在创建实例时指定容量(capacity),将调用第二个构造方法,实际就是创建一个capacity大小的数组。

普通数组必须在声明时指定大小,那么问题来了:

  • List<>如何实现扩容?
  • 如何新增元素?
  • 如何实现元素的删除?
  • 如何实现元素的插入?

其实List<>的做法很简单,总结起来就是四个字:
简单粗暴!

3.2 Add()接口

将给定对象添加到此列表的末尾。每次增加元素前,都会判断数组容量是否够。不够则进行扩容

public void Add(T item)
  • 每次扩容都会创建一个容量翻倍的新数组。刚开始数组初始值为0,第一次扩容的初始值为4,因此整个扩容路线是:0-4-8-16-32-64-125-256……
  • 每次扩容都会申请新的空间,然后进行元素拷贝,然后还要回收旧空间。会给GC造成不小的负担。
  • 另外还有浪费空间的缺点。比如存放126个元素时,会扩容到256的空间,剩下的130个空间就浪费了。

3.3 Remove()接口

删除列表中首次出现的item。将从头到尾搜索,使用Object.Equal()进行相等的判断。

public bool Remove(T item)
  • 找到要删除的位置后,使用Array.Copy()进行覆盖;
  • 时间复杂度O(n);
  • 删除一个元素后,内部数组的容量不变。也就是说,如果你现在的容量是1024,但是即使你把全部的元素全部Remove掉,它的容量还是1024。

3.4 Inster()接口

功能:在给定索引处插入元素。每次插入元素前都会检查当前容量是否足够,如果不够则进行扩容。

public void Insert(int index, T item);
  • 在插入元素时,采用的也是复制数组的形式,将数组指定索引后面的元素向后移动一个位置。
  • 与Add()类似,扩容会给GC产生负担,并且存在空间浪费的问题。

3.5 Clear()接口

功能:清除列表内容。注意:只是将Count置,之前申请的空间不变,也就是Capacity不变。

List<int> list1= new List<int>();
list1.Add(0);
WriteLine($"通过Add()添加一个元素后的容量:{list1.Capacity}"); 
WriteLine($"通过Add()添加一个元素后的数量:{list1.Count}");
list1.Clear();
WriteLine($"Clear后的容量:{list1.Capacity}"); //清除后容量不变。即之前申请的空间不变
WriteLine($"Clear后的数量:{list1.Count}"); //数量置0

以上输出:

通过Add()添加一个元素后的容量:4
通过Add()添加一个元素后的数量:1
Clear后的容量:4
Clear后的数量:0

3.6 Contains()接口

功能:确定某元素是否在List中。

public bool Contains(T item)
  • 使用线性查找的方式,挨个比较元素。

3.7 ToArray()接口

复制列表到一个新数组,先申请一个大小一样的数组,再一个一个地复制,O(n)的操作。

public T[] ToArray()

3.8 Find()接口

功能:查找满足指定条件的元素。接受一个Predicate委托作为参数,该委托定义了要应用于每个元素的条件。

public T Find(Predicate<T> match)
  • 线性查找的方式,挨个比较,返回满足条件的第一个元素。
  • 用法:
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
    // 使用Find函数查找大于3的第一个元素
    int result = numbers.Find(x => x > 3); //Lambda表达式x => x > 3表示条件
    Console.WriteLine(result);  // 输出: 4
    

3.8 Sort()接口

功能:对列表中的元素进行排序。Sort 方法使用元素的默认比较器进行排序,或者可以传递一个自定义的比较器作为参数。

public void Sort(int index, int count, TComparee<T> comparer)
  • index与count是指定排序区间。不指定的话则对整个列表进行排序;
  • 用法
    List<int> numbers = new List<int> { 5, 2, 8, 1, 3 };
    numbers.Sort();
    numbers.Sort((x, y) => y.CompareTo(x));// 使用自定义比较器对列表进行降序排序
    
  • 该方法在原地修改列表,而不是返回排序后的列表。
  • 排序时使用Array.Sort()方法进行排序,该方法内部采用了快速排序,效率是O(nlogn).

4. 总结

  1. List的效率并不高,甚至比数组还差,只是通用性强而已;
  2. List 的内存分配方式也不合理。当List 里的元素不断增加时,会多次重新分配数组,导致原来的数组被抛弃,造成回收的压力。
  3. 对于第2点的问题,我们可以在创建List 实例时提前告知 List 对象最多会有多少元素在里面,这样 List 就不会因为空间不够而抛弃原有的数组去重新申请教组了。例如:
    List<int> list11 = new List<int>(128);
    WriteLine($"容量:{list11.Capacity}"); //容量:128
    WriteLine($"数量:{list11.Count}"); //数量:0
    
  4. List是线程不安全的。
    • 当多个线程同时访问和修改同一个 List 实例时,可能会导致不可预测的结果或发生错误。
    • 例如,并发读写的问题。一个线程正在读取列表的元素,而另一个线程在同时修改列表,这可能导致读取到无效或不正确的数据。
    • 可使用同步机制解决该问题。lock语句或其他同步机制来保护对 List 的并发访问。确保同一时间只有一个线程访问列表。

5. 参考

List源码地址:链接: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs

  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值