В статье мы рассмотрим некоторые такие случаи, как широкоизвестные, так и не очень.
Декларация и вызов событий
Иногда на собеседовании могут задать вопрос, как правильно вызывать события?
Классический способ декларации события выглядит следующим образом:
public event EventHandler HandlerName;
а сам вызов так:
var handler = HandlerName;
if (handler != null) handler(o, e);
Мы копируем обработчик в отдельную переменную handler, поскольку это защищает от NullReferenceException в многопоточных приложениях. Ведь при записи
if (HandlerName != null) HandlerName(o, e);
другой поток может отписаться от события уже после проверки на null, что приведёт к исключению, если это был единственный или последний подписчик.
Но существует более лаконичный путь без дополнительных проверок:
public event EventHandler HandlerName = (o, e) => { };
public event EventHandler HandlerName = delegate { };
HandlerName(o, e);
Отписаться от пустого делегата нельзя, поэтому HandlerName гарантировано не равно null.
Справедливости ради стоит заметить, что в многопоточных приложениях оба этих способа могут привести к вызову обработчика события у уже отписавшегося объекта, поэтому на стороне подписчика нужно предусматривать такое поведение.
Паттерны INotifyPropertyChanging и INotifyPropertyChanged
Весьма часто для уведомления об изменении значений свойств прибегают к следующей записи:
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
var handler = PropertyChanged;
if (handler != null)
PropertyChanged(this, new PropertyChangedEventArgs("Name"));
}
}
Её вряд ли можно назвать лаконичной, да и строковая константа с именем свойства выглядит не очень красиво. Поэтому был разработан более элегантный способ нотификации на основе лямбда-выражений.
public string Name
{
get { return Get(() => Name); }
set { Set(() => Name, value); }
}
Иногда можно услышать возражения, что этот способ медленный. Да, в синтетических тестах он уступает первому, но в реальных приложениях никакого сколько-нибудь заметного снижения производительности не происходит, ведь это большая редкость, когда свойство изменяется с огромной частотой и нужно отслеживать каждое такое изменение. Кроме того, допустимо комбинирование различных способов уведомления, поэтому вариант с лямбда-выражениями очень даже хорош на практике.
Привычная подписка на уведомления об изменении события выглядит так:
PropertyChanged += (o, e) =>
{
if (e.PropertyName != "Name") return;
// do something
}
Однако существует и другой вариант достойный внимания с перегрузкой индексатора:
this[() => Name].PropertyChanging += (o, e) => { // do somethig };
this[() => Name].PropertyChanged += (o, e) => { // do somethig };
viewModel[() => viewModel.Name].PropertyChanged += (o, e) => { // do somethig };
Если нужно выполнить немного действий, то легко можно уложиться в одну строку кода:
this[() => Name].PropertyChanged += (o, e) => SaveChanges();
C помощью этого же подхода удобно реализуется валидация свойств в комбинации с паттерном IDataErrorInfo.
this[() => Name].Validation += () =>
Error = Name == null || Name.Length < 3
? Unity.App.Localize("InvalidName")
: null;
О производительности беспокоится в данном случае также не стоит, поскольку разбор лямбда-выражения выполняется только один раз во время самой подписки.
Цепочное приведение типа методом Of
Встречаются порой ситуации, когда нужно выполнить несколько преобразований типа подряд:
((Type2)((Type1)obj).Property1)).Property2 = 77;
Количество скобок зашкаливает и читаемость падает. На выручку приходит дженерик-метод-расширение
Of<TType>()
.obj.Of<Type1>().Property1.Of<Type2>.Property2 = 77;
Реализация его очень простая:
public static class Sugar
{
public static T Of<T>(this object o)
{
return (T) o;
}
public static bool Is<T>(this object o)
{
return o is T;
}
public static T As<T>(this object o) where T : class
{
return o as T;
}
}
ForEach
У класса
List<TItem>
есть удобный метод ForEach, однако его полезно расширить и для коллекций других типов public static void ForEach<T>(this IEnumerable<T> collection, Action<T> action)
{
foreach (var item in collection)
{
action(item);
}
}
Теперь некоторые операции можно описать лишь одной строкой, не прибегая к методу ToList().
persons.Where(p => p.HasChanged).ForEach(p => p.Save());
Sync Await
Асинхронное программирование с async/await — огромный шаг вперёд, но в редких случаях, например, для обратной совместимости нужно асинхронные методы превращать в синхронные. Тут поможет небольшой класс-адаптер.
public static class AsyncAdapter
{
public static TResult Await<TResult>(this Task<TResult> operation)
{
// deadlock safe variations
// var result = default(TResult);
// Task.Factory.StartNew(async () => result = await operation).Wait();
// return result;
// return Task.Run(() => operation.Result).Result;
return operation.Result;
}
public static TResult Await<TResult>(this IAsyncOperation<TResult> operation)
{
return operation.AsTask().Result;
}
public static TResult Await<TResult, TProgress>(this IAsyncOperationWithProgress<TResult, TProgress> operation)
{
return operation.AsTask().Result;
}
}
Применение его очень простое:
// var result = await source.GetItemsAsync();
var result = source.GetItemsAsync().Await();
Команды
xaml-ориентированные разработчики хорошо знакомы с паттерном ICommand. В рамках MVVM-подхода встречаются разные его реализации. Но чтобы грамотно реализовать паттерн, необходимо учитывать тот факт, что визуальный контрол обычно подписывается на событие CanExecuteChanged у команды, что может вести к утечкам памяти при использовании динамических интерфейсов. Всё это часто ведёт к усложнению синтаксиса работы с командами.
Интерес представляет концепция контекстно-ориентировынных команд.
public class HelloViewModel : ContextObject, IExposable
{
public string Message
{
get { return Get(() => Message); }
set { Set(() => Message, value); }
}
public virtual void Expose()
{
this[() => Message].PropertyChanged += (sender, args) => Context.Make.RaiseCanExecuteChanged();
this[Context.Make].CanExecute += (sender, args) => args.CanExecute = !string.IsNullOrEmpty(Message);
this[Context.Make].Executed += async (sender, args) =>
{
await MessageService.ShowAsync(Message);
};
}
}
<Window DataContext="{Store Key=viewModels:HelloViewModel}">
<StackPanel>
<TextBox Text="{Binding Message, Mode=TwoWay}">
<Button Content="{Localizing Make}" Command="{Context Key=Make}">
</StackPanel>
</Window>
Причём контекстные команды совместимы с Routed Commands в WPF.
<Button Command="New"/>
this[ApplicationCommands.New].Executed += (o, e) => { ... };
Немаловажно и то, что обработчики команд запросто могут быть как синхронными, так и асинхронными.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Все эти сладости реализованы в библиотеке Aero Framework, новая версия которой доступна по ссылке (резервная ссылка), где можно увидеть их вживую и в действии.
Комментарии (95)
denismaster
14.04.2015 20:58+1Очень интересная статья, не знал про события и пустые делегаты. Спасибо.
lair
14.04.2015 20:59+1Приведение типа методом Of
ForEach
monads.net
public static TResult Await<TResult>(this Task<TResult> operation) { var result = default(TResult); Task.Factory.StartNew(async () => result = await operation).Wait(); return result; }
Эээ, а чем вам простойoperation.Result
-то не угодил?Makeman Автор
15.04.2015 00:15Интересная ссылка про монады. Правда, непривычный синтаксис иногда получается, хотя в некотрых случаях выглядит красивее обычного.
Ох, перемудрил немного с Await, да, достаточно operation.Result :) Спасибо, что исправили!lair
15.04.2015 00:18+1Правда, непривычный синтаксис иногда получается, хотя в некотрых случаях выглядит красивее обычного.
Ничем не непривычнее вашихOf
иForEach
достаточно operation.Result
Вот именно, что достаточно. Зачем extension-метод-то писать, который просто вызывает общедоступное свойство?
Norfolc
15.04.2015 00:24+3Простой вызов operation.Result может привезти к дедлоку — если в качестве operation передать результат асинхронной функции, которая использует текущий SynchronizationContext.
Поэтому решением здесь как раз таки бывает вызов нового таска:
return Task.Run(() => operation.Result).Result;
Makeman Автор
15.04.2015 00:44Смутно припоминаю, что у меня как раз и была ситуация, когда обычный operation.Result не работал, поэтому пришлось создавать новый таск. Хотя в большинстве случаев хватит первого варианта, второй также стоит взять на заметку.
Norfolc
15.04.2015 01:02+3Оба решения, в зависимости от ситуации, могут как работать, так и не работать.
Решение без Task-а вызовет дедлок в WinForms/WPF приложении, если обращение к Result будет до окончания последнего вызова, захватывающего контекст, внутри асинхронной функции.
Решение с таском может не работать, например, если внутри асинхронной функции будет обращение к UI…
lair
15.04.2015 01:17+1Разве не достаточно сделать
await operation.ConfigureAwait(false)
?Norfolc
15.04.2015 01:36И какую задачу это решит? Если operation — асинхронная функция, вызывающая у себя внутри другие асинхронные функции, то это ни на что не повлияет.
На самом деле, плохо совмещать синхронный и асинхронный код. Но если уж приходится это делать, то нужно это делать с умом, учитывая особенности в каждом конкретном месте.
Например, если не важно, в каком потоке будет работать код после await, то можно использовать ConfigureAwait(false) на всех библиотечных функциях и после этого можно вызвать свойство Result…lair
15.04.2015 11:12+1Это решит проблему с дедлоками.
В принципе, я согласен с тем, что не надо совмещать синхронное с асинхронным, и разбираться надо по месту, но давайте все-таки разберемся.
Что делаетTask.Result
? Он блокируется на текущем контексте синхронизации до завершения выполнения задачи. Как следствие, что произойдет при вызове кода:Task.Run(() => operation.Result).Result
? Текущий код создаст задачу, которая будет ожидать выполнения переданной задачи, заблокирует текущий контекст и будет ждать выполнения. Если внутри переданной задачи тоже есть блокировка на контексте, то будет все тот же самый дедлок.
Ilya81
15.04.2015 15:56+1Думаю, это решит многие задачи, по крайней мере я почти везде так пишу в async Task-методах, просто await обычно лишь в async void-методах. Как я в своё время специально проверял,
всегда и сразу приводит к возврату в поток SynchronizationContext, если он есть, а какой смысл делать эти переключения в тех классах, что отделены от пользовательского интерфейса? Я полагаю, что нагрузку на UI-поток нужно минимизировать (о чём особенно Android любит напоминать), так что как только ушли от взаимодействия с пользовательским интерфейсом, в диспетчеризации потоков лучше не использовать SynchronizationContext, а вернуться к нему непосредственно перед выдачей результатов пользователю.await operation
Viacheslav01
15.04.2015 16:04Тогда к нам возвращается ручное управление синхронизацией, лишаемся бонуса простоты.
П.С. у меня в ViewModel OnPropertyChanged всегда синхронизируется с контекстом UI, ну так на всякий случай :)Ilya81
17.04.2015 17:08Во ViewModel у меня тоже (и в контроллере в случае iOS и т. п.). Но там у меня обычно как раз async void-методы, в них и вызываются всякие OnPropertyChanged в UI-потоке после вызова async Task-метода без ConfigureAwait. А использование ConfigureAwait(false) у меня в модели данных и далее.
mayorovp
15.04.2015 06:30+1Если для завершения operation надо освободить контекст синхронизации — то какая разница сколько задач будет создано? При синхронном ожидании висеть будет вся цепочка.
mayorovp
15.04.2015 06:37А вот такой вариант может оказаться полезным (если не выпадет по переполнению стека):
if (SynchronizationContext.Current is WindowsFormsSynchronizationContext) { while (operation.State не помню дальше условие) Application.DoEvents(); } return operation.Result;
Viacheslav01
15.04.2015 13:26+3return Task.Run(() => operation.Result).Result;
Этот вызов так же приведет к блокировке, потому что текущий поток будет ждать окончания нового таска, который в свою очередь для окончания требует синхронизации в ожидающий поток.Norfolc
15.04.2015 15:21Согласен. В моём коде есть ошибка.
Если operation будет вызван заранее (как при использовании extension-метода), то будет дедлок:
var operation = OperationAsync(); return Task.Run(() => operation).Result; // Deadlock!!
Однако дедлока можно избежать, если сделать такой вызов:
return Task.Run(() => OperationAsync()).Result;
В этом случае OperationAsync() не будет захватывать контекст синхронизации, и как результат не будет дедлока.
Следствием этого всего является то, что невозможно написать extension-метод для Task-а без дедлока в общем случае.Viacheslav01
15.04.2015 16:02Я бы описал метод решения задачи иначе, для каждого случая решение свое, но программист должен отчетливо понимать, что происходит, а не тупо лепить что куда попало.
Мой личный хит это HttpClient.GetStringAsync(...).Result из UI потока, для WP гарантированная смерть :)
justmara
15.04.2015 13:18-3Да, Await()-метод прекрасен своей бесполезностью. из той же серии встречал:
public static bool IsNullOrEmpty(this string value) { return string.IsNullOrEmpty(value); }
Неоднократно встречал такой идиотизм.
Вообще экстеншн-методы прям-таки манят слабых духом впасть в маразм расширенияклепания на каждый чих. С матом и подручными предметами приходится отстаивать system.object и классы фреймворка, потому что если не уследить, то там начинается такая дурь…PsyHaSTe
15.04.2015 13:23+4Не буду пытаться вам ничего доказать, но этот «идиотизм» не все таковым считают.
justmara
15.04.2015 13:32-1monads.net
Сборник «happy debugging!» Особо умиляют полезностью методы TryDo/Catch, If/IfNot — просто гимн абсурду.
Прекрасный пример, когда из одного метода With было как-то стыдно делать библиотеку и потому туда насовали абсурдных «подобных» методов просто чтоб было. При дебаге такого когда вспомнишь весь словарь обсценной лексики в отношении автора.lair
15.04.2015 13:37+2Особо умиляют полезностью методы TryDo/Catch, If/IfNot — просто гимн абсурду.
Вы, видимо, никогда не писали сложный парсинг, каждый шаг которого нужно залогировать, но при этом должно не падать, а возвращать дефолтное значение. Или маппинг класса «если значение число — то пишем его, любое другое — игнорируем».
При дебаге такого когда вспомнишь весь словарь обсценной лексики в отношении автора.
Для меня смысл написания монадического кода — в уменьшении количества дебага.justmara
15.04.2015 16:26-6точно также могу ответить, что, видимо, вы никогда не писали код, который после вас будет использоваться кем-то ещё
EngineerSpock
15.04.2015 22:32Аргументы будут? lair утверждает, что дебаггинга меньше и оно понятно почему или вам непонятно? Или вы не согласны, но тогда с чем?
PsyHaSTe
15.04.2015 01:49+1int i = 10; Console.WriteLine(i.Of<long>()); // bam
т.к. каст Of<T> полезен только к производным классам, а структуры не наследуются, следует так переписать:
public static TResult Of<T, TResult>(this T o) where T : class { return (TResult) (object) o; }
лишний каст среда всё равно уберет скорее всего, зато при попытке выполнить код выше получим
Error CS1061 'int' does not contain a definition for 'Of' and no extension method 'Of' accepting a first argument of type 'int' could be found
Тут правда недостаток, что нужно указывать два типа — откуда кастуем и куда, среда не позволяет опустить типизацию, если хотя бы одну не может разрезолвить… Так что стоит подумать, как это можно решить.areht
15.04.2015 03:56Это может решить
public static TResult Of<TResult>(this object o)
О недостатках я догадываюсь, но вы правда от них будете спать хуже по ночам?
withkittens
15.04.2015 02:09+6Извините, а можно я тут поною?
инжекции
Декларация
В русском языке конечно есть такие слова, но они не о том, о чём вы пишете.
В вашем случае это, скажем, внедрение и объявление.
Не загрязняйте, пожалуйста, язык. И не приучайте к этому в статьях, тем более с ярлычком tutorial.PsyHaSTe
15.04.2015 02:23Термин «SQL-инъекция» существует, а термина «SQL-внедрение» — нет. Можно сказать «Внедрение вредоносного SQL-кода», но это как бы не эквиваленты — одно слово и целое предложение.
Я тоже радею за чистый язык, но одно дело, когда человек говорит "Чтобы получить доступ к имплементации необходимо кастовать инстанцию класса к интерфейсу" (хотя после небольшого тренинга воспринимать такие фразы не составляет особого труда), а другое дело статья, где встречается пара заимствованных слов, использующихся по делу.
Закончу в итоге стихом 1854 года
Тут филолог для корнесловья
Отыщет новые условья,
Найдет, что русский корень есть
И слову чуждому «визиты»,
Успев стократно произнесть
Извозчику: «Да ну ж! вези ты!»
Язык наш — ключ заморских слов:
Восстань, возрадуйся, Шишков!
Не так твои потомки глупы;
В них руссицизм твоей души,
Твои родные «мокроступы»
И для визитов хороши.
Зачем же всё в чужой кумирне
Молиться нам? — Шишков! Ты прав,
Хотя — увы! — в твоей «ходырне»
Звук русский несколько дырав.
Тебя ль не чтить нам сердца вздохом,
В проезд визитный бросив взгляд
И зря, как, грозно бородат,
Маркер трактирный с «шаропёхом»
Стоит, склонясь на «шарокат»?
A1lfeG
15.04.2015 09:13+4В русском языке есть слово «инъекция», вполне подходит под назначение.
Singerofthefall
15.04.2015 09:48+2Оно не просто подходит, оно как раз и является переводом слова injection.
Makeman Автор
15.04.2015 10:19+2Стараюсь внимательно относиться к терминам.
Инжекции — это инженерный термин, близким переводом которого с английского является внедрение. О смысловых оттенках можно спорить долго, но существует такое понятие как инжектор — механизм или приспособление для внедрения чего-либо. В технической области это слово чаще употребляется, чем, скажем, инъектор или внедритель и звучит благозвучно, а также наиболее созвучно с английским написанием, поэтому выбор пал именно на понятие инжекции.
Декларировать, декларация чего-либо достаточно часто встречаются в документации для разработчиков.
На мой взгляд, чем ближе и созвучнее перевод слова к английскому оригиналу, тем меньше путаницы это вносит в его дальнейшее использование.
Спасибо, что следите за чистотой языка, это тоже важно!Makeman Автор
15.04.2015 10:42Сделаю ещё небольшое пояснение к сказанному.
Например, начинающий разработчик встретился с понятием внедрение зависимостей и хочет найти английскую литературу по этому вопросу. Для этого нужно сформулировать соответствующий запрос. Но, например, гугл предлагает следующие переводы для слова внедрение: introduction, implantation, plantation, inculcation, intrusion, intercalation, infiltration.
Среди них даже не встречается injection, которое состоит в изначальном понятии Dependency Injections, поэтому в переводах технических терминов иногда проще не уходить далеко от оригинала. Да и это помогает легче запоминать иностранные слова.withkittens
15.04.2015 13:44+2Декларировать, декларация чего-либо достаточно часто встречаются в документации для разработчиков.
Смею предположить, речь идёт про документацию либо изначально русскую, либо с второсортным переводом на русский.
MSDN — Деклараторы и объявления переменных
(Хм, официальная русская документация есть только у Microsoft?)
SwiftBook — Объявление констант и переменных
Например, начинающий разработчик встретился с понятием внедрение зависимостей и хочет найти английскую литературу по этому вопросу.
Я ожидаю, что в хорошей литературе тут же будет дан оригинальный английский термин, как в википедии:
Внедрение зависимости (англ. Dependency injection, DI) — процесс предоставления внешней зависимости программному компоненту.
либо
Потокобезопасность (англ. Thread safety) — это концепция программирования...Makeman Автор
15.04.2015 23:23-1Я ожидаю, что в хорошей литературе тут же будет дан оригинальный английский термин, как в википедии
В том-то и дело, что новый термин может встретиться в любом месте, например, на форуме, и человеку захочется с ним разобраться. Первым делом он начнёт поиски с наиболее предсказуемых вариантов перевода.
Конечно, методом проб и ошибок, скорее всего, он найдёт, что нужно, однако если слова из разных языков схожи, то этот процесс произойдёт быстрее. Да общей путаницы в понятиях будет меньше.
Например, оригинальное название статьи с MSDN — Declarators and Variable Declarations. Конечно, русский язык богат, и можно подобрать множество синонимов к слову Declaration, но всё-таки, на мой взгляд, хорошо, когда различные термины более-менее универсальны и созвучны на разных языках. Это моё личное мнение, которого придерживаюсь при написании статей.withkittens
15.04.2015 23:47Не нужно подбирать никакие синонимы или созвучные аналоги, если есть конкретный, устоявшийся, общеиспользуемый перевод.
Declaration — это объявление.
Кейс про новый термин на форуме и последующий поиск англоязычного термина, если честно, я представляю себе с трудом.
hmspns
15.04.2015 11:41+1Я бы в методы класса Sugar атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)] добавил.
Makeman Автор
15.04.2015 12:14Спасибо, хорошая рекомендация. Только она подойдёт лишь для Desktop-версии библиотеки. В Portable-варианте перечисление MethodImplOptions включает только два флага NoInlining и NoOptimization. Возможно, компилятор самостоятельно производит Inlining, где это нужно.
PsyHaSTe
15.04.2015 13:10+1Не поможет никак :)
Например код для As.method public hidebysig static !!T As<class T>(object o) cil managed { .custom instance void [mscorlib]System.Runtime.CompilerServices.ExtensionAttribute::.ctor() = ( 01 00 00 00 ) // Размер кода: 12 (0xc) .maxstack 8 IL_0000: ldarg.0 IL_0001: isinst !!T IL_0006: unbox.any !!T IL_000b: ret } // end of method Sugar::As
А теперь вспоминаем, что JIT-x86 не умеет инлайнить методы, в IL-коде которых содержатся инструкции starg или ldarga, и получаем, что мы напишем атрибут, что не напишем, толку не будет, т.к. для AnyCPU фактически всегда генерируется x86-код (никто же не снимает незаметную галочку Prefer x86?).hmspns
15.04.2015 13:49+2Значение перечисления в атрибуте говорит о том, что нужно инлайнить, если возможно. А то что компилятор пока не умеет — не зона ответственности прикладного программиста.
Потому что сейчас не умеет, потом научится (по ссылке об этом говорится: «JIT today can't inline methods that contains „starg“ opcode»). Или другой компилятор будет уметь. А код с атрибутом останется.PsyHaSTe
15.04.2015 14:41+1RyuJIT, который как бы и является новым джитом, имеет тот же самый баг, ведь он и основан-то на кодовой базе x86. А что он не научится инлайнить — факт, потому что баг закрыт с won't fix аргументацией.
Кстати, был разбор, что это не баг, а особенности работы компилятора, ибо CoreCLR в открытом доступе и можно посмотреть, то что он смотрит наличие этой инструкции, и если находит, то делает goto noInline
switch (opcode) // ... case CEE_STARG: case CEE_STARG_S: goto ARG_WRITE; case CEE_LDARGA: case CEE_LDARGA_S: case CEE_LDLOCA: case CEE_LDLOCA_S: goto ADDR_TAKEN;
ARG_WRITE: if (compIsForInlining()) { #ifdef DEBUG if (verbose) { printf("\n\nInline expansion aborted due to opcode at offset [%02u] which writes to an argument\n", codeAddr-codeBegp-1); } #endif /* The inliner keeps the args as trees and clones them. Storing the arguments breaks that * simplification. To allow this, flag the argument as written to and spill it before * inlining. That way the STARG in the inlinee is trivial. */ inlineFailReason = "Inlinee writes to an argument."; goto InlineNever; } else
Так что я бы не рассчитывал на изменение этого поведения.hmspns
15.04.2015 14:59+1Мне кажется мы с вами о разных вещах говорим. С точки зрения текущей работы компиляторов — вы правы. С точки зрения прикладного программиста, который не заморачивается прогнозированием будущего (которое не спрогнозировать), для кода вида:
public static T Of<T>(this object o) { return (T) o; }
запросить у компилятора встраивание разумно.PsyHaSTe
15.04.2015 15:32-1Ибо закон дырявых абстракций. То, что вы пишете на ЯВУ не значит, что мы не должны понимать, как работает процессор, и какой ассемблер генерирует JIT и почему. Просить есть смысл, если компилятор может это сделать, иначе это просто захламление ненужными атрибутами.
mayorovp
15.04.2015 19:41+1Что-то не могу найти в этом коде ни starg, ни ldarga… Вижу только ldarg.0 — но эта инструкция встречается в каждом первом методе, она никак не может мешать инлайнить.
hummerd
15.04.2015 12:05А ссылка на репозиторий кода есть? Гитхаб там какой-нибудь…
Makeman Автор
15.04.2015 12:16-3Да, исходные коды, конечно, есть. Ссылка в окончании статьи. На всякий случай продублирую тут: Aero Framework.
Viacheslav01
15.04.2015 13:12Кроме того, искусственно возможно даже смоделировать ситуацию вызова обработчика у объекта уже утилизированного сборщиком мусора. Конечно, это приведёт к исключению.
Покажите как.Makeman Автор
15.04.2015 14:14По этой теме можно зачитаться материалами и комментариями в статье Потокобезопасные события в C# или Джон Скит против Джеффри Рихтера.
Вызов обработчика у отписавшегося объекта воспроизводится легко (например, достаточно поставить вызов Thread.Sleep() перед handler(o, e)). Экспериментально установлено, что var handler = Handler удерживает оба объекта от сборки мусора даже если других ссылок на объекты не осталось, но, теоретически, как мне думается, операция handler = Handler может произойти не атомарно, и если в этот короткий промежуток времени произойдёт отписка и сборка мусора, то в дальнейшем получится исключение. К сожалению, такое воспроизвести сложно.
Возможно, конечно, и ошибаюсь в чём-то :)
PsyHaSTe
15.04.2015 14:54Ну вот на коленке пример набросал:
using System; using System.Threading; public class Program { public static SomeClass SomeClassInstance; private static void Main() { new Thread(() => { Thread.Sleep(100); GC.Collect(); }) { IsBackground = true }.Start(); var wr = new WeakReference(new SomeClass()); Console.WriteLine("IsAlive = {0}", wr.IsAlive); ((SomeClass)wr.Target).Foo(); Console.WriteLine("IsAlive = {0}", wr.IsAlive); } } public class SomeClass { public SomeClass() { Console.WriteLine("Constructor called"); } ~SomeClass() { Console.WriteLine("Destructor called"); } public void Foo() { Thread.Sleep(1000); Console.WriteLine("Foo"); } }
Viacheslav01
15.04.2015 15:41А где события? Где вызов по событию метода собранного объекта?
PsyHaSTe
15.04.2015 15:57С событиями можно так придумать:
using System; using System.Threading; public class Program { public static event EventHandler SomeEvent = delegate { }; private static void Main() { object UnsubscribeObject = new object(); for(int i = 0; i < 2*1024*1024; i++) { var obj = new object(); SomeEvent += (sender, args) => obj.GetHashCode(); } SomeEvent += (sender, args) => UnsubscribeObject.GetHashCode(); Console.WriteLine("Ready to start"); new Thread(() => SomeEvent(null, null)).Start(); Console.WriteLine("Event invocation started"); SomeEvent -= (sender, args) => UnsubscribeObject.GetHashCode(); UnsubscribeObject = null; Console.WriteLine("UnsubscribeObject is null - {0}", UnsubscribeObject == null); } }
Если закоментить предпоследнюю строчку, всё отработает норм. Тут нет сборщика, то там та же логика будетViacheslav01
15.04.2015 16:11Это шутка? Приведенные примеры это банальные NPE из за коряво написанного кода, а ни как не вызов обработчика события у собранного GС объекта.
Viacheslav01
15.04.2015 16:14И да это еще один минус в копилку public static event EventHandler SomeEvent = delegate { };
Вместо NPE при вызове делегата получаем NPE в методе обработчика!PsyHaSTe
15.04.2015 16:46+1Тут как ни вызывай, один фиг получишь тыкву. Тут все неплохо расписано. В итоге приходим к выводу, что с текущей событийной моделью в многопотоке в любом случае имеем боль.
Viacheslav01
15.04.2015 18:45А для меня важно, то, что можно огрести проблем с вызовом кода который должен уже быть отписанным, проблема не новая и давно решается на автомате, в то время как проблема доступа к объекту собранному GC, это совершенно другая тема.
И я хочу увидеть возможно ли это на практике, а вместо этого вижу надуманные примеры показывающие другую проблему, которая меня не интересует.Makeman Автор
15.04.2015 23:56-1Долго искал, но, к сожалению, не нашёл того комментария, где когда-то читал о событиях и сборщике мусора. Или мне приснилось?
В общем, проблема, потенциально, может возникнуть в том случае, если операция копирования значения в переменную выполняется не атомарно (var handler = Handler) и поток прервётся не докопировав всё как положено, а за это время кто-то успеет отписаться и вызвать сборщик мусора. После этого управление вернётся исходному потоку и копирование завершиться, но исходный объект будет уничтожен сборщиком мусора, что, очевидно, приведёт к исключению.
Тут всё зависит от диспетчеризации потоков, поэтому вручную такое весьма сложно воспроизвести. И если действительно проблема существует, то она крайне трудноуловима.lair
16.04.2015 00:23Эээ, что за «копирование значения в переменную»?
var handler = Handler
— это присвоение ссылки (reference assignment), и оно в .net атомарно.Makeman Автор
16.04.2015 01:07-1Сразу прошу извинить меня, если несу чушь, но…
Насколько мне известно, переменные делегатов являются неизменяемыми типами (immutable types), как структуры, то есть во время присваиваивания создаётся новая копия данных, а не просто присваивается ссылка. Вот не знаю только, происходит ли это атомарно.
MSDN
Структуры копируются при присваивании. При присваивании структуры к новой переменной выполняется копирование всех данных, а любое изменение новой копии не влияет на данные в исходной копии. Это важно помнить при работе с коллекциями типов значений, такими как Dictionary<string, myStruct>.
Makeman Автор
16.04.2015 01:13Пример:
EventHandler handler1 = (sender, eventArgs) => Console.WriteLine("1"); var handler2 = handler1; handler2 += (sender, eventArgs) => Console.WriteLine("2"); handler1(null, EventArgs.Empty); // out => 1 Console.ReadKey(); handler2(null, EventArgs.Empty); // out => 1 , 2 Console.ReadKey();
lair
16.04.2015 01:15A delegate is a reference type
То, что он immutable, еще не означает, что он структура. А в вашем случае он еще и не делегат, а список делегатов, и отчетливо mutable, иначе бы (а) не работала бы подписка/отписка и (б) не были бы нужны все пляски с копированием ссылок и проверками. У неизменных объектов нет проблем с многопоточностью.mayorovp
16.04.2015 06:43+1Список делегатов — тоже делегат. И он все равно immutable — добавление делегата к списку создает новый список, а не изменяет существующий.
У неизменных объектов нет проблем с многопоточностью.
Поправка: нет проблем «внутри». Код снаружи все равно может столкнуться с проблемами — что иногда и происходит.lair
16.04.2015 09:32Вы правы, да. Я зачем-то перепутал изменяемость экземпляра с изменяемостью поля, где хранится ссылка.
Makeman Автор
16.04.2015 15:06Всё же решил убрать это утверждение из статьи, поскольку оно спорное.
Не мной оно было придумано, но, к большому сожалению, не удалось найти тот первоисточник, где это изначально встретил.
Viacheslav01
15.04.2015 13:42Я как обычно внесу свою долю скепсиса :)
Все описанное выше, очень дорого в рамках ограниченных вычислительных ресурсов.
Например лямды для определения свойств, в свое время в профайлере доходило до 50% таймфрейма на разбор деревьев выражений, и дело даже не в разборе, а в том, что каждый такой выбов влечет за собой сначала генерацию дерева, потом разбор, а потом утилизацию.
То же с пустым делегатом, вызов пустого делегата всегда дороже проверки на пустой указатель. А если учитывать то, что этот вызов почти на 100% будет из UI потока, то это гарантированная задержка обработки потока событий пользователя, пусть очень ненадолго, но задержка.
Если говорить не о WPF, а о Silverlight то там, подписка на CanBeExecutedChanged выполняется с помощью слабых ссылок, утечек не будет.
А вот подписка на событие с помощью лямды, это почти гарантированная утечка памяти!
И уж точно отсутствие точного понимания того, как работает асинхронный не исправить хитрыми реализациями Await
Большая часть вышесказанного относится к разработке WP SL, который приучил меня наперед думать о трате ресурсов, даже на мелочи!Makeman Автор
15.04.2015 14:28Будем реалистами :) все эти задержки в большинстве случаев настолько ничтожны, что преимущества от их испольования затмевают собой недостатки. Тем более ничто не запрещает комбинировать разные подходы между собой, где это на самом деле нужно.
А вот подписка на событие с помощью лямды, это почти гарантированная утечка памяти!
Поподробнее, что вы имеете в виду? Да, как и любые другие подписки они могут привести к утечкам памяти.
viewModel[() => viewModel.Name].PropertyChanged += (o, e) => { // do somethig };
this[() => Name].PropertyChanged += (o, e) => { // do somethig };
Соглашусь, что с первым вариантом внешней подписки нужно быть внимательным, но второй замкнутый вполне безопасен.Viacheslav01
15.04.2015 15:44+1А я и есть реалист, потер из своего кода, после того как в определенный момент профайлер показал мне наглядно, что я не прав!
А какой смысл подписываться на свое событие? Это оверхед на ровном месте, разве нет?Makeman Автор
15.04.2015 22:58А я и есть реалист, потер из своего кода, после того как в определенный момент профайлер показал мне наглядно, что я не прав!
Наверно мне везёт, но пока ни разу не возникало хоть сколько-нибудь заметных тормозов из-за лямбд, какие бы они медленные ни были в синтетических тестах.
А какой смысл подписываться на свое событие? Это оверхед на ровном месте, разве нет?
Не совсем понимаю, что вы имеете в виду.
Конструкция
this[() => Name].PropertyChanged += (o, e) => { // do somethig };
эквивалентна
this.PropertyChanged += (o, e) => { if (e.PropertyName != "Name") return; // do something };
Смысл в том, чтобы избавиться от if-оператора и получить более компактную запись. На практике порой хватает одной-двух строк кода вмето, как минимум, четырёх (при условии стандартного форматирования).
Makeman Автор
18.04.2015 00:15Ради интереса замерил разницу в скороти чтения и записи на реализациях INotifyPropertyChanged с лямбда-выражениями и без них.
Получившиеся результаты:
По скороти доступа (get) классическая реализация выигрывает примерно в 100 раз
По записи (set) классическая лучше примерно в 20 раз
На результаты немного также влияет упаковка-распаковка численных значений. В лямбда-реализациях числа хранятся в виде объектов (упакованы), поэтому сразу может показаться, что это хуже. Однако если вдуматься, UI-привязки работают через рефлексию, поэтому, скорее всего, в классических реализациях упаковка происходит неявно. Тут зависит от конкретного случая, где и как используется свойство чаще, в коде вью-модели или на UI.
Разница в 100 и 20 раз выглядит внушительно, но на деле это оказываются наносекунды (скорость записи, более медленная операция, у меня получилась около 2нс для лямбд, но зависит от производительности устройства). Частота же обносвления экрана в 100 Гц эквивалентна10 мс, поэтому в реальных приложениях на FPS это оказывает ничтожное влияние, если вообще оказывает. Экономятся лишь такты процессора.
В плане красоты разработки и гибкости лямбда-выражения выигрывают, а действительно значимого падения производительности никак не происходит в большинстве обычных случаев, поэтому боятся их уж точно не стоит, это факт.Norfolc
19.04.2015 11:15Это довольно серьёзная разница в производительности. Может быть для современных PC это не существенная разница, но при использовании такого кода на мобильных устройствах это может быть очень заметно.
Viacheslav01
15.04.2015 15:51Подписка на событие PropertyChanged внешнего объекта отдельная история, когда от него отписываться?
Мне пришлось делать свою версию слабой подписки для решения этой проблемы.
public class WeakPropertyChangedListener { private WeakReference _weakEventListener; private INotifyPropertyChanged _notifyCollectionChanged; public WeakPropertyChangedListener(INotifyPropertyChanged notify, IWeakPropertyChangedListener eventListener) { if (notify == null || eventListener == null) { return; } _notifyCollectionChanged = notify; _notifyCollectionChanged.PropertyChanged += PropertyChanged; _weakEventListener = new WeakReference(eventListener); } private void PropertyChanged(object sender, PropertyChangedEventArgs e) { var listener = _weakEventListener; if (listener == null) { return; } var eventListener = listener.Target as IWeakPropertyChangedListener; if (eventListener != null) { eventListener.EventHandler(sender, e); return; } Disconnect(); } public void Disconnect() { var source = _notifyCollectionChanged; _notifyCollectionChanged = null; _weakEventListener = null; if (source == null) { return; } source.PropertyChanged -= PropertyChanged; } }
kraidiky
16.04.2015 01:31+1Это зависит. Сначала делаешь сложные конструкции в каждом свойстве, а потом вдруг тебе нужно создать тайловую карту 100 на 100 в которой в каждой ячейке объект с 10 свойствами, и склонировать её. И вот где-то в этот момент понимаешь, что управляемый код иногда ваще вава.
impwx
15.04.2015 14:56К большинству из этих вещей так или иначе приходишь по мере работы, стараясь избежать написания однообразного кода. К счастью, необходимость в лямбдах для описания полей отпадет в С# 6 с появлением ключевого слова
nameof
.
Не понимаю только, зачем для получения ссылки наPropertyChanged
приспособили индексатор. Для общего совета это слишком смелое допущение — например, если модель составная и описывает список вложенных моделей, то новый индексатор будет семантически конфликтовать с существующим. Более поддерживаемое решение — сделать приватный методPty(Expression<Func<T>> expr)
и заранее продумать архитектуру, чтобы для каждого свойства, изменение которого планируется отслеживать снаружи, было отдельное событие.Viacheslav01
15.04.2015 15:48+1Так давно доступен CallerMemberNameAttribute
Я в итоге пришел к такой версии, меня полностью устраивает
protected bool SetValue<T>(ref T field, T value, Action<T, T> successHandler = null, [CallerMemberName] string propertyName = "") { Guard.NotNullAndEmpty(propertyName); if (Equals(field, value)) { return false; } var oldValue = field; field = value; OnPropertyChanged(propertyName); if (successHandler != null) { successHandler(oldValue, field); } if (_trackChildProperties) { SubscribeToChildNotifications(field, propertyName); } return true; }
Да свойство сложнее немного чем в статье, но снипет propn решает эту проблему :)
private int _field; public int Field { get { return _field; } set { SetValue(ref _field, value); } }
Зато нет деревьев и индексаторовVVS0205
15.04.2015 17:43+4Чтобы избежать упаковки, советую заменить:
if (Equals(field, value))
на:
if (EqualityComparer<T>.Default.Equals(field, value))
Makeman Автор
15.04.2015 22:40Паттерн IDataErrorInfo работает через индексатор и это выглядит довольно красиво. На этой почве возникла идея неявных индексных свойств, о которых можно прочитать в документации к библиотеке, где ключом так же является строковое имя свойства (полиморфизм в действии). После этого очень логичным шагом показалась перегрузка индексатора для возможности подписки на изменение значений свойств (INotifyPropertyChanged). Кроме того существует перегрузка индексатора и для команд (ICommand).
На мой взгляд, всё это выглядит очень эстетично и стройно. Во-первых, модификатор this часто опускается и его редко видно, а тут ему нашлось интересное применение, во-вторых же, синтаксис подсказывает, что операция производится с текущим объектом.
если модель составная и описывает список вложенных моделей, то новый индексатор будет семантически конфликтовать с существующим
Не совсем понял, что вы имеете здесь в виду.
и заранее продумать архитектуру, чтобы для каждого свойства, изменение которого планируется отслеживать снаружи, было отдельное событие.
В текущей реализации для каждого отслеживаемого свойства создаётся отдельный обработчик как снаружи, так и изнутри.impwx
15.04.2015 23:44Паттерн IDataErrorInfo работает через индексатор и это выглядит довольно красиво.
Имхо, это одно из наиболее странных архитектурных решений в .NET, что мне доводилось видеть.
Не совсем понял, что вы имеете здесь в виду.
Вот например:
var a = usersListModel[1]; var b = usersListModel["Name"]; var c = usersListModel[() => CollectionChanged];
Если для индексатора есть несколько перегрузок, которые выполняют абсолютно разные действия, то это сбивает с толку. Гораздо лучше было бы использовать метод с понятным названием.Makeman Автор
16.04.2015 00:07Мне нравится полиморфизм и вариант с индексатором :)
Стандартных подсказок достаточно, чтобы ни в чём не запутаться. Зато вью-модели получаются такими, что любо глянуть.
Исходный код библиотеки полностью открыт, поэтому при желании можно реализовать всё на свой вкус!
onikiychuka
Про ForEach для IEnumerable — я просто оставлю это(http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx) здесь
Makeman Автор
Соглашусь с тем, что у начинающего программиста может вызвать вопросы наличие двух альтернативных конструкций, выполняющих по сути одно и тоже. Но стоит всё же признать, что в плане красоты и лаконичности кода ForEach-расширение иногда смотрится выгоднее.
kraidiky
Использую свой ForEach только он у меня ещё чуть более хитрый, и возвращает IEnureable Элементов, к которым форич был применён. Иногда крайне полезно.
Ещё завёл себе
T MinElement(Func<T, float>)
И совсем уж страшно удобную штуку
IEnumerable MinElements(Func<T, float>, int count)
Такой код может казаться менее читаемым, но зато всё, что вы хотите сделать с последовательностью помешается в три строки вместо того чтобы занимать пол экрана, и от этого читаемость напротив вырастает.
Makeman Автор
Если использовать стандартные средства, то ваш методы, насколько понимаю, эквивалентены таким конструкциям
kraidiky
Ну по результату эквивалентны, а по времязатратам, как вы понимаете, если в списке, 5000 элементов, а вам из них только пять старших нужны, то весьма некислая экономия времени получается.
Хммм-м. Вернее должна получаться. Я тут задумался, что я же, в общем-то, не мерил. Хрен его знает какие там оптимизации могут быть.
Но должно получаться сильно быстрее, да.
Makeman Автор
Конечно, сложно судить о производительности и затратах памяти ваших методов, не имея перед глазами их полной реализации. Возможно, вы применяете какой-то хитрый алгоритм оптимизированный для работы с вещественными числами, большими или маленькими коллекциями — всякое допустимо.
Но по опыту могу сказать, что зачастую безопаснее использовать стандартные механизмы. Хотя раньше сам решал подобные задачи в лоб, не подозревая даже, что для этого годятся OrderBy, Take и другие методы из этой серии :)
kraidiky
Вот ведь стыд. :(((( Померил скорость и стандартная реально работает быстрее в два раза на списке в 5000 элементов. :((((((((((((
list2.OrderBy(i => i * 3).Take(20).ToArray();
list1.MinElements(i => i * 2, 20).ToArray();
Интересные у них там алгоритмы под капотом. Может в дерево разбивается при первом применении лямбды правда тогда памяти должно скушать, конечно.
В общем спасибо, это было весьма поучительно.
withkittens
Алгоритмы под капотом вы всегда можете посмотреть.
PsyHaSTe
Ничего, LINQ всё-равно можно ускорить. Лично я планирую написать пару удобных методов, которых в LINQ нет, и исправить пару линковских родных методов (например Single с предикатом проходит всю коллекцию, даже если нашел два элемента, подходящие под правило).
Makeman Автор
Также довольно полезен бывает метод Skip. Например, если нужно взять десять минимальных элементов, но не первых, а, скажем, начиная с сотого, то операция будет выглядеть так:
Вообще же в Linq предусмотрены методы на многие случаи из жизни, поэтому прежде чем использовать свои лучше заглянуть в документацию. Хотя свои методы делать тоже полезно для развития мышления, сразу дойти до оптимального решения обычно трудно, а многое уже придумано до нас.