Pull to refresh

Comments 96

Соглашусь с тем, что у начинающего программиста может вызвать вопросы наличие двух альтернативных конструкций, выполняющих по сути одно и тоже. Но стоит всё же признать, что в плане красоты и лаконичности кода ForEach-расширение иногда смотрится выгоднее.
Использую свой ForEach только он у меня ещё чуть более хитрый, и возвращает IEnureable Элементов, к которым форич был применён. Иногда крайне полезно.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

Для меня смысл написания монадического кода — в уменьшении количества дебага.
точно также могу ответить, что, видимо, вы никогда не писали код, который после вас будет использоваться кем-то ещё
Аргументы будут? lair утверждает, что дебаггинга меньше и оно понятно почему или вам непонятно? Или вы не согласны, но тогда с чем?
int i = 10;
Console.WriteLine(i.Of<long>()); // bam

т.к. каст Of&ltT> полезен только к производным классам, а структуры не наследуются, следует так переписать:

        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


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

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

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

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

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

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

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

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

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

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

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

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

либо

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

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

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

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

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


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

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


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

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

Возможно, конечно, и ошибаюсь в чём-то :)
Ну вот на коленке пример набросал:
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");
    }
}

А где события? Где вызов по событию метода собранного объекта?
С событиями можно так придумать:
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);
    }
}


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

Articles