heap.tech
лаборатория велосипедов
×

Выживание на собеседование, часть 3. Массивы, коллекции, перечисления, foreach

13 декабря 2016
На собеседованиях любят задавать вопросы на тему как работают коллекции, про итераторы, индексаторы и вообще, какие типы можно использовать в цикле foreach. Практическая ценность таки вопросов стремится к 0 и они, на мой взгляд, задаются в двух случаях: чтобы проверить, насколько кандидат может логически рассуждать или завалить его. Увы, второй вариант встречается чаще, но мы то знаем, что на собеседованиях обычно спрашивают то, что узнали вчера.
Почему ценность этих знаний столь мала? Да просто - на практике необходимость реализации собственных коллекций, перечислений или массивов возникает чуть реже, чем никогда. В .Net уже реализовано все, что нужно для работы и даже больше – универсальные коллекции, перечисления, словари, хеш-таблицы, стек, да тысячи их.
В этой статье я опишу основные принципы коллекций и массивов в .Net с оглядкой на типовые вопросы на собеседовании. Это позволит поставить собеседующего в тупик и разорвать его нежные шаблоны в клочья.

Ссылки на предыдущие уроки выживания
Упаковка и распаковка (boxing, unboxing)
Принципы SOLID (СОЛИД)

Для примера создадим собственную коллекцию с блекджеком и куртизанками индексатором и публичными методами. Спеки будущей коллекции:
1. Быть универсальной (generic <T>)
2. Обладать индексатором
3. Быть юзабильной в конструкции foreach
4. Обладать методами добавления и удаления ячеек (удаление по индексу)

Перед началом создания собственной коллекции создадим класс, который будем использовать в качестве примера в работе с коллекцией. Примитивы типа int, string - не путь джедая, нужно что-то сложнее. Поэтому объект Person
class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } }

Пусть класс Person представляет некую персону, участвующую, например, в конференции. КО намекает, что для хорошей конференции нужно больше, чем один человек, поэтому, для представления списка участников конференции в учетной программе мы создадим универсальную коллекцию, работающую с нашим объектом. Да и не только с ним, коллекция все же универсальная.
Поехали по пунктам.

Создание универсальной коллекции

(generic collection <T>)

class CustomCollection<T> where T : class { T[] _itemsArray; int _itemsArraySize; public CustomCollection() { this._itemsArraySize = 0; this._itemsArray = new T[0]; } public CustomCollection(int size) { this._itemsArraySize = size; this._itemsArray = new T[size]; } }

Универсальный класс CustomCollection, может принимать любой тип, если он является классом (ссылочным значением). Объявлены два конструктора: по-умолчанию (просто инициализирует нашу будущую коллекцию с нулевой длиной) и второй конструктор, принимающий int для создания массива определенной длины.
Пример использования
var conferencePeople = new CustomCollection<Person>(); //или, для инициализации коллекции с 10 пустыми ячейками, зарезервированными для гостей var conferencePeople = new CustomCollection<Person>(10);


Добавить индексатор


Для начала разберемся, что такое индексатор. Для обращения к определенному элементу массива используется конструкция [x] (где x целочисленное значение) – это и есть вызов индексатора. В c# программисту доступно объявлять собственные индексаторы при создании перечислений.
public T this[uint index] { set { if (this._itemsArraySize >= index) this._itemsArray[index] = value; } get { if (this._itemsArraySize >= index) return this._itemsArray[index]; else return null; } }

Индексатор готов, для обращения к конкретной ячейке коллекции можно использовать конструкцию []. Для исключения ошибок, связанных с обращением к несуществующим индексам (индексам за пределами коллекции), введем проверку. Еще стоит обратить внимание, на то, что наша коллекция начинается с нуля, поэтому нужно блокировать обращение к ячейкам по отрицательному индексу (например так conferencePeople[-1]), поэтому uint. Но можно, а может и лучше, вводить проверку на отрицательный index через if.
На практике выглядит так
//создание и инициализация коллекции из одного элемента var conferencePeople = new CustomCollection<Person>(1); //присваивание значения первому (и последнему) элементу conferencePeople[0] = new Person(1,"Ivan", "Ivanovich");

Что такое индексатор?
Индексатор это параметризованное свойство, которое позволяет получать адресный доступ к определенному элементу массива (коллекции) используя ее индекс (порядковый номер ячейки в массиве). Индексатор может использоваться как для получения, так и для установки значения.

Конструкция foreach


В c# конструкция foreach это синтаксический сахар, ВНЕЗАПНО, она (конструкция) разворачивается в цикл while. Разворачивание происходит скрытно, поэтому для программиста все выгядит так, что он как-бы работает c циклом foreach, оперируя объявленным в теле цикла предикатом. На самом деле все совсем не так, а вернее совсем не так. Вот так будет развернута конструкция foreach
var foreachСompatibleСollection = new CustomCollection<Person>(); //конструкция foreach foreach (var predicate in foreachСompatibleСollection) { Console.WriteLine(predicate.Id); } //преобразуется в CustomCollection<Person>.CustomCollectionEnumerator enumerator = foreachСompatibleСollection.GetEnumerator(); while (enumerator.MoveNext()) { var element = enumerator.Current; Console.WriteLine(element.Id); }

