C# — продуманный и развитый язык программирования, в котором предусмотрено немало синтаксического сахара, упрощающего написание рутинного кода. Но всё-таки существует ряд сценариев, где нужно проявить некоторую смекалку и изобретательность, чтобы сохранить стройность и красоту.
В статье мы рассмотрим некоторые такие случаи, как широкоизвестные, так и не очень.
Декларация и вызов событий
Иногда на собеседовании могут задать вопрос, как правильно вызывать события?
Классический способ декларации события выглядит следующим образом:
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) { var result = default(TResult); Task.Factory.StartNew(async () => result = await operation).Wait(); return 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, новая версия которой доступна по ссылке, где можно увидеть их вживую и в действии.
ссылка на оригинал статьи http://habrahabr.ru/post/255759/
Добавить комментарий