Перейти к содержимому

C# Особенности работы List

Исходный код: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/list.cs,d2ac2c19c9cf1d44

Конструктор

У класса List есть 3 конструктора:

  1. Без аргументов;
  2. С числовым аргументом int capacity;
  3. С перечислением IEnumerable collection;

Первые два работают схоже — создается внутренний массив _items типа T с нужным размером (0, либо capacity).

Третий (IEnumerable collection) работает немного сложнее.

Алгоритм можно посмотреть ниже:

  1. Вначале IEnumerable<T> collection преобразуется к ICollection<T>;
  2. Далее логика зависит от того — удалось ли преобразование:
    1. Удалось — значит используем метод CopyTo из одного массива в другой (всё тот же _items);
    2. Не удалось — получаем энумератор и итерируя его и заполняем _items;

Работа с данными

Работа через индекс

Чтение — возвращает просто данные из _items по индексу:

Запись — записывает данные в массив _items по индексу и увеличивает версию :

Метод Add

Тут сложнее логика.

  1. Идёт вызов метода EnsureCapacity(_size + 1);
  2. Идёт присвоение в массив items по индексу _size + 1: _items[_size++] = item;

Далее не получится продолжить не рассмотря метод EnsureCapacity.

EnsureCapacity

На вход данный метод получает аргумент int min.

Стоит уточнить, что логика данного метода работает исключительно в случае, когда количество элементов в массиве _items меньше, чем указано в аргументе min.

Новая размерность рассчитывается по следующему алгоритму:

  1. Если в массиве _items отсутствуют элементы — устанавливается стандартная размерность: private const int _defaultCapacity = 4;
  2. Если нет — текущая размерность массива _items умножается на два;
  3. Если полученное число больше 2146435071 (Array.MaxArrayLength), то оно заменяется этим числом;
  4. Если полученное число меньше, чем требуемое в аргументе (int min), то оно заменяется этим числом;
  5. Полученное число устанавливается в свойство this.Capacity.

Вышел интересный алгоритм, с ним поиграемся далее, пока нужно посмотреть логику setter’а у свойства this.Capacity.

Capacity (get; set;)

Getter у данного свойства простой — он возвращает _items.Length. Вот с Setter'ом всё сложнее.

Если новое значение менее _size (фактическое количество элементов), то будет ошибка.

Если новое значение равно _size, то на этом логика работы setter’а завершается.

Остается последний путь — когда новое значение более того, что указано в _size.

Создается новый массив с размерностью, которую мы пытаемся присвоить в Setter у свойства Capacity. В новый массив копируются все элементы из _items, а затем в это же поле _items присваевается этот новый массив. Таким образом расширяется внутренний массив в классе List.

Insert

При добавлении идёт вызов EnsureCapacity, на случай, если количество элементов внутри массива равна _size.

Далее, если аргумент index, меньше _size (то есть мы хотим добавить элемент в середину массива), вызывается следующая логика:

Напомню — как выглядит сигнатура данного метода:

То есть из внутреннего массива List’а, начиная с индекса, куда мы хотим записать новый элемент, мы копируем в этот же массив, но со следующего за индексом элемента. А количество элементов — количество элементов за вычетом данного index’а. То есть фактически — мы просто сдвигаем все элементы на единицу вправо. Для того, чтобы в освободившуюся ячейку записать новый элемент.

InsertRange (он же AddRange, который является частным случаем InsertRange )

На вход идёт два аргумента — int index и IEnumerable collection.

Далее идёт уже известная нам логика из третьего конструктора — приведения энумератора к ICollection.

Если это невозможно, то идёт итеративный вызов Insert.

Если возможно, то происходит опять логика, похожая на логику Insert со сдвигами и Array.CopyTo.

В List ещё много интересных методов, но основные описаны в статье. По сути — это обёртка над массивом с кучей удобств.

Немного экспериментов

Конструкторы:

Первый конструктор без аргументов — выставил capacity 0. Элементов внутри массива и не было. Поэтому вывод: 0 / 0.

Второй конструктор с аргументом 0. Это эквивалентно первому примеру. Вывод: 0 / 0. Если бы мы указали в конструкторе 1, то вывод был бы 0 / 1.

Третий конструктор с массивом на входе — подстроил List под массив, поэтому вывод: 3 / 3.

Дальше посмотрим как работает расширение..

Вывод будет следующим:

То есть вначале Capacity был равен 0, однако после первого вызова Add — внутренний массив _items увеличился до 4. При добавлении пятого элемента — он увеличился в два раза до 8.. И так далее при попытках добавить новый элемент при заполненном внутреннем массиве. Если мы бы добавили 65 элементов, то Capacity стало бы 128.

Capacity

Думаю тут всё и так понятно. Добавляем элементы, пока не получим размерность 5/8 (5 элементов в листе, 8 размерность листа).

Затем выставляем Capacity = 5. Так как в листе элементов как раз 5 — это разрешается и мы получаем 5/5.

При попытке установить размерность меньше, чем элементов в листе — мы получаем ошибку ArgumentOutOfRangeException

EnsureCapacity

По сути — это метод-хелпер над Setter‘ом у Capacity.

Он выставляет размерность внутреннего массива исходя из логики:

  1. Если элементов 0, то стандартная стартовая размерность, равная 4;
  2. Если больше, то размерность увеличивается в 2 раза;
  3. Если полученная размерность меньше, чем пришло в аргументе, то используется число из аргумента;

Давайте проведем эксперимент:

Если бы в последнем расширении массива мы бы использовали любое значение от 9 до 16, то размерность бы получилась 16. Но так как мы поставили размерность больше, чем автовычисленные по логике «умножить прошлую размерность на 2», то алгоритм взял то число, которое мы передали аргументом.

Выводы

Ну в первую очередь — лист это удобный и гибкий инструмент, который решает многие проблемы за вас. Его логика работы простая и понятная, а значит понимая, как он работает — можно использовать его на полную. А именно — учитывать определенные нюансы при построении вашего приложения, когда работаете с List.

Основная проблема List‘а — это периодическое расширение массива, который используется у него под капотом, во время заполнения List‘а. А значит — зная хотя бы приблизительно порядок элементов, которые мы планируем добавить в List — мы можем избежать данных накладных расходов с помощью:

  1. Конструктора с аргументом int capacity на входе (var list = new List(1000));
  2. Выставление capacity через setter свойства Capacity (list.Capacity = 1000);
  3. Либо используя метод EnsureCapacity (list.EnsureCapacity(1000)).

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *