Обобщения
Обобщённые типы и методы, ограничения, IEnumerable и IQueryable, обобщённые коллекции.
7 вопросов
JuniorТеорияОчень частоЧто такое обобщения в C# и какую проблему они решают по сравнению с object?
Что такое обобщения в C# и какую проблему они решают по сравнению с object?
Обобщения параметризуют тип или метод аргументом типа T, поэтому одно определение вроде List<T> работает с любым типом, оставаясь строго типизированным. Это даёт типобезопасность на этапе компиляции и переиспользование кода без приведений object и упаковки значимых типов. JIT специализирует код под каждый значимый тип, сохраняя эффективность.
Типичные ошибки
- ✗Считать обобщения лишь контейнерами на
objectс приятным синтаксисом, упуская проверку типов на этапе компиляции - ✗Полагать, что C# стирает обобщённые типы во время выполнения, как Java, хотя
Tостаётся овеществлённым и восстановимым - ✗Думать, что
List<int>упаковывает элементы, хотя специализация JIT держит значимые типы без упаковки
Уточняющие вопросы
- →Чем специализация обобщений CLR отличается для ссылочных и значимых типов?
- →Почему
Tможно восстановить через рефлексию в C#, но не в стёртых обобщениях Java?
JuniorТеорияЧастоЧто дают List<T>, Dictionary<TKey,TValue> и HashSet<T> по сравнению с ArrayList?
Что дают List<T>, Dictionary<TKey,TValue> и HashSet<T> по сравнению с ArrayList?
List<T> — растущий массив, Dictionary<TKey,TValue> — хеш-словарь по ключу, HashSet<T> — множество уникальных элементов. Будучи обобщёнными, они хранят один известный тип, поэтому компилятор обеспечивает типобезопасность, а значимые элементы избегают упаковки. Старые ArrayList/Hashtable хранили object, требуя приведений и упаковки каждого значения.
Типичные ошибки
- ✗Считать обобщённые коллекции лишь синтаксическим сахаром над
ArrayList, упуская их проверку типов на этапе компиляции - ✗Думать, что хранение
intвList<int>всё ещё упаковывает, хотя обобщения держат значимые элементы без упаковки - ✗Полагать, что при чтении из
Dictionary<TKey,TValue>нужно приведение, как было сHashtable
Уточняющие вопросы
- →Когда вы выберете
SortedDictionary<TKey,TValue>вместоDictionary<TKey,TValue>? - →Как
HashSet<T>решает, равны ли два элемента?
JuniorТеорияЧастоКакой контракт задаёт IEnumerable<T> и как foreach его использует?
Какой контракт задаёт IEnumerable<T> и как foreach его использует?
IEnumerable<T> объявляет единственный GetEnumerator(), возвращающий перечислитель с MoveNext() и Current. foreach вызывает GetEnumerator(), затем в цикле вызывает MoveNext(), читая Current. Это модель однонаправленного перебора только для чтения и основа ленивых, отложенных последовательностей, выдающих элементы по требованию.
Типичные ошибки
- ✗Считать, что
IEnumerable<T>гарантируетCountили индексатор, хотя он обещает лишь последовательный доступ вперёд - ✗Думать, что вся последовательность строится заранее, упуская, что элементы могут выдаваться лениво по требованию
- ✗Путать
Current/MoveNext()перечислителя с членами самой коллекции
Уточняющие вопросы
- →Как метод с
yield returnреализует этот контракт без написанного вручную перечислителя? - →Почему повторный перебор одного запроса
IEnumerable<T>может заново выполнить его работу?
MiddleТеорияЧастоЧто делают ограничения обобщений where T: и какие их виды бывают?
Что делают ограничения обобщений where T: и какие их виды бывают?
Условие where T: ограничивает допустимые аргументы типа, чтобы тело метода могло использовать эти гарантированные возможности. Виды: class и struct (ссылочный или значимый тип), new() (публичный конструктор без параметров), базовый класс, интерфейс и notnull. С where T: IComparable<T> можно вызвать CompareTo на значении T.
Типичные ошибки
- ✗Считать, что ограничения проверяются во время выполнения, хотя компилятор отвергает неверные аргументы заранее
- ✗Полагать, что на параметр допустимо лишь одно ограничение, хотя несколько можно объединить в одном условии
- ✗Забывать, что без ограничения тело может трактовать
Tлишь какobject, не вызывая его члены
Уточняющие вопросы
- →Почему
new()должно идти последним при сочетании с другими ограничениями на тот жеT? - →Чем
where T: structотличается отwhere T: notnullдля значимого типа?
MiddleТеорияЧастоЧем обобщённый метод отличается от обобщённого типа и как обычно задаётся T?
Чем обобщённый метод отличается от обобщённого типа и как обычно задаётся T?
Обобщённый метод объявляет собственные параметры типа в угловых скобках после имени, независимо от параметров типа содержащего класса — даже необобщённый класс может его иметь. Компилятор обычно выводит T из типов аргументов в месте вызова, поэтому <T> редко пишут явно; его указывают, лишь когда вывод невозможен.
Типичные ошибки
- ✗Считать, что обобщённый метод объявит лишь обобщённый класс, хотя его может иметь и необобщённый класс
- ✗Всегда прописывать
<T>в месте вызова, не зная, что компилятор выводит его из аргументов - ✗Путать собственные параметры типа метода с параметрами охватывающего типа
Уточняющие вопросы
- →Почему компилятор не может вывести
Tлишь из возвращаемого типа обобщённого метода? - →Как разрешение перегрузок взаимодействует с выводом аргументов типа обобщения?
MiddleТеорияИногдаКакое значение даёт default(T) и зачем оно нужно для открытого T?
Какое значение даёт default(T) и зачем оно нужно для открытого T?
default(T) (или просто default) даёт нулевое значение типа: null для ссылочных типов, 0/false для числовых и bool значимых типов и обнулённый экземпляр для структуры. Оно нужно потому, что внутри обобщения нельзя написать литерал, подходящий каждому возможному T, но всё равно требуется корректное начальное или резервное значение этого типа.
Типичные ошибки
- ✗Полагать, что
default(T)всегдаnull, забывая, что для значимых типов это0/false/обнулённая структура - ✗Думать, что
default(T)вызывает конструктор или инициализаторы полей, хотя оно лишь обнуляет память - ✗Считать, что для открытого
Tможно написать литерал вроде0илиnullвместоdefault
Уточняющие вопросы
- →Чем
defaultдля nullable значимого типаint?отличается отdefault(int)? - →Почему целетипизированная форма
defaultпозволяет опускатьTво многих контекстах?
SeniorТеорияИногдаЧем IEnumerable<T> отличается от запросного интерфейса IQueryable<T> при выполнении LINQ?
Чем IEnumerable<T> отличается от запросного интерфейса IQueryable<T> при выполнении LINQ?
IEnumerable<T> выполняет LINQ в памяти: его операторы — скомпилированные делегаты, перебирающие объекты локально (LINQ-to-Objects). IQueryable<T> вместо этого строит дерево выражений, которое провайдер транслирует — например в SQL — и выполняет у источника, так что фильтрация и постраничность уходят на сторону источника. Вызов оператора IEnumerable<T> на запросе к БД сначала тянет все строки в память.
Типичные ошибки
- ✗Путать, какой интерфейс транслируется в SQL, приписывая дерево выражений
IEnumerable<T> - ✗Рано вызывать оператор
IEnumerable<T>, случайно затягивая всю таблицу в память до фильтрации - ✗Считать, что оба откладывают одинаково, упуская, что лишь
IQueryable<T>спускает работу источнику
Уточняющие вопросы
- →Что происходит, когда провайдер
IQueryable<T>не может транслировать пользовательский метод в дереве выражений? - →Как запросный интерфейс
IQueryable<T>определяет, где проходит граница перехода в память в смешанном запросе?