Делегаты и события
Делегаты, multicast, Func/Action/Predicate, лямбды, связывание и динамическая диспетчеризация.
7 вопросов
JuniorТеорияОчень частоЧто такое делегат в C#, и как он объявляется и вызывается?
Что такое делегат в C#, и как он объявляется и вызывается?
Делегат — это типобезопасная ссылка на метод, типизированный указатель на функцию. Вы объявляете его как тип с фиксированной сигнатурой, присваиваете любой метод, чьи параметры и тип возврата совпадают, затем вызываете переменную-делегат как метод, чтобы выполнить целевой метод. Компилятор проверяет сигнатуры, поэтому несовпадающий метод отвергается.
Типичные ошибки
- ✗Путать делегат с интерфейсом — делегат ссылается на один метод, а не на контракт целого типа
- ✗Считать, что можно присвоить любой метод, игнорируя, что сигнатура должна совпадать с типом делегата
- ✗Думать, что делегат копирует тело метода, а не хранит ссылку на целевой метод
Уточняющие вопросы
- →Чем делегат отличается от встроенных делегатов
FuncилиAction? - →Что хранит экземпляр делегата кроме метода — захватывает ли он целевой объект?
JuniorТеорияОчень частоЧто такое лямбда-выражение в C#, и что такое замыкание?
Что такое лямбда-выражение в C#, и что такое замыкание?
Лямбда вроде x => x * 2 — это встроенная анонимная функция: компактный синтаксис => для тела метода, присваиваемого подходящему делегату или дереву выражений. Её тело может читать и менять переменные из охватывающей области, а не только свои параметры. Когда она захватывает такую внешнюю переменную, она становится замыканием, и захваченная переменная живёт, пока жива лямбда.
Типичные ошибки
- ✗Считать, что лямбда трогает только свои параметры, а не переменные охватывающей области
- ✗Полагать, что замыкание снимает копию захваченных переменных, а не делит живую переменную
- ✗Забывать, что лямбда — значение, которое надо присвоить делегату или дереву выражений
Уточняющие вопросы
- →Почему захваченная лямбдой переменная цикла может удивить между итерациями?
- →Когда лямбда компилируется в дерево выражений, а не в исполняемый код делегата?
JuniorТеорияЧастоЧто такое встроенные делегаты Func, Action и Predicate?
Что такое встроенные делегаты Func, Action и Predicate?
Это обобщённые типы делегатов из стандартной библиотеки, поэтому свои объявляют редко. Func<...,TResult> принимает ноль или больше входов и возвращает значение TResult. Action<...> принимает входы, но возвращает void. Predicate<T> принимает один T и возвращает bool, используется для проверок вроде фильтрации. Они делают сигнатуры единообразными и избавляют от шаблонных объявлений делегатов.
Типичные ошибки
- ✗Путать
FuncиAction— забывать, чтоActionвозвращаетvoid, аFunc— значение - ✗Объявлять свой тип делегата, когда уже подходит встроенный
Func,ActionилиPredicate - ✗Думать, что
Predicate<T>может вернуть любой тип, а не всегдаbool
Уточняющие вопросы
- →Почему
Predicate<T>на практике почти взаимозаменяем сFunc<T, bool>? - →Где в списке параметров стоит последний тип-аргумент
Func<T1, T2, TResult>?
MiddleТеорияИногдаЧто такое анонимные методы, и как они связаны с лямбдами и замыканиями?
Что такое анонимные методы, и как они связаны с лямбдами и замыканиями?
Анонимный метод использует старый синтаксис delegate { ... }, задавая встроенное тело делегата без имени метода; лямбда — новая, более краткая форма той же идеи. Оба компилируются в скрытый метод и присваиваются там, где ожидается делегат. Оба также образуют замыкания — захватывают внешние переменные по ссылке, разделяя живую переменную, а не копию, поэтому изменения видны с обеих сторон.
Типичные ошибки
- ✗Считать анонимные методы и лямбды несвязанными, а не двумя синтаксисами встроенных делегатов
- ✗Полагать, что захваченные переменные копируются по значению, а не делятся по ссылке как замыкание
- ✗Считать, что анонимный метод нельзя хранить в делегате или захватить внешнее состояние
Уточняющие вопросы
- →Когда сегодня вы всё ещё выберете синтаксис
delegate { }вместо лямбды? - →Почему захват одной переменной по ссылке в цикле может вызвать тонкую ошибку?
MiddleТеорияИногдаВ чём разница между ранним (статическим) и поздним связыванием в C#?
В чём разница между ранним (статическим) и поздним связыванием в C#?
Связывание — это сопоставление вызова метода с реализацией. Раннее, или статическое, связывание разрешает вызов на этапе компиляции по объявленному типу переменной, давая проверку типов, IntelliSense и быстрые прямые вызовы. Позднее связывание откладывает разрешение до выполнения по фактическому объекту — через dynamic или рефлексию, — так что член не обязан быть известен при компиляции. Вы меняете безопасность и скорость на гибкость.
Типичные ошибки
- ✗Считать позднее связывание быстрее, хотя оно пропускает проверки компиляции и добавляет накладные расходы
- ✗Думать, что
dynamicвсё равно даёт проверку типов и IntelliSense, как статический тип - ✗Полагать, что время связывания решает значимый-vs-ссылочный тип, а не объявленный тип
Уточняющие вопросы
- →Какое исключение времени выполнения заменяет ошибку компиляции, когда члена
dynamicнет? - →Чем разрешение перегрузок различается у статически типизированного и
dynamic-вызова?
MiddleТеорияИногдаЧто такое multicast-делегат, и как ведёт себя его список вызова?
Что такое multicast-делегат, и как ведёт себя его список вызова?
Каждый делегат хранит список вызова целей. += добавляет метод, а -= удаляет, поэтому один делегат может ссылаться на многие методы. Вызов выполняет каждую цель в порядке добавления. Для делегата с возвратом значения вы получаете результат только последней цели; ранние возвраты отбрасываются. Если любая цель бросает исключение, вызов прекращается и оставшиеся методы не выполняются.
Типичные ошибки
- ✗Ждать все возвращаемые значения, не понимая, что выживает только результат последней цели
- ✗Считать, что брошенное исключение проглатывается и остаток списка всё равно выполняется
- ✗Думать, что
+=заменяет цель, а не дописывает её в список вызова
Уточняющие вопросы
- →Как вызвать каждую цель вручную, чтобы собрать все возвраты или изолировать исключения?
- →Что делает
-=, если один метод встречается в списке вызова несколько раз?
SeniorТеорияРедкоЧто такое динамическая диспетчеризация, и чем она отличается от статической?
Что такое динамическая диспетчеризация, и чем она отличается от статической?
Динамическая диспетчеризация выбирает выполняемую реализацию метода по фактическому типу объекта во время выполнения, а не по объявленному типу ссылки. Для virtual-методов среда выполнения ищет цель через таблицу методов объекта, поэтому переопределённая версия подкласса выбирается даже через ссылку базового типа. Статическая диспетчеризация же фиксирует цель при компиляции по объявленному типу, поэтому невиртуальные вызовы нельзя переопределить полиморфно.
Типичные ошибки
- ✗Думать, что диспетчеризация идёт по объявленному типу ссылки, а не по фактическому типу объекта
- ✗Считать, что все методы диспетчеризуются динамически, забывая, что невиртуальные связываются статически
- ✗Путать динамическую диспетчеризацию с поздним связыванием через
dynamic, рефлексию или DLR
Уточняющие вопросы
- →Как таблица методов делает
virtual-вызов лишь немного медленнее прямого? - →Что
sealedна переопределении позволяет JIT сделать с ранее виртуальным местом вызова?