Пользователь
0,0
рейтинг
14 апреля 2015 в 19:47

Разработка → Сахарные инжекции в C# tutorial

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, новая версия которой доступна по ссылке (резервная ссылка), где можно увидеть их вживую и в действии.
@Makeman
карма
28,0
рейтинг 0,0
Пользователь
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (95)

  • +7
    Про ForEach для IEnumerable — я просто оставлю это(http://blogs.msdn.com/b/ericlippert/archive/2009/05/18/foreach-vs-foreach.aspx) здесь
    • 0
      Соглашусь с тем, что у начинающего программиста может вызвать вопросы наличие двух альтернативных конструкций, выполняющих по сути одно и тоже. Но стоит всё же признать, что в плане красоты и лаконичности кода ForEach-расширение иногда смотрится выгоднее.
    • +1
      Использую свой ForEach только он у меня ещё чуть более хитрый, и возвращает IEnureable Элементов, к которым форич был применён. Иногда крайне полезно.

      Ещё завёл себе
      T MinElement(Func<T, float>)
      И совсем уж страшно удобную штуку
      IEnumerable MinElements(Func<T, float>, int count)

      Такой код может казаться менее читаемым, но зато всё, что вы хотите сделать с последовательностью помешается в три строки вместо того чтобы занимать пол экрана, и от этого читаемость напротив вырастает.
      • 0
        Если использовать стандартные средства, то ваш методы, насколько понимаю, эквивалентены таким конструкциям

         persons.OrderBy(p=>p.Age).First()
         persons.OrderBy(p=>p.Age).Take(10)
        
        • 0
          Ну по результату эквивалентны, а по времязатратам, как вы понимаете, если в списке, 5000 элементов, а вам из них только пять старших нужны, то весьма некислая экономия времени получается.

          Хммм-м. Вернее должна получаться. Я тут задумался, что я же, в общем-то, не мерил. Хрен его знает какие там оптимизации могут быть.
          Но должно получаться сильно быстрее, да.
          • 0
            Конечно, сложно судить о производительности и затратах памяти ваших методов, не имея перед глазами их полной реализации. Возможно, вы применяете какой-то хитрый алгоритм оптимизированный для работы с вещественными числами, большими или маленькими коллекциями — всякое допустимо.

            Но по опыту могу сказать, что зачастую безопаснее использовать стандартные механизмы. Хотя раньше сам решал подобные задачи в лоб, не подозревая даже, что для этого годятся OrderBy, Take и другие методы из этой серии :)
            • +2
              Вот ведь стыд. :(((( Померил скорость и стандартная реально работает быстрее в два раза на списке в 5000 элементов. :((((((((((((
              list2.OrderBy(i => i * 3).Take(20).ToArray();
              list1.MinElements(i => i * 2, 20).ToArray();
              Интересные у них там алгоритмы под капотом. Может в дерево разбивается при первом применении лямбды правда тогда памяти должно скушать, конечно.
              В общем спасибо, это было весьма поучительно.
              • +1
                Алгоритмы под капотом вы всегда можете посмотреть.
              • 0
                Ничего, LINQ всё-равно можно ускорить. Лично я планирую написать пару удобных методов, которых в LINQ нет, и исправить пару линковских родных методов (например Single с предикатом проходит всю коллекцию, даже если нашел два элемента, подходящие под правило).
              • 0
                Также довольно полезен бывает метод Skip. Например, если нужно взять десять минимальных элементов, но не первых, а, скажем, начиная с сотого, то операция будет выглядеть так:

                persons.OrderBy(p=>p.Age).Skip(100).Take(10)
                

                Вообще же в Linq предусмотрены методы на многие случаи из жизни, поэтому прежде чем использовать свои лучше заглянуть в документацию. Хотя свои методы делать тоже полезно для развития мышления, сразу дойти до оптимального решения обычно трудно, а многое уже придумано до нас.
  • +1
    Очень интересная статья, не знал про события и пустые делегаты. Спасибо.
  • +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-то не угодил?
    • 0
      Интересная ссылка про монады. Правда, непривычный синтаксис иногда получается, хотя в некотрых случаях выглядит красивее обычного.

      Ох, перемудрил немного с Await, да, достаточно operation.Result :) Спасибо, что исправили!
      • +1
        Правда, непривычный синтаксис иногда получается, хотя в некотрых случаях выглядит красивее обычного.

        Ничем не непривычнее ваших Of и ForEach

        достаточно operation.Result

        Вот именно, что достаточно. Зачем extension-метод-то писать, который просто вызывает общедоступное свойство?
      • +3
        Простой вызов operation.Result может привезти к дедлоку — если в качестве operation передать результат асинхронной функции, которая использует текущий SynchronizationContext.
        Поэтому решением здесь как раз таки бывает вызов нового таска:
        return Task.Run(() => operation.Result).Result;
        
        • 0
          Смутно припоминаю, что у меня как раз и была ситуация, когда обычный operation.Result не работал, поэтому пришлось создавать новый таск. Хотя в большинстве случаев хватит первого варианта, второй также стоит взять на заметку.
          • +3
            Оба решения, в зависимости от ситуации, могут как работать, так и не работать.
            Решение без Task-а вызовет дедлок в WinForms/WPF приложении, если обращение к Result будет до окончания последнего вызова, захватывающего контекст, внутри асинхронной функции.
            Решение с таском может не работать, например, если внутри асинхронной функции будет обращение к UI…
        • +1
          Разве не достаточно сделать await operation.ConfigureAwait(false)?
          • 0
            И какую задачу это решит? Если operation — асинхронная функция, вызывающая у себя внутри другие асинхронные функции, то это ни на что не повлияет.
            На самом деле, плохо совмещать синхронный и асинхронный код. Но если уж приходится это делать, то нужно это делать с умом, учитывая особенности в каждом конкретном месте.
            Например, если не важно, в каком потоке будет работать код после await, то можно использовать ConfigureAwait(false) на всех библиотечных функциях и после этого можно вызвать свойство Result…
            • +1
              Это решит проблему с дедлоками.

              В принципе, я согласен с тем, что не надо совмещать синхронное с асинхронным, и разбираться надо по месту, но давайте все-таки разберемся.

              Что делает Task.Result? Он блокируется на текущем контексте синхронизации до завершения выполнения задачи. Как следствие, что произойдет при вызове кода: Task.Run(() => operation.Result).Result? Текущий код создаст задачу, которая будет ожидать выполнения переданной задачи, заблокирует текущий контекст и будет ждать выполнения. Если внутри переданной задачи тоже есть блокировка на контексте, то будет все тот же самый дедлок.
            • +1
              Думаю, это решит многие задачи, по крайней мере я почти везде так пишу в async Task-методах, просто await обычно лишь в async void-методах. Как я в своё время специально проверял,
              await operation
              всегда и сразу приводит к возврату в поток SynchronizationContext, если он есть, а какой смысл делать эти переключения в тех классах, что отделены от пользовательского интерфейса? Я полагаю, что нагрузку на UI-поток нужно минимизировать (о чём особенно Android любит напоминать), так что как только ушли от взаимодействия с пользовательским интерфейсом, в диспетчеризации потоков лучше не использовать SynchronizationContext, а вернуться к нему непосредственно перед выдачей результатов пользователю.
              • 0
                Тогда к нам возвращается ручное управление синхронизацией, лишаемся бонуса простоты.

                П.С. у меня в ViewModel OnPropertyChanged всегда синхронизируется с контекстом UI, ну так на всякий случай :)
                • 0
                  Во ViewModel у меня тоже (и в контроллере в случае iOS и т. п.). Но там у меня обычно как раз async void-методы, в них и вызываются всякие OnPropertyChanged в UI-потоке после вызова async Task-метода без ConfigureAwait. А использование ConfigureAwait(false) у меня в модели данных и далее.
        • +1
          Если для завершения operation надо освободить контекст синхронизации — то какая разница сколько задач будет создано? При синхронном ожидании висеть будет вся цепочка.
          • 0
            А вот такой вариант может оказаться полезным (если не выпадет по переполнению стека):

            if (SynchronizationContext.Current is WindowsFormsSynchronizationContext)
            {
                while (operation.State не помню дальше условие) Application.DoEvents();
            }
            return operation.Result;
            
        • +3
          return Task.Run(() => operation.Result).Result;
          


          Этот вызов так же приведет к блокировке, потому что текущий поток будет ждать окончания нового таска, который в свою очередь для окончания требует синхронизации в ожидающий поток.
          • 0
            Согласен. В моём коде есть ошибка.
            Если operation будет вызван заранее (как при использовании extension-метода), то будет дедлок:
            var operation = OperationAsync();
            return Task.Run(() => operation).Result; // Deadlock!!
            

            Однако дедлока можно избежать, если сделать такой вызов:
            return Task.Run(() => OperationAsync()).Result;
            


            В этом случае OperationAsync() не будет захватывать контекст синхронизации, и как результат не будет дедлока.
            Следствием этого всего является то, что невозможно написать extension-метод для Task-а без дедлока в общем случае.
            • 0
              Я бы описал метод решения задачи иначе, для каждого случая решение свое, но программист должен отчетливо понимать, что происходит, а не тупо лепить что куда попало.

              Мой личный хит это HttpClient.GetStringAsync(...).Result из UI потока, для WP гарантированная смерть :)
    • –3
      Да, Await()-метод прекрасен своей бесполезностью. из той же серии встречал:
      public static bool IsNullOrEmpty(this string value)
      {
         return string.IsNullOrEmpty(value);
      }
      

      Неоднократно встречал такой идиотизм.
      Вообще экстеншн-методы прям-таки манят слабых духом впасть в маразм расширенияклепания на каждый чих. С матом и подручными предметами приходится отстаивать system.object и классы фреймворка, потому что если не уследить, то там начинается такая дурь…
      • +4
        Не буду пытаться вам ничего доказать, но этот «идиотизм» не все таковым считают.
        • +1
          я думаю, многим будет интересно узнать, где и зачем это может пригодиться
          • 0
            Например чтобы сделать цепочку вызовов. Для этого же например существует pipeline-оператор в функциональных языках. Например в F# это |> и <|
    • –1
      monads.net

      Сборник «happy debugging!» Особо умиляют полезностью методы TryDo/Catch, If/IfNot — просто гимн абсурду.
      Прекрасный пример, когда из одного метода With было как-то стыдно делать библиотеку и потому туда насовали абсурдных «подобных» методов просто чтоб было. При дебаге такого когда вспомнишь весь словарь обсценной лексики в отношении автора.
      • +2
        Особо умиляют полезностью методы TryDo/Catch, If/IfNot — просто гимн абсурду.

        Вы, видимо, никогда не писали сложный парсинг, каждый шаг которого нужно залогировать, но при этом должно не падать, а возвращать дефолтное значение. Или маппинг класса «если значение число — то пишем его, любое другое — игнорируем».

        При дебаге такого когда вспомнишь весь словарь обсценной лексики в отношении автора.

        Для меня смысл написания монадического кода — в уменьшении количества дебага.
        • –6
          точно также могу ответить, что, видимо, вы никогда не писали код, который после вас будет использоваться кем-то ещё
          • 0
            Аргументы будут? lair утверждает, что дебаггинга меньше и оно понятно почему или вам непонятно? Или вы не согласны, но тогда с чем?
  • +1
    int 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


    Тут правда недостаток, что нужно указывать два типа — откуда кастуем и куда, среда не позволяет опустить типизацию, если хотя бы одну не может разрезолвить… Так что стоит подумать, как это можно решить.
    • 0
      Это может решить
      public static TResult Of<TResult>(this object o)

      О недостатках я догадываюсь, но вы правда от них будете спать хуже по ночам?
      • 0
        Это не может решить, потому что это оригинальный вариант, где изменили имя T на TResult
        • 0
          Получается оригинальный вариант достаточно хорош.
  • +6
    Извините, а можно я тут поною?
    инжекции
    Декларация
    В русском языке конечно есть такие слова, но они не о том, о чём вы пишете.
    В вашем случае это, скажем, внедрение и объявление.
    Не загрязняйте, пожалуйста, язык. И не приучайте к этому в статьях, тем более с ярлычком tutorial.
    • 0
      Термин «SQL-инъекция» существует, а термина «SQL-внедрение» — нет. Можно сказать «Внедрение вредоносного SQL-кода», но это как бы не эквиваленты — одно слово и целое предложение.

      Я тоже радею за чистый язык, но одно дело, когда человек говорит "Чтобы получить доступ к имплементации необходимо кастовать инстанцию класса к интерфейсу" (хотя после небольшого тренинга воспринимать такие фразы не составляет особого труда), а другое дело статья, где встречается пара заимствованных слов, использующихся по делу.

      Закончу в итоге стихом 1854 года
      Тут филолог для корнесловья
      Отыщет новые условья,
      Найдет, что русский корень есть
      И слову чуждому «визиты»,
      Успев стократно произнесть
      Извозчику: «Да ну ж! вези ты!»
      Язык наш — ключ заморских слов:
      Восстань, возрадуйся, Шишков!
      Не так твои потомки глупы;
      В них руссицизм твоей души,
      Твои родные «мокроступы»
      И для визитов хороши.
      Зачем же всё в чужой кумирне
      Молиться нам? — Шишков! Ты прав,
      Хотя — увы! — в твоей «ходырне»
      Звук русский несколько дырав.
      Тебя ль не чтить нам сердца вздохом,
      В проезд визитный бросив взгляд
      И зря, как, грозно бородат,
      Маркер трактирный с «шаропёхом»
      Стоит, склонясь на «шарокат»?
    • +4
      В русском языке есть слово «инъекция», вполне подходит под назначение.
      • +2
        Оно не просто подходит, оно как раз и является переводом слова injection.
    • +2
      Стараюсь внимательно относиться к терминам.

      Инжекции — это инженерный термин, близким переводом которого с английского является внедрение. О смысловых оттенках можно спорить долго, но существует такое понятие как инжектор — механизм или приспособление для внедрения чего-либо. В технической области это слово чаще употребляется, чем, скажем, инъектор или внедритель и звучит благозвучно, а также наиболее созвучно с английским написанием, поэтому выбор пал именно на понятие инжекции.

      Декларировать, декларация чего-либо достаточно часто встречаются в документации для разработчиков.

      На мой взгляд, чем ближе и созвучнее перевод слова к английскому оригиналу, тем меньше путаницы это вносит в его дальнейшее использование.

      Спасибо, что следите за чистотой языка, это тоже важно!
      • 0
        Сделаю ещё небольшое пояснение к сказанному.

        Например, начинающий разработчик встретился с понятием внедрение зависимостей и хочет найти английскую литературу по этому вопросу. Для этого нужно сформулировать соответствующий запрос. Но, например, гугл предлагает следующие переводы для слова внедрение: introduction, implantation, plantation, inculcation, intrusion, intercalation, infiltration.

        Среди них даже не встречается injection, которое состоит в изначальном понятии Dependency Injections, поэтому в переводах технических терминов иногда проще не уходить далеко от оригинала. Да и это помогает легче запоминать иностранные слова.
        • +2
          Декларировать, декларация чего-либо достаточно часто встречаются в документации для разработчиков.
          Смею предположить, речь идёт про документацию либо изначально русскую, либо с второсортным переводом на русский.

          MSDN — Деклараторы и объявления переменных
          (Хм, официальная русская документация есть только у Microsoft?)
          SwiftBook — Объявление констант и переменных
          Например, начинающий разработчик встретился с понятием внедрение зависимостей и хочет найти английскую литературу по этому вопросу.
          Я ожидаю, что в хорошей литературе тут же будет дан оригинальный английский термин, как в википедии:

          Внедрение зависимости (англ. Dependency injection, DI) — процесс предоставления внешней зависимости программному компоненту.

          либо

          Потокобезопасность (англ. Thread safety) — это концепция программирования...
          • –1
            Я ожидаю, что в хорошей литературе тут же будет дан оригинальный английский термин, как в википедии

            В том-то и дело, что новый термин может встретиться в любом месте, например, на форуме, и человеку захочется с ним разобраться. Первым делом он начнёт поиски с наиболее предсказуемых вариантов перевода.

            Конечно, методом проб и ошибок, скорее всего, он найдёт, что нужно, однако если слова из разных языков схожи, то этот процесс произойдёт быстрее. Да общей путаницы в понятиях будет меньше.

            Например, оригинальное название статьи с MSDN — Declarators and Variable Declarations. Конечно, русский язык богат, и можно подобрать множество синонимов к слову Declaration, но всё-таки, на мой взгляд, хорошо, когда различные термины более-менее универсальны и созвучны на разных языках. Это моё личное мнение, которого придерживаюсь при написании статей.
            • 0
              Не нужно подбирать никакие синонимы или созвучные аналоги, если есть конкретный, устоявшийся, общеиспользуемый перевод.
              Declaration — это объявление.

              Кейс про новый термин на форуме и последующий поиск англоязычного термина, если честно, я представляю себе с трудом.
  • +1
    Я бы в методы класса Sugar атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)] добавил.
    • 0
      Спасибо, хорошая рекомендация. Только она подойдёт лишь для Desktop-версии библиотеки. В Portable-варианте перечисление MethodImplOptions включает только два флага NoInlining и NoOptimization. Возможно, компилятор самостоятельно производит Inlining, где это нужно.
      • 0
        Ну да, но их всегда можно в #if… #endif обернуть
    • +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?).
      • +2
        Значение перечисления в атрибуте говорит о том, что нужно инлайнить, если возможно. А то что компилятор пока не умеет — не зона ответственности прикладного программиста.
        Потому что сейчас не умеет, потом научится (по ссылке об этом говорится: «JIT today can't inline methods that contains „starg“ opcode»). Или другой компилятор будет уметь. А код с атрибутом останется.
        • +1
          RyuJIT, который как бы и является новым джитом, имеет тот же самый баг, ведь он и основан-то на кодовой базе 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
          


          Так что я бы не рассчитывал на изменение этого поведения.
          • +1
            Мне кажется мы с вами о разных вещах говорим. С точки зрения текущей работы компиляторов — вы правы. С точки зрения прикладного программиста, который не заморачивается прогнозированием будущего (которое не спрогнозировать), для кода вида:
                    public static T Of<T>(this object o)
                    {
                        return (T) o;
                    }
            

            запросить у компилятора встраивание разумно.
            • –1
              Ибо закон дырявых абстракций. То, что вы пишете на ЯВУ не значит, что мы не должны понимать, как работает процессор, и какой ассемблер генерирует JIT и почему. Просить есть смысл, если компилятор может это сделать, иначе это просто захламление ненужными атрибутами.
      • +1
        Что-то не могу найти в этом коде ни starg, ни ldarga… Вижу только ldarg.0 — но эта инструкция встречается в каждом первом методе, она никак не может мешать инлайнить.
  • 0
    А ссылка на репозиторий кода есть? Гитхаб там какой-нибудь…
    • –3
      Да, исходные коды, конечно, есть. Ссылка в окончании статьи. На всякий случай продублирую тут: Aero Framework.
  • 0
    Кроме того, искусственно возможно даже смоделировать ситуацию вызова обработчика у объекта уже утилизированного сборщиком мусора. Конечно, это приведёт к исключению.


    Покажите как.
    • 0
      По этой теме можно зачитаться материалами и комментариями в статье Потокобезопасные события в C# или Джон Скит против Джеффри Рихтера.

      Вызов обработчика у отписавшегося объекта воспроизводится легко (например, достаточно поставить вызов Thread.Sleep() перед handler(o, e)). Экспериментально установлено, что var handler = Handler удерживает оба объекта от сборки мусора даже если других ссылок на объекты не осталось, но, теоретически, как мне думается, операция handler = Handler может произойти не атомарно, и если в этот короткий промежуток времени произойдёт отписка и сборка мусора, то в дальнейшем получится исключение. К сожалению, такое воспроизвести сложно.

      Возможно, конечно, и ошибаюсь в чём-то :)
    • 0
      Ну вот на коленке пример набросал:
      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");
          }
      }
      
      
      • 0
        А где события? Где вызов по событию метода собранного объекта?
        • 0
          С событиями можно так придумать:
          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);
              }
          }
          


          Если закоментить предпоследнюю строчку, всё отработает норм. Тут нет сборщика, то там та же логика будет
          • 0
            Это шутка? Приведенные примеры это банальные NPE из за коряво написанного кода, а ни как не вызов обработчика события у собранного GС объекта.
          • 0
            И да это еще один минус в копилку public static event EventHandler SomeEvent = delegate { };
            Вместо NPE при вызове делегата получаем NPE в методе обработчика!
            • +1
              Тут как ни вызывай, один фиг получишь тыкву. Тут все неплохо расписано. В итоге приходим к выводу, что с текущей событийной моделью в многопотоке в любом случае имеем боль.
              • 0
                А для меня важно, то, что можно огрести проблем с вызовом кода который должен уже быть отписанным, проблема не новая и давно решается на автомате, в то время как проблема доступа к объекту собранному GC, это совершенно другая тема.
                И я хочу увидеть возможно ли это на практике, а вместо этого вижу надуманные примеры показывающие другую проблему, которая меня не интересует.
                • –1
                  Долго искал, но, к сожалению, не нашёл того комментария, где когда-то читал о событиях и сборщике мусора. Или мне приснилось?

                  В общем, проблема, потенциально, может возникнуть в том случае, если операция копирования значения в переменную выполняется не атомарно (var handler = Handler) и поток прервётся не докопировав всё как положено, а за это время кто-то успеет отписаться и вызвать сборщик мусора. После этого управление вернётся исходному потоку и копирование завершиться, но исходный объект будет уничтожен сборщиком мусора, что, очевидно, приведёт к исключению.

                  Тут всё зависит от диспетчеризации потоков, поэтому вручную такое весьма сложно воспроизвести. И если действительно проблема существует, то она крайне трудноуловима.
                  • 0
                    Эээ, что за «копирование значения в переменную»? var handler = Handler — это присвоение ссылки (reference assignment), и оно в .net атомарно.
                    • –1
                      Сразу прошу извинить меня, если несу чушь, но…
                      Насколько мне известно, переменные делегатов являются неизменяемыми типами (immutable types), как структуры, то есть во время присваиваивания создаётся новая копия данных, а не просто присваивается ссылка. Вот не знаю только, происходит ли это атомарно.

                      MSDN
                      Структуры копируются при присваивании. При присваивании структуры к новой переменной выполняется копирование всех данных, а любое изменение новой копии не влияет на данные в исходной копии. Это важно помнить при работе с коллекциями типов значений, такими как Dictionary<string, myStruct>.
                      • 0
                      • 0
                        Пример:
                                    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();
                        
                      • 0
                        A delegate is a reference type

                        То, что он immutable, еще не означает, что он структура. А в вашем случае он еще и не делегат, а список делегатов, и отчетливо mutable, иначе бы (а) не работала бы подписка/отписка и (б) не были бы нужны все пляски с копированием ссылок и проверками. У неизменных объектов нет проблем с многопоточностью.
                        • +1
                          Список делегатов — тоже делегат. И он все равно immutable — добавление делегата к списку создает новый список, а не изменяет существующий.

                          У неизменных объектов нет проблем с многопоточностью.
                          Поправка: нет проблем «внутри». Код снаружи все равно может столкнуться с проблемами — что иногда и происходит.
                          • 0
                            Вы правы, да. Я зачем-то перепутал изменяемость экземпляра с изменяемостью поля, где хранится ссылка.
    • 0
      Всё же решил убрать это утверждение из статьи, поскольку оно спорное.
      Не мной оно было придумано, но, к большому сожалению, не удалось найти тот первоисточник, где это изначально встретил.
  • 0
    Я как обычно внесу свою долю скепсиса :)

    Все описанное выше, очень дорого в рамках ограниченных вычислительных ресурсов.

    Например лямды для определения свойств, в свое время в профайлере доходило до 50% таймфрейма на разбор деревьев выражений, и дело даже не в разборе, а в том, что каждый такой выбов влечет за собой сначала генерацию дерева, потом разбор, а потом утилизацию.

    То же с пустым делегатом, вызов пустого делегата всегда дороже проверки на пустой указатель. А если учитывать то, что этот вызов почти на 100% будет из UI потока, то это гарантированная задержка обработки потока событий пользователя, пусть очень ненадолго, но задержка.

    Если говорить не о WPF, а о Silverlight то там, подписка на CanBeExecutedChanged выполняется с помощью слабых ссылок, утечек не будет.

    А вот подписка на событие с помощью лямды, это почти гарантированная утечка памяти!

    И уж точно отсутствие точного понимания того, как работает асинхронный не исправить хитрыми реализациями Await

    Большая часть вышесказанного относится к разработке WP SL, который приучил меня наперед думать о трате ресурсов, даже на мелочи!
    • 0
      Будем реалистами :) все эти задержки в большинстве случаев настолько ничтожны, что преимущества от их испольования затмевают собой недостатки. Тем более ничто не запрещает комбинировать разные подходы между собой, где это на самом деле нужно.

      А вот подписка на событие с помощью лямды, это почти гарантированная утечка памяти!

      Поподробнее, что вы имеете в виду? Да, как и любые другие подписки они могут привести к утечкам памяти.

      viewModel[() => viewModel.Name].PropertyChanged += (o, e) => { // do somethig };
      

      this[() => Name].PropertyChanged += (o, e) => { // do somethig };
      

      Соглашусь, что с первым вариантом внешней подписки нужно быть внимательным, но второй замкнутый вполне безопасен.
      • +1
        А я и есть реалист, потер из своего кода, после того как в определенный момент профайлер показал мне наглядно, что я не прав!
        А какой смысл подписываться на свое событие? Это оверхед на ровном месте, разве нет?
        • 0
          А я и есть реалист, потер из своего кода, после того как в определенный момент профайлер показал мне наглядно, что я не прав!
          Наверно мне везёт, но пока ни разу не возникало хоть сколько-нибудь заметных тормозов из-за лямбд, какие бы они медленные ни были в синтетических тестах.

          А какой смысл подписываться на свое событие? Это оверхед на ровном месте, разве нет?
          Не совсем понимаю, что вы имеете в виду.

          Конструкция
          this[() => Name].PropertyChanged += (o, e) => { // do somethig };
          

          эквивалентна
          this.PropertyChanged += (o, e) =>
          {
              if (e.PropertyName != "Name") return;
              // do something
          };
          

          Смысл в том, чтобы избавиться от if-оператора и получить более компактную запись. На практике порой хватает одной-двух строк кода вмето, как минимум, четырёх (при условии стандартного форматирования).
        • 0
          Ради интереса замерил разницу в скороти чтения и записи на реализациях INotifyPropertyChanged с лямбда-выражениями и без них.

          Получившиеся результаты:
          По скороти доступа (get) классическая реализация выигрывает примерно в 100 раз
          По записи (set) классическая лучше примерно в 20 раз

          На результаты немного также влияет упаковка-распаковка численных значений. В лямбда-реализациях числа хранятся в виде объектов (упакованы), поэтому сразу может показаться, что это хуже. Однако если вдуматься, UI-привязки работают через рефлексию, поэтому, скорее всего, в классических реализациях упаковка происходит неявно. Тут зависит от конкретного случая, где и как используется свойство чаще, в коде вью-модели или на UI.

          Разница в 100 и 20 раз выглядит внушительно, но на деле это оказываются наносекунды (скорость записи, более медленная операция, у меня получилась около 2нс для лямбд, но зависит от производительности устройства). Частота же обносвления экрана в 100 Гц эквивалентна10 мс, поэтому в реальных приложениях на FPS это оказывает ничтожное влияние, если вообще оказывает. Экономятся лишь такты процессора.

          В плане красоты разработки и гибкости лямбда-выражения выигрывают, а действительно значимого падения производительности никак не происходит в большинстве обычных случаев, поэтому боятся их уж точно не стоит, это факт.
          • 0
            Это довольно серьёзная разница в производительности. Может быть для современных PC это не существенная разница, но при использовании такого кода на мобильных устройствах это может быть очень заметно.
            • 0
              Вообще, было бы интересно посмотреть на методику проведения бенчмарка.
      • 0
        Подписка на событие 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;
        		}
        	}
        
      • +1
        Это зависит. Сначала делаешь сложные конструкции в каждом свойстве, а потом вдруг тебе нужно создать тайловую карту 100 на 100 в которой в каждой ячейке объект с 10 свойствами, и склонировать её. И вот где-то в этот момент понимаешь, что управляемый код иногда ваще вава.
  • 0
    К большинству из этих вещей так или иначе приходишь по мере работы, стараясь избежать написания однообразного кода. К счастью, необходимость в лямбдах для описания полей отпадет в С# 6 с появлением ключевого слова nameof.

    Не понимаю только, зачем для получения ссылки на PropertyChanged приспособили индексатор. Для общего совета это слишком смелое допущение — например, если модель составная и описывает список вложенных моделей, то новый индексатор будет семантически конфликтовать с существующим. Более поддерживаемое решение — сделать приватный метод Pty(Expression<Func<T>> expr) и заранее продумать архитектуру, чтобы для каждого свойства, изменение которого планируется отслеживать снаружи, было отдельное событие.
    • +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); }
      }
      


      Зато нет деревьев и индексаторов
      • +4
        Чтобы избежать упаковки, советую заменить:
        if (Equals(field, value))

        на:
        if (EqualityComparer<T>.Default.Equals(field, value))
        • +1
          Не могу плюсовать, поэтому напишу СПАСИБО!
          • 0
            Я за вас плюсану. Использую похожую штуку но до такого решения не допёр.
    • 0
      Паттерн IDataErrorInfo работает через индексатор и это выглядит довольно красиво. На этой почве возникла идея неявных индексных свойств, о которых можно прочитать в документации к библиотеке, где ключом так же является строковое имя свойства (полиморфизм в действии). После этого очень логичным шагом показалась перегрузка индексатора для возможности подписки на изменение значений свойств (INotifyPropertyChanged). Кроме того существует перегрузка индексатора и для команд (ICommand).

      На мой взгляд, всё это выглядит очень эстетично и стройно. Во-первых, модификатор this часто опускается и его редко видно, а тут ему нашлось интересное применение, во-вторых же, синтаксис подсказывает, что операция производится с текущим объектом.

      если модель составная и описывает список вложенных моделей, то новый индексатор будет семантически конфликтовать с существующим

      Не совсем понял, что вы имеете здесь в виду.

      и заранее продумать архитектуру, чтобы для каждого свойства, изменение которого планируется отслеживать снаружи, было отдельное событие.

      В текущей реализации для каждого отслеживаемого свойства создаётся отдельный обработчик как снаружи, так и изнутри.
      • 0
        Паттерн IDataErrorInfo работает через индексатор и это выглядит довольно красиво.

        Имхо, это одно из наиболее странных архитектурных решений в .NET, что мне доводилось видеть.
        Не совсем понял, что вы имеете здесь в виду.

        Вот например:

        var a = usersListModel[1];
        var b = usersListModel["Name"];
        var c = usersListModel[() => CollectionChanged];
        

        Если для индексатора есть несколько перегрузок, которые выполняют абсолютно разные действия, то это сбивает с толку. Гораздо лучше было бы использовать метод с понятным названием.
        • 0
          Мне нравится полиморфизм и вариант с индексатором :)
          Стандартных подсказок достаточно, чтобы ни в чём не запутаться. Зато вью-модели получаются такими, что любо глянуть.

          Исходный код библиотеки полностью открыт, поэтому при желании можно реализовать всё на свой вкус!

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.