Метод GetEnumerator() возвращает объект CustomCollectionEnumerator, у которого есть метод MoveNext() и свойство Current. Далее создается цикл while, он итерационно вызывает метод MoveNext(), его задача проверять, не достигнут ли конец массива и возвращать true/false соответственно. Для этого используется глобальная переменная, в которой хранится текущее состояние (т.е. положение в массиве), её значение каждый раз увеличивается. Эту же переменную использует свойство Current, задача которого возвращать текущую ячейку коллекции. Устройство MoveNext и Current хорошо отражено в листинге
public class CustomCollectionEnumerator { int _currentItem; int _itemsLength; T[] _items; public CustomCollectionEnumerator(T[] items) { this._currentItem = -1; this._itemsLength = items.Length; this._items = items; } public T Current { get { return this._items[this._currentItem]; } } public bool MoveNext() { if (this._currentItem == (this._itemsLength - 1)) { this._currentItem = 0; return false; } this._currentItem++; return true; } }

У класса CustomCollectionEnumerator есть конструктор, принимающий массив с типом T (в нашем случае T это Person). Не трудно догадаться, что метод GetEnumerator коллекции CustomCollection и есть то место, где вызывается конструктор класса CustomCollectionEnumerator. Глобальные переменные _currentItem и _itemsLength служат для хранения текущей позиции курсора и общем количестве элементов соответственно.

Какие типы можно использовать в конструкции foreach?
Любой тип, у которого есть метод GetEnumerator(), возвращающий объект, который, в свою очередь, реализует метод MoveNext и свойство Current. Другими словами – в foreach можно использовать любой тип, если он реализует интефейс IEnumerable с методом GetNumerator, который возвращает любой объект, реализующий IEnumerator.

добавление и удаление элементов в коллекции


Любая коллекция в .NET обладает некоторым набором методов, позволяющим изменять ее размерность (см. интерфейс System.Collections.Generic.ICollection<T>, методы Add, Remove, Clear, а некоторые коллекции имеют еще больший функционал - методы Insert, AddRange, InsertRange, RemoveRange etc). Чтобы не загружать нашу велосипед коллекцию большим количеством кода – добавим только самые основные возможности: добавлять и удалять элементы по индексу (т.е RemoveAt).
public int Add(T item) { Array.Resize<T>(ref this._itemsArray, ++_itemsArraySize); this._itemsArray[this._itemsArraySize - 1] = item; return _itemsArraySize; } public void RemoveAt(int index) { if (index < this._itemsArraySize) { for (var i = index; i < (this._itemsArraySize - 1); i++) { this._itemsArray[i] = this._itemsArray[i + 1]; } Array.Resize(ref this._itemsArray, --this._itemsArraySize); } }

Метод Add() увеличивает размер массива _itemsArray и присваивает значение (точнее ссылку на объект в куче) объекта item в новую ячейку массива. Далее устанавливается новое значение _itemsArraySize, равное увеличенной (на один) длине массива. Это немного ускоряет работу коллекции, индексируя значение её длины (вместо вызова свойства _itemsArray.Length будет возвращено значение переменной).
Метод RemoveAt() работает чуть сложнее - взять элемент на позиции X (переменная index) и двигать его вниз, пока он не станет последним элементом, после уменьшить длину массива _itemsArray на один. И не забываем установить новое значение переменной _itemsArraySize, равное новому размеру массива (просто уменьшаем текущее значение, используя оператор декремента --).

Полный листинг кода CustomCollection
class CustomCollection<T> where T : class { public class CustomCollectionEnumerator { int _currentItem; int _itemsLength; T[] _items; public CustomCollectionEnumerator(T[] items) { this._currentItem = -1; this._itemsLength = items.Length; this._items = items; } public T Current { get { return this._items[this._currentItem]; } } public bool MoveNext() { if (this._currentItem == (_itemsLength - 1)) { this._currentItem = 0; return false; } this._currentItem++; return true; } } T[] _itemsArray; int _itemsArraySize; public CustomCollection() { this._itemsArraySize = 0; this._itemsArray = new T[0]; } public CustomCollection(int size) { this._itemsArraySize = size; this._itemsArray = new T[size]; } public CustomCollectionEnumerator GetEnumerator() { return new CustomCollectionEnumerator(this._itemsArray); } //indexer public T this[uint index] { set { if (this._itemsArraySize >= index) this._itemsArray[index] = value; } get { if (this._itemsArraySize >= index) return this._itemsArray[index]; else return null; } } public int Count { get { return this._itemsArraySize; } } public int Add(T item) { Array.Resize<T>(ref this._itemsArray, ++this._itemsArraySize); this._itemsArray[this._itemsArraySize - 1] = item; return _itemsArraySize; } public void RemoveAt(int index) { if (index < this._itemsArraySize) { for (var i = index; i < (this._itemsArraySize - 1); i++) { this._itemsArray[i] = this._itemsArray[i + 1]; } Array.Resize(ref this._itemsArray, --this._itemsArraySize); } } }
Пример использования
static void Main(string[] args) { var visitors = new CustomCollection<Person>(); visitors.Add(new Person { FirstName = "A", Id = 0, LastName = "aa" }); visitors.Add(new Person { FirstName = "B", Id = 1, LastName = "bb" }); visitors.Add(new Person { FirstName = "C", Id = 2, LastName = "cc" }); visitors.Add(new Person { FirstName = "D", Id = 3, LastName = "dd" }); //попытка выхода за границы массива visitors[31] = new Person(); foreach(var visitor in visitors) Console.WriteLine(visitor.Id); }


Следующий и последний урок выживания
Принципы ООП: инкапсуляция, абстракция, полиморфизм, наследование
 
2853
0

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

пока никто не оставлял комментариев