What is the behind of List<T>
The List<T>
class in .NET is a dynamic array-based collection that automatically resizes itself as elements are added or removed. Here's a deeper look into how List<T>
works, especially focusing on the internal mechanics behind the scenes, such as the dynamic resizing (or "growing") of the underlying array.
Behind List<T>
:
- Internal Array:
List<T>
is essentially a wrapper around an internal array of typeT[]
.- This array starts with an initial capacity (which can be specified when the list is created or defaults to a small size).
- Dynamic Resizing (Growing):
- Automatic Resizing: When elements are added to the
List<T>
, and the internal array becomes full, the list automatically resizes itself to accommodate more elements. - Capacity vs. Count:
- Count: The number of elements currently in the
List<T>
. - Capacity: The size of the internal array, which may be larger than
Count
to allow for future growth without immediate resizing.
- Count: The number of elements currently in the
- Automatic Resizing: When elements are added to the
- The Grow Mechanism:
- Doubling Strategy: When the list grows beyond its current capacity, it typically doubles the size of the internal array. This strategy balances the trade-off between memory usage and the cost of resizing.
- Reallocation and Copying: When the array needs to grow:
- A new, larger array is allocated (usually twice the size of the current array).
- The existing elements are copied from the old array to the new one.
- The old array is then discarded (and eventually garbage-collected).
- Ensuring Capacity: The
EnsureCapacity
method is often called internally before adding elements to ensure there's enough space in the internal array. If the current capacity is insufficient, the array is resized.
- Efficiency Considerations:
- Amortized O(1) Add: Although resizing is expensive (O(n) due to copying elements), it happens infrequently. Therefore, adding elements to a
List<T>
is considered O(1) on average, or "amortized O(1)." - Memory Overhead: The doubling strategy leads to some extra memory overhead because the internal array often has more space than the current number of elements.
- Amortized O(1) Add: Although resizing is expensive (O(n) due to copying elements), it happens infrequently. Therefore, adding elements to a
Key Methods Involved in Growth:
- Add Method
public void Add(T item)
{
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size++] = item;
_version++;
}
- When you add an item to the list using
Add
, the method checks if the current array has enough capacity to hold the new item. If not, it triggers the resizing process. - EnsureCapacity Method
private void EnsureCapacity(int min)
{
if (_items.Length < min)
{
int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2;
if ((uint)newCapacity > Array.MaxLength) newCapacity = Array.MaxLength;
if (newCapacity < min) newCapacity = min;
Capacity = newCapacity;
}
}
- This method is responsible for ensuring that the list can accommodate a specified number of elements. If the current capacity is insufficient, it increases the capacity.
- DefaultCapacity: The initial capacity when a new list is created without specifying an initial capacity.
- Array.MaxLength: The maximum possible size of an array in .NET (to prevent exceeding memory limits).
- Capacity Property
public int Capacity
{
get { return _items.Length; }
set
{
if (value < _size)
throw new ArgumentOutOfRangeException(nameof(value), SR.ArgumentOutOfRange_SmallCapacity);
if (value != _items.Length)
{
if (value > 0)
{
T[] newItems = new T[value];
if (_size > 0)
{
Array.Copy(_items, 0, newItems, 0, _size);
}
_items = newItems;
}
else
{
_items = s_emptyArray;
}
}
}
}
- You can also manually set the capacity of the
List<T>
using theCapacity
property, which allows you to avoid frequent resizing if you know the expected number of elements in advance.
Summary:
List<T>
in .NET is backed by an array that dynamically grows as needed.- Resizing: The list typically doubles its capacity when it runs out of space, leading to efficient amortized performance but with occasional costly resizing operations.
- Growth Control: Developers can control the capacity manually or let the list handle it automatically, balancing performance and memory usage.
Understanding this internal mechanism helps in writing more efficient code, particularly when working with large collections or performance-sensitive applications.