Каково назначение аргумента sender в Событиях?
Недавно приступил к изучению темы Событий в C#, последняя тема перед оконными приложениями. При объяснении конвенции оформления событий преподаватель множество раз уточнял, что причина существования и самой конвенции, и отдельных решений внутри нее (в частности, того же sender-а) в том, что во времена ее создания в C# еще не было ни LINQ, ни лямбда-выражений, и сейчас с их помощью те же самые задачи можно решать намного эффективнее (ссылка на первоисточник).
В связи с тем, что курс по которому я учусь достаточно старый (создан около 10 лет) у меня вопрос: повлияло ли появление LINQ и лямбда-выражений на правила оформления Событий в C#, и существует ли до сих пор сама конвенция их оформления, или можно использовать LINQ и делегаты на полную?) Заранее спасибо!
Нашел у Майкрософт следующую информацию. Согласно данным соглашениям, использование лямбда-выражений прямо рекомендовано для определения обработчиков событий. Не знаю я, кому верить...
Ответы (3 шт):
Там на видео вроде нормальный чел, говорит хорошо, в темпе, но несёт пургу.
- Как написал @CrazyElf, "
Linq
и лямбды" никакого прямого отношения к элементам управления Windows Forms не имеют. В примере они используются по своему назначению - для обработки перечислений (IEnumerable
). В частности, с помощью этих функций автор получает из строчного массива массив объектов типаMenuItem
. (sender,args)=>CreateReport(name)
- это такая лямбда, которая соответствует сигнатуре обработчика события Click, а именно имеет два аргумента соотвествующих типов. Студия покажет, что тип аргументаsender
-object
, аargs
-EventArgs
. И никакой другой вариант не подойдёт. То естьsender
никуда не девается. Просто эти аргументы в лямбде игнорируются. Кстати, сейчас это можно записать как(_, _) => CreateReport(name)
.- Зачем
sender
? Чтобы в обработчике события был доступ к объекту, который вызвал этот обработчик. Поскольку обработчик - это внешний по отношению к элементу управления метод, то передать в него информацию об этом элементе управления можно только таким способом. Нужна такая информация? Очевидно, нужна. - Лямбда с замыканием компилируется в специальный класс, который имеет поля для значений переменных замыкания, и метод - собственно саму лямбду, которая ссылается на эти поля. Поскольку лямбда находится в методе
Select
, который вызываетя для каждого элемента коллекцииmonthNames
, т. е. 12 раз, то во время выполнения создаётся 12 вспомогательных объектов со значением переменной замыкания, и событиеClick
каждого элемента массиваmenuItems
получает ссылку на метод лямбды своего индивидульного объекта. То есть с помощьюLinq
и лямбда-выражения он автоматизировал создание обработчиков. А вот когда он создалMenuItemSelected
, то это действительно стал единственный обработчик на 12 команд меню. И обработчик берёт название месяца из аргументаsender
.
// это примерно то, что он создал
class MenuItemSelected {
string _name;
public MenuItemSelected(string name) { _name = name;}
public void Lambda(object sender, EventArgs args) => CreateReport(_name);
}
items[0].Click += new MenuItemSelected("январь").Lambda;
items[1].Click += new MenuItemSelected("февраль").Lambda;
// и т. д.
Правильным вариантом будет второй, а дополнительный метод можно убрать:
static void MenuItemSelected(object sender, EventArgs e) => MessageBox.Show($"создаю отчёт за {(sender as MenuItem).Text}...");
...
var menuItems = monthNames.Select(name =>
{
var menuItem = new MenuItem(name);
menuItem.Click += MenuItemSelected;
return menuItem;
}).ToArray();
// или то же самое, но с лямбдой
// здесь она просто порождает анонимный метод без сюрпризов
var menuItems = monthNames.Select(name =>
{
var menuItem = new MenuItem(name);
menuItem.Click += (sender, _) => { MessageBox.Show($"создаю отчёт за {(sender as MenuItem).Text}..."); }
return menuItem;
}).ToArray();
Если для каждой команды меню нужна какая-то дополнительная информация, то класс Control, от которого унаследованы все элементы управления Windows Forms, прелагает свойство Tag, в которое можно записать ссылку на объект с дополнительной информаций.
static void MenuItemSelected(object sender, EventArgs e) {
var mi = sender as MenuItem;
MessageBox.Show($"создаю отчёт за {mi.Text}...");
var to = mi.Tag as ATargetType;
to. ...
}
- Искать что-то в Гугле - это такое... Начинать нужно с документации.
- Про конвенцию. Конвенция с
sender
- это одна из парадигм ООП - инкапсуляция. См. п. 3. - С этим автором программировать вы научитесь, конечно, но понимать, что вы делаете, скорее всего, будете плохо. А так видосы норм.
- Это видео (которое у вас первоисточник) после 7:03 слушать категорически нельзя. Про WPF тоже пурга.
- Кстати, если вы захотите использовать команду меню с галочкой, то менять её состояние в обработчике
Click
безsender
у вас никак не получится, и никакой лямбдой тут не извратишься.
P. S. Зачем нужен EventArgs
? Вы посмотрите что-то посложнее команды меню, например DataGridView. Возьмём событие CellClick, которое в объекте производного от EventArgs
класса сообщает обработчику, на какой именно ячейке случился Click
.
По поводу рекомендаций:
В C# разрешается формировать какие угодно разновидности событий, но ради совместимости программных компонентов следует придерживаться рекомендаций, установленных Microsoft. Эти рекомендации сводятся к следующему требованию: у обработчиков событий должно быть два параметра. Первый — ссылка на объект, формирующий событие, второй — параметр типа EventArgs
, содержащий любую дополнительную информацию о событии, которая требуется обработчику. Таким образом, .NET-совместимые обработчики событий должны иметь следующую общую форму:
void Обработчик(object отправитель, EventArgs данныеСобытия) {
// код
}
отправитель
— это параметр, передаваемый вызывающим кодом с помощью ключевого слова this
. А параметр данныеСобытия
типа EventArgs
(или производного от него типа) содержит дополнительную информацию о событии и может быть проигнорирован, если он не нужен. Класс EventArgs
не содержит поля, которые могут быть использованы для передачи дополнительных данных обработчику. Класс EventArgs
служит в качестве базового класса, от которого получается производный класс, содержащий все необходимые поля. Также EventArgs
имеется одно static
поле Empty
, которое представляет собой объект типа EventArgs
без данных.
Пример кода (проверял на .NET 8):
class Program
{
static void Main(string[] args)
{
// Инициализация
var stock = new Stock();
var stockWrapper = new StockWrapper(stock);
// Вызываем основной код
stock.Price = 100M;
stock.Price = 200M;
stock.Price = 300M;
// Не даём закрыться консоли после выполнения
Console.ReadLine();
}
}
/// <summary>
/// Событие
/// </summary>
/// <param name="lastPrice">Прошлая стоимость</param>
/// <param name="newPrice">Новая стоимость</param>
class PriceChangedEventArgs(decimal lastPrice, decimal newPrice) : EventArgs
{
/// <summary>
/// Прошлая стоимость
/// </summary>
public decimal LastPrice { get; } = lastPrice;
/// <summary>
/// Новая стоимость
/// </summary>
public decimal NewPrice { get; } = newPrice;
}
/// <summary>
/// Обёртка над <see cref="Stock"/>
/// </summary>
class StockWrapper
{
public StockWrapper(Stock stock)
{
stock.PriceChanged += Stock_PriceChanged!;
}
/// <summary>
/// Обработчик события
/// </summary>
/// <param name="sender">Объект отправитель</param>
/// <param name="e">Данные события</param>
private void Stock_PriceChanged(object sender, PriceChangedEventArgs e)
{
// Выводим на консоль данные события
Console.WriteLine($"OldPrice: {e.LastPrice} | NewPrice: {e.NewPrice}");
}
}
class Stock
{
private decimal _price;
/// <summary>
/// Текущая стоимость
/// </summary>
public decimal Price
{
set
{
// Если значение не изменилось, не меняем его и не вызываем событие
if (_price == value)
return;
// Если изменилось, то меняем значение и создаём событие для всех подписчиков на него
decimal lastPrice = _price;
decimal newPrice = value;
PriceChanged(this, new PriceChangedEventArgs(lastPrice, _price = newPrice));
}
}
// Тип EventHandler определён так: public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
public event EventHandler<PriceChangedEventArgs> PriceChanged = delegate { };
}
Смотрите.
Для начала, стандартные события — это штука, определённая не на уровне C# и не на уровне прости господи WinForms. Гайдлайны по созданию событий придуманы в самом .NET, и они должны работать во всех языках, которые поддерживаются .NET: и в C#, и в Visual Basic, и в экзотике наподобие IronPython (вот полный список).
Фреймворк .NET не привязан к C# и не имеет права исходить из того, что в вашем языке найдутся удобные замыкания. Впрочем, в C# на момент принятия этой конвенции тоже не было замыканий.
(А вот LINQ к такому определению событий не при чём, конечно: всё то же было бы и без LINQ.)
А вот в вашем собственном коде вы можете определять события как вы хотите, например так:
public event Action EverythingCrashed;
Но будьте готовы к тому, что у ваших коллег, которые привыкли к определениям по гайдлайнам, поднимутся брови, и вам придётся отстаивать ваше решение.
Нужна ли эта старая конвенция всё ещё в C#, или к ней следует относиться как к архаике? Иногда нужна.
Когда вы пишете код UI, вы можете позволить себе неэкономное расходование ресурсов, потому что этот код выполняется нечасто.
Но если ваш код более низкого уровня, то с использованием лямбд есть особенность: каждая лямбда, которая захватывает переменные, — это новый объект, который размещается в куче и немного нагружает сборщик мусора. Для кода, который выполняется очень часто или очень много раз, это может стать реальной проблемой, и много раз по «немного» превращается во «много». В этом случае вы захотите по старинке завести один общий метод, который будет подписан на событие, а свой контекст (то есть, дополнительную информацию, наподобие названия месяца в примере) получить из sender
и args
.
Кстати, лямбда вовсе не обязательно размещается в куче. Если лямбда не захватывает никакие переменные, то компилятор достаточно умён, чтобы превратить её в метод. Для того, чтобы убедиться, что ваша лямбда ничего не захватывает, даже придумали специальный модификатор static
(static x => x * x
). Но такие лямбды, конечно, не могут захватывать состояние, и вам придётся «доставлять» контекст в метод снова-таки при помощи sender
/args
.
Как же нужно правильно определять события? «Правильного» пути нет, каждый программист (если вы работаете в команде — каждая команда) определяет для себя самостоятельно.
Вы можете следовать гайдлайнам .NET, тогда ваш код будет более привычным. Вы можете проигнорировать гайдлайны и определять события, поставляя в обработчик лишь ту информацию, которая абсолютна необходима, в этом случае код, использующий события, будет более удобным. Решать лишь вам (вашей команде).
Если вы пишете библиотеку общего пользования, то имеет смысл следовать гайдлайнам, чтобы не создавать сюрпризов для ваших пользователей. Если вы пишете код, являющийся частью большего фреймворка (например, View-часть в WPF- или WinForms-приложении), имеет смысл тоже определять события так же, как и в остальном коде (то есть согласно гайдлайнам). А если вы пишете код нижних уровней, тут можно и применить нестандартный подход, если он вам больше по душе.