Pull to refresh

Сахарные инжекции в C#

Reading time 5 min
Views 42K
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)
        {
            // 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, новая версия которой доступна по ссылке (резервная ссылка), где можно увидеть их вживую и в действии.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+22
Comments 96
Comments Comments 96

Articles