Правостороннее присваивание и другие необычные приёмы программирования в C#

    В этой статье будут рассмотрены с нового ракурса такие привычные и фундаментальные вещи, как присваивание и передача параметров в методы.

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

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

    За дело!

    image

    1. Правосторонние операции: присваивание, декларация переменных и приведение типа


    Существует два направления присваивания: правое и левое

    IModel m;
    m = GetModel(); // left side assignment
    GetModel().To(out m); // right side assignment
    

    Да, все методы с `out` и частично с `ref` параметрами являются вариациями правостороннего присваивания.

    С ранних версий C# поддерживает `out` и `ref` параметры, что даёт некоторые преимущества, но не очень впечатляющие, однако C# 7 совершил эволюционный скачок!

    Добавление синтаксического сахара вроде `o.To(out var x)` позволило объединить правостороннее присваивание вместе с декларацией переменной, что дало возможность обобщить и уточнить некоторые распространённые сценарии в программировании…

    Исторически более привычной является традиционная левостронняя ориентация при присваивании. Возможно, это влияние математики, где `y = f(x)` является стандартной нотацией. Но на практике в программировании такое положение вещей вызывает некоторые ограничения (будут упомянуты далее) и неудобства, например, визуальный переизбыток скобок ('parentheses hell') при цепочном привидении типов для урегулирования приоритетов

    public void EventHandler(object sender, EventArgs args) =>
    	((IModel) ((Button) sender).DataContext).Update();
    
    // in a general case there is not possible settle priorities without parentheses
    // (IModel) (Button) sender.DataContext.Update();
    

    что подталкивает разработчиков к использованию многословных либо плохих решений наподобие

    /* NullReferenceException instead of InvalidCastException */
    public void EventHandler(object sender, EventArgs args) =>
    	((sender as Button).DataContext as IModel).Update();
    
    /* miss of InvalidCastException */
    public void EventHandler(object sender, EventArgs args) =>
    	((sender as Button)?.DataContext as IModel)?.Update();
    
    /* verbose */
    public void EventHandler(object sender, EventArgs args)
    {
    	var button = (Button) sender;
    	var model = (IModel) button.DataContext;
    	model.Update();
    }
    

    Тем не менее существует менее очевидное, но более элегантное решение проблемы путём правостороннего приведения типа

    public void EventHandler(object sender, EventArgs args) =>
    	sender.To<Button>().DataContext.To<IModel>().Update();
        
    public static T To<T>(this object o) => (T) o;
    

    При дальнейшем обобщении подхода мы получаем следующий набор методов-расширений

    public static object ChangeType(this object o, Type type) =>
    	o == null || type.IsValueType || o is IConvertible ?
    		Convert.ChangeType(o, type, null) :
    		o;
    
    public static T To<T>(this T o) => o;
    public static T To<T>(this T o, out T x) => x = o;
    public static T To<T>(this object o) => (T) ChangeType(o, typeof(T));
    public static T To<T>(this object o, out T x) => x = (T) ChangeType(o, typeof(T));
    

    которые позволяют отзеркалить направление всех трёх базовых операций: декларации переменной, привидения типа и присваивания значения

    sender.To(out Button b).DataContext.To(out IModel m).Update();
    /* or */
    sender.To(out Button _).DataContext.To(out IModel _).Update();
    

    Эти примеры иллюстрируют, что исторически C# потерял что-то вроде оператора `to`. Сравните

    ((sender to Button b).DataContext to IModel m).Update();
    ((sender to Button _).DataContext to IModel _).Update();
    /* or even */
    sender to Button b.DataContext to IModel m.Update();
    sender to Button _.DataContext to IModel _.Update();
    


    2. to-with паттерн


    Многим разработчикам хорошо знакомы инициализационные блоки в духе `json`

    var person = new Person
    {
    	Name = "Abc",
    	Age = 28,
    	City = new City
    	{
    		Name = "Minsk"
    	}
    };
    

    вместо

    var person = new Person();
    person.Name = "Abc";
    person.Age = 28;
    person.City = new City();
    person.City.Name = "Minsk";
    

    Они довольно хороши и удобны, поскольку позволяют структурировать код и сделать его более чистым и читаемым. Однако у таких блоков есть недостаток — они применимы лишь в связке с конструктором. Но иногда для создания объектов предпочтительно использование методов-фабрик, и, к сожалению, в таких случаях инициализационные блоки непригодны

    var person = CreatePerson()
    {
    	Name = "Abc",
    	Age = 28,
    	City
    	{
    		Name = "Minsk"
    	}
    }; // cause compile errors
    

    Другими словами, простая замена конструктора на метод-фабрику может вызывать кардинальную смену структуры кода. Как этого избежать?

    Для начала рассмотрим два метода-расширения

    public static T To<T>(this T o, out T x) => x = o;
    public static T With<T>(this T o, params object[] pattern) => o;
    

    Они позволяют нам переписать код следующими способами

    var person = new Person().To(out var p).With
    (
    	p.Name = "Abc",
    	p.Age = 28,
    	p.City = new City().To(out var c).With
    	(
    		c.Name = "Minsk"
    	)
    );
    

    либо

    var person = CreatePerson().To(out var p)?.With
    (
    	p.Name = "Abc",
    	p.Age = 28,
    	p.City.To(out var c)?.With
    	(
    		c.Name = "Minsk"
    	)
    );
    

    * при желании можно поиграть с примерами в онлайн-компиляторе по ссылке

    Это чуть более многословная, но обощённая запись, в сравнении с инициализационными блоками. Немаловажно, что поддерживаются рекурсивные выражения совместно с оператором проверки на `null` (`?`), а также вызовы функциональных методов, возвращающих значения, например,

    var person = CreatePerson().To(out var p)?.With
    (
    	...
    	p.ToString().To(out var personStringView)
    );
    

    Однако предложенная реализация метода `With` имеет несколько недостатков:

    • создание массивов и выделение для них памяти (array allocations)
    • возможная упаковка для типов-значений (boxing for value types)

    Эти проблемы могут быть устранены следующим образом

    public static T With<T>(this T o) => o;
    public static T With<T, A>(this T o, A a) => o;
    public static T With<T, A, B>(this T o, A a, B b) => o;
    public static T With<T, A, B, C>(this T o, A a, B b, C c) => o;
    		/* ... */
    

    Если же необходимо получить крупное, но хорошо оптимизированное `With` выражение, то допустима конкатенация (склеивание) нескольких более коротких выражений

    GetModel().To(out var m)
    	.With(m.A0 = a0, ... , m.AN = an).With(m.B0 = b0, ... ,m.BM = bM).Save();
    

    Данный подход имеет производительность предельно близкую к идеальной.

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

    public static TX Put<T, TX>(this T o, TX x) => x;
    public static TX Put<T, TX>(this T o, ref TX x) => x;
    

    Дело в том, что при вызове метода-расширения для структуры происходит её копирование, в результате чего метод `With` возвращает её оригинал вместо модифицированного экземпляра

    static AnyStruct SetDefaults(this AnyStruct s) =>
    	s.With(s.Name = "DefaultName").Put(ref s);
    

    Также `With` метод полезен в подобных сценариях

    // possible NRE
    void UpdateAppTitle() => Application.Current.MainWindow.Title = title;
    
    // currently not supported by C#, possible, will be added later
    void UpdateAppTitle() =>
    	Application.Current.MainWindow?.Title = title;
    
    // classical solution
    void UpdateAppTitle() {
    	var window = Application.Current.MainWindow;
    	if (window != null) window.Title = title;
    }
    
    void UpdateAppTitle() =>
    	Application.Current.MainWindow.To(out var w)?.With(w.Title = title);
    

    Это базовая информация о `to-with` паттерне, но не вся.

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

    Это означает, что мы можем его использовать для инициализации и деконструкции объектов одновременно!

    GetPerson().To(out var p).With
    (
    	/* deconstruction-like variations */
    	p.Name.To(out var name), /* right side assignment to the new variable */
    	p.Name.To(out nameLocal), /* right side assignment to the declared variable */
    	NameField = p.Name, /* left side assignment to the declared variable */
    	NameProperty = p.Name, /* left side assignment to the property */
    
    	/* a classical initialization-like variation */
    	p.Name = "AnyName"
    )
    

    Как видно, обычные `json` подобные инициализационные блоки являются лишь ограниченной (отчасти из-за левостороннего присваивания) частной синтаксической вариацией намного более обобщённого `with` паттерна.

    Кроме того, подобный подход применим и для инициализаторов коллекций

    public CustomCollection GetSampleCollection() =>
    	new CustomCollection().To(out var c).With(c.Name = "Sample").Merge(a, b, c, d);
    
    /* currently not possible */
    public CustomCollection GetSampleCollection() =>
    	new CustomCollection { Name = "Sample" } { a, b, c, d };
    

    где

    public static TCollection Merge<TCollection, TElement>(
    	this TCollection collection, params TElement[] items)
    	where TCollection : ICollection<TElement> =>
    	items.ForEach(collection.Add).Put(collection);
    

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

    if (GetPerson() is Person p && p.Check
    	(
    		p.FirstName is "Keanu",
    		p.LastName is string lastName,
    		p.Age.To(out var age) > 23
    	).All(true)) ...
        
    if (GetPerson() is Person p && p.Check
    	(
    		p.FirstName.Is("Keanu"), /* check for equality */
    		p.LastName.Is(out var lastName), /* check for null */
    		p.City.To(out var city).Put(true), /* always true */
    		p.Age.To(out var age) > 23
    	).All(true)) ...
    
    case Person p when p.Check
    	(
    		p.FirstName.StartWith("K"),
    		p.LastName.StartWith("R"),
    		p.Age.To(out var age) > 23
    	).Any(true): ...
    
    case Point p when p.Check
    		(
    		p.X > 9,
    		p.Y > 7 && p.Y < 221
    		p.Z > p.Y
    		p.T > 0
    	).Count(false) == 2: ...
    

    Взгляните

    public static bool[] Check<T>(this T o, params bool[] pattern) => pattern;
    


    3. Другие фишки


    put паттерн


    Предназначен для смены контекста в цепочке вызовов, а также декларации произвольного значения в любом контексте

    use паттерн


    Позволяет объявить новую переменную в цепочке вызовов либо выполнить сторонний метод

    if (GetPerson() is Person p && p.Check
    	(
    		...
    		p.City.To(out var city).Put(true), /* always true */
    		p.Age.To(out var age) > 23
    	).All(true)) ...
    


    persons.Use(out var j, 3).ForEach(p => p.FirstName = $"Name{j++}");
    


    private static bool TestPutUseChain() =>
    	int.TryParse("123", out var i).Put(i).Use(Console.WriteLine) == 123;
    


    new паттерн


    Предоставляет возможность использовать вывод типов при декларации массивов и коллекций, а также создавать объекты с помощью обобщённого метода

    var words = New.Array("hello", "wonderful", "world");
    var ints = New.List(1, 2, 3, 4, 5);
    
    var item = New.Object<T>();
    


    value propagation / group assignment


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

    var (x, y, z) = 0;
    (x, y, z) = 1;
    
    var ((x, y, z), t, n) = (1, 5, "xyz");
    


    lambda-styled type matching


    Альтернатива классическому оператору `switch` на основе лямбда-выражений

    public static double CalculateSquare(this Shape shape) =>
    	shape.Match
    	(
    		(Line _) => 0,
    		(Circle c) => Math.PI * c.Radius * c.Radius,
    		(Rectangle r) => r.Width * r.Height,
    		() => double.NaN
    	);
    

    Детальные реализации и примеры кода находятся по ссылкам
    Github mirror: implementation / some tests
    Bitbucket mirror: implementation / some tests

    Результаты


    Рассмотренные расширения очень помогают при написании `expression-bodied`методов, а также позволяют сделать код более чистым и выразительным. И если ты тоже ощутил вкус этих расширений, то приятного применения на практике!

    Послесловие от автора


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

    Похожие публикации

    Комментарии 369
      +4
      GetPerson().To(out var p).With
      (
      /* deconstruction-like variations */
      p.Name.To(out var name), /* right side assignment to the new variable */
      p.Name.To(out nameLocal), /* right side assignment to the declared variable */
      NameField = p.Name, /* left side assignment to the declared variable */
      NameProperty = p.Name, /* left side assignment to the property */
      /* a classical initialization-like variation */
      p.Name = "AnyName"
      )

      … и, простите, как конкретно это работает, учитывая, что у вас внутри With не анонимная функция? Что со скоупом переменных, временем выполнения и так далее?


      PS


      In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.
        +1

        Работает как обычная функция в которую передаются результаты выполнения выражений. Вообще в ней можно написать "левое" выражение типа "х=100/2". Выглядит все это забавно, но в реальной жизни я бы такое использовать не стал. Пользы нет, так ещё и лишние вызовы.

          0
          Работает как обычная функция в которую передаются результаты выполнения выражений

          Спасибо, кэп. Вопрос как раз в том, как у "обычной функции" будет работать область видимости out var.

            0
            Будет явно объявлена локальная переменная в вызывающем методе. Если до вызова и инициализации переменной дело может не дойти, то при попытке её использования в небезопасном месте компилятор выдаст ошибку.

            Так что в скомпилированном коде переменная будет точно проинииализирована, если не произойдёт исключений.
              0
              Будет явно объявлена локальная переменная в вызывающем методе.

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

                0
                С точки зрения метода 'With' есть контекст, который будет обратно возвращён в цепочку вызовов.

                Да, одинаковые названия переменных будут конфликтовать, но есть возможность их повторного переиспользования.
                  0
                  С точки зрения метода 'With' есть контекст, который будет обратно возвращён в цепочку вызовов.

                  Вот только этот контекст никак не влияет на поведение With и на его аргументы.


                  (полезно сравнить и с With в VB.net, и со стандартной реализацией With как монады x.With(y => y.x))


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

                  Во-первых, только если типы совпадают. Во-вторых, это худший вид побочного эффекта.

                    0
                    Да, контекст не влияет на поведение 'with' и аргументы. Здесь ответственность программиста и полная свобода действий.

                    Лично для меня близка свобода в программировании, делай, как тебе нравится, а если что-то работает не так, то сам виноват. Меньше ограничений — больше возможностей.

                    Монады не позволяют совершать деконструкцию объекта и объявлять новые переменные для дальнейшего использования в вызывающем методе.
                      0
                      Да, контекст не влияет на поведение 'with' и аргументы.

                      … это значит, опять, что контекста нет.

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

            Польза не совсем очевидна, но здорово помогает писать bodied методы в одну цепочку.
              0
              Польза не совсем очевидна, но здорово помогает писать bodied методы в одну цепочку.

              Я вас расстрою, но любой метод — bodied.

                0
                Я не силён в терминологии, но подразумеваю такие методы, которые не содержат скобок и декларируются наподобие лямбда-выражений

                IModel GetModel() => new AnyModel();
          0
          Работает по аналогии с инициализационными блоками
          new Person
          {
              Name = "AnyName"
          }.DoSomethig();

          раскладывается компилятором в
          var tmpContext = new Person();
          tmpContext.Name = "AnyName"
          tmpContext.DoSomething();

          В случае с 'With' мы декларируем контекст явно
          new Person().To(out var p).With
          (
              p.Name = "AnyName"
          ).DoSomething();

          Единственное отличие состоит в дополнительном вызове метода 'With' для которого подготавливаются переменные в стеке. Декомпиляцию можно посмотреть тут.

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

          Для сравнения вVisualBasic есть оператор 'With', а доступ к временному контексту выполняется через '.', что-то пожее на следующий псевдо-код
          new Person().With
          {
              .Name = "AnyName",
              .ToString() to var stringView
          }.DoSomethig();


          В любом случае, дело вкуса. Мне лично 'With' паттерн особенно нравится тем, что очень помогает писать bodied методы.
            +1
            В случае с 'With' мы декларируем контекст явно

            Вот задекларировали вы "контекст" (на самом деле — нет). Внутри него вызвали метод с out var. Какая будет область видимости у созданной переменной?

              0
              Локальная переменная в методе
              void Test()
              {
                  GetPoint().To(out var p).With
                  (
                      p.X.To(out var x),
                      p.Y.To(out var y),
                  ).DoSomething();
                  
                  Console.WriteLine($"{x}, {y}");
              }
                +1

                Вот я и говорю: нет никакого "контекста". Этот With не значит ничего.

                  0
                  Он и не должен что-то значить — это лишь синтаксический сахар для структуризации кода.
                    +2

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

                      0
                      Ваш выбор и ваше дело.

                      Для меня видимость структуризации, читаемость и выразительность — очень близкие в данном случае понятия.

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

                        "Выразительность" — это когда какое-то слово что-то выражает. А у вас есть слово With, которое ничего не выражает. Это отрицательная выразительность, если так можно выразиться.


                        А говорить о математической красоте, когда вы не просто вводите побочные эффекты, а ставите их своей целью, я бы не стал.

                          0
                          Вообще-то в нашем случае 'with' выражает пусть и видимую, но вполне осязаемую структуру кода.

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

                          Теперь у нас есть конструктор или лего, из которых можно собрать множество вариаций автомобилей и не только, даже самых нелепых и абсурдных! Огромный простор для фантазии!

                          В программировании для меня намного более близок второй подход. Пускай лучше инструменты позволяют делать даже бредовые вещи, как их примениять или не применять, это уже мне самому потом решать. :)
                            +1
                            Вообще-то в нашем случае 'with' выражает пусть и видимую, но вполне осязаемую структуру кода.

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


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

                            Этот подход плохо применим в командной работе.

                              0
                              Граница есть, но лишь условная, поэтому вы можете её свободно пересекать в любую сторону — в этом своя прелесть!

                              Этот подход плохо применим в командной работе.

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

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

                                Нет в этом прелести, в том-то и дело. Это банальный обман ожиданий.


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

                                Спасибо, мне достаточно примеров кода, которые вы приводите в дискуссиях.

                                  0
                                  Ну, это только чать айзберга. :)
                                    +1

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

                                      –1
                                      Это вы так считаете, но касательно меня это мало о чём говорит. Да и репозитории тоже публичные, так что я показываю значительно больше, чем вы видете здесь, и лишь ваша принципиальная позиция ограничивает вам самим обзор. )
                                        0

                                        У меня нет задачи или цели расширить свой обзор в вашем отношении, так что I'm totally fine.

                                          –1
                                          Замечательно! Рад за вас! )
                                  –7
                                  Хотите без границ пишите в С :)
                                  C# и .NET это в основном язык для кровавого энтерпрайза и для девелоперов с мат аппаратом ниже среднего.

                                  Выше среднего идут в игроделанье или сток трейдинг какой-нить и фигачут все на С/С++ без границ.
                                    +1
                                    На самом деле C# довольно хороший и продуманный язык. Да и это всего лишь инструмент, а как его использовать, решать самому разработчику. Ведь если язык позволяет вытворять такие вещи, что описаны в публикации, то почему бы и нет? )
                                      0
                                      Имхо такое годно для какого-нить пет-проекта на гитхабе, при этом не уверен что ссылка на такой проект пойдет даже в плюс в резюме. Т.е. чисто для себя или для какого-нить комьюнити любителей такого кода, обычно некоммерческого.

                                      Но всё же если взять среднюю.нет кодбазу, то над ней больше часов проводят девелоперы, которые её не писали с нуля и которые не знают всю окружающую инфраструктуру. Девелоперы в среднем хорошо знают C#, встроенные апи .NET, самые популярные нугет пакеты и мб специализированные фреймворки.
                                      Поддерживать, менять и профилировать такой код сложнее чем прямолинейный нативный C#, если ты работраешь с этим кодом раз в год.
                                      И согласен с lair про скоп out параметров, он сделан довольно коряво из-за наследия C#, также как и is var x и case int x. Поэтому использовать хорошо и без неожиданных сюрпризов это можно в коротких узкоспециализированных методах в которых уже становится не очень важно насколько красиво они написаны.
                                        0
                                        Думаю, каждый разработчик сам сможет определить область применения рассмотренным приёмам программирования. )

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

                                        В любом случае, иногда полезно взглянуть даже на хорошо знакомые вещи с другой стороны. Своего рода разминка для ума. :)
            0
            Бесспорно мощно, Джона Скита на вас нет :)
            А что по поводу производительности — открыл sharplab по вашей ссылки, перешел в IL — там же какой-то локальный ад?
              0

              … ну да, например вот все вызовы статических функций-пустышек не инлайнятся.

                0
                Что вас смущает? ) Переменные в стеке? Это не работа с кучей, здесь ощутимого падения производительности не происходит.
                  0
                  Вызов функции — это не только передача аргументов через стек. Это ещё и «сброс» состояния локальных переменных метода, из которого идёт вызов и их восстановление после возврата. Короче говоря, даже в 21-м веке вызов функции — это не самая дешёвая операция.
                    0
                    Что вы подразумеваете под сбросом состояния? Насколько я понимаю, локальные переменные всегда остаются в стеке (во многом это и вызывает stack overflow при крайне глубоком рекурсивном вызове методов). Зачем их куда-то ещё сбрасывать, чтобы потом восстанавливать?

                    Думаю, что современные языки отлично оптимизированы для работы со стеком вызовов и поощеряют разделение кода на маленькие методы.

                    Моё понимание интуитивно, поэтому могу ошибаться, поправьте, если что не так. :)
                      +1
                      Думаю, что современные языки отлично оптимизированы для работы со стеком вызовов и поощеряют разделение кода на маленькие методы.

                      … в основном они пытаются эти "маленькие методы" инлайнить. Наверное, не просто так. И наверное не просто так на куче таких маленьких методов стоит хинт "инлайнить максимально агрессивно".

                        0
                        Вы пробовали когда-нибудь замерить, скольколько же стоит вызов пустого метода?
                        var w = new Stopwatch();
                        w.Start();
                        for (int i = 0; i < 100000000; i++)
                        {
                        	w.With(w, i);
                        }
                        w.Stop();
                        System.Console.WriteLine(w.ElapsedMilliseconds);

                        На моём не самом передовом компьютере 100 000 000 вызовов заняли около секунды на релизном билде. То есть вызов 'With' занимает 1/100 000 000 (одну стомиллионную секунды)!

                        Не знаю, как вам, но для моих задач этого хватит с лихвой.

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

                        Так что не стоит сгущать краски над 'With', вызов этот занимает порядка одной стомиллионной секунды на среднем компьютере. Уверен, что даже для мобильных устройств эта цифра будет вполне адекватная.
                          +1
                          Вы пробовали когда-нибудь замерить, скольколько же стоит вызов пустого метода?

                          Вы когда-нибудь читали, как правильно делать микробенчмарки?

                            –1
                            Предлагаю вам самим сделать бенчмарк по вашим канонам и сравнить с результатами, полученными мной. Возможно, моя методика не так уж точна, но порядок величин, думаю, вполне ясен. Хотите опровергнуть — за дело!

                            Буду рад распрощаться со своими заблуждениями насчёт вызова пустых методов.
                              +2

                              Да пожалуйста:


                              BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
                              [Host]: .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0
                              DefaultJob: .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2633.0

                                Method |      Mean |     Error |    StdDev |
                              -------- |-----------|-----------|-----------|
                                  With | 27.082 ns | 0.3048 ns | 0.2545 ns |
                               Without |  6.095 ns | 0.2036 ns | 0.1904 ns |

                              Разница в четыре с половиной раза.


                              Код
                              public class Person
                              {
                                  public string Name { get; set; }
                                  public int Age { get; set; }
                              }
                              
                              public class WithAndWithout
                              {
                                  private readonly string _name = Guid.NewGuid().ToString();
                                  private readonly int _age = 42;
                              
                                  [Benchmark]
                                  public object With()
                                  {
                                      return new Person()
                                          .To(out var p)
                                          .With(
                                              p.Name = _name,
                                              p.Age = _age
                                              );
                                  }
                              
                                  [Benchmark]
                                  public object Without()
                                  {
                                      return new Person
                                      {
                                          Name = _name,
                                          Age = _age
                                      };
                                  }
                              }
                              
                              public static class Q
                              {
                                  public static T To<T>(this T o, out T x) => x = o;
                                  public static T With<T>(this T o, params object[] pattern) => o;
                              }
                              
                              public class Program
                              {
                                  static void Main(string[] args)
                                  {
                                      var summary = BenchmarkRunner.Run<WithAndWithout>();
                                  }
                              }
                                0
                                Спасибо! Но когда речь идёт о стомиллионных долях секунды, то разница в пять и даже сто раз просто не ощутима на практике, за исключением очень редких случаев, где важна производительность или очень много данных.

                                Поэтому для себя не вижу существенных причин отказываться от 'With'.

                                  +1
                                  Но когда речь идёт о стомиллионных долях секунды, то разница в пять и даже сто раз просто не ощутима на практике

                                  Это пока вы не создаете объекты сотнями тысяч и миллионов. Пять секунд против секунды — и упс.


                                  Поэтому для себя не вижу существенных причин отказываться от 'With'.

                                  Ну то есть вас перформанс не волнует, на самом деле. Ок.

                                    –1
                                    Знаете, за немало лет коммерческого программирования мне трудно вспомнить даже пару случаев, когда бы я имел дело с сотнями тысяч и уж тем более с миллионами объектов. Разве что в тестовых целях проверял производительность каких-то методов на больших массивах.

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

                                      Сто накладных, тысяча наименований на накладную — вот вам и сто тысяч объектов. Теперь представили, что у вас ORM и внешний DTO — помножили на три. А это так, ленивый день на обувном складе.

                                        0
                                        > А это так, ленивый день на обувном складе.

                                        Там действительно 5 секунд лишних за день не выделить?
                                          0

                                          Злые интеграторы ноют, что у них запросы не прокачиваются (понятно, что не в создании объектов дело, но иногда бывают нелепые достаточно ботлнеки).


                                          Ну и как бы да, пять секунд один раз никого не пугают, но они же накапливаются.

                                          0
                                          Ох, не знаю, какой у вас проект, но обычно никто не держит в памяти по сто тысяч объектов за редкими исключениями. В интерпрайз-решениях, как правило, используют виртуализацию на уровне данных и/или визуального интерфейса.

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

                                          Поэтому не вижу смысла отказываться от паттерна общего назначения, из-за каких-то маловероятных падений производительноти. Если вдруг начнёт что-то ощутимо замедляться из-за вызовов 'With', то не вижу проблемы их убрать, благо, код не потребует огромной реструктуризации.
                                            +2
                                            Ох, не знаю, какой у вас проект, но обычно никто не держит в памяти по сто тысяч объектов за редкими исключениями.

                                            А я и не говорил, что их держат в памяти, их прочитали-трансформировали-записали, но это же все равно столько же созданий объектов.


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

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


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

                                            … а зачем он тогда нужен?

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

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

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


                                          Не в первый раз встречаю это странное утверждение. Если на низкоуровневом языке использовать подобного рода конструкции, то тоже ничего хорошего не выйдет.
                                            0
                                            Смотря для каких задач использовать. )
                                        0
                                        "-Сколько у Вас стоит капля водки?
                                        -Нисколько.
                                        -Отлично! Накапайте стаканчик!"

                                        Весь наш код складывается из «стомилионных долей секунды». Каждый вызов, каждая строчка, каждое выражение. Если вы пишете, например, десктопный UI, то пользователь, скорее всего, не заметит просадки ни в пять ни в сто раз. Но в бэкенде, это окажется критичным. А подход лучше использовать один ко всему коду.
                                          –2
                                          Если перефразировать ваше утверждение в терминах программирования, учитывая порядок величин, то получится что-то вроде
                                          "-Сколько у Вас стоит капля водки?
                                          -Нисколько.
                                          -Отлично! Накапайте цистерну!
                                          -Без проблем! Начинайте капать..."

                                          Да и если здраво подойти к вопросу, то
                                          "-Сколько у Вас стоит капля водки?
                                          -Нисколько.
                                          -Отлично! Накапайте стакан!
                                          -Стакан стоит столько-то центов..."

                                          :)
                                        +1
                                        Ваш код немного отличается от заявленного Makeman.
                                        У вас в хелперах:
                                        public static T With<T>(this T o, params object[] pattern) => o;
                                        

                                        Что приводит к боксингу int'а в
                                        .With(
                                            p.Name = _name,
                                            p.Age = _age
                                            );
                                        

                                        В то время как у Makeman эти хелперы объявлены так, что боксинга не будет:
                                         public static T With<T>(this T o) => o;
                                         public static T With<T, A>(this T o, A a) => o;
                                         public static T With<T, A, B>(this T o, A a, B b) => o;
                                         public static T With<T, A, B, C>(this T o, A a, B b, C c) => o;
                                        

                                        Чтобы не быть голословным, на моей машине бенчмарк вашего кода

                                        BenchmarkDotNet=v0.10.14, OS=Windows 7 SP1 (6.1.7601.0)
                                        Intel Xeon CPU X5670 2.93GHz, 1 CPU, 12 logical and 6 physical cores
                                        Frequency=2864628 Hz, Resolution=349.0855 ns, Timer=TSC
                                        [Host] : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2563.0
                                        DefaultJob : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2563.0

                                        Method | Mean | Error | StdDev |
                                        -------- |----------:|----------:|----------:|
                                        With | 21.618 ns | 0.5293 ns | 0.4692 ns |
                                        Without | 5.212 ns | 0.2985 ns | 0.2931 ns |

                                        Разница действительно больше чем в четыре раза.
                                        Бенчмарка кода c поправленными хелперами:

                                        BenchmarkDotNet=v0.10.14, OS=Windows 7 SP1 (6.1.7601.0)
                                        Intel Xeon CPU X5670 2.93GHz, 1 CPU, 12 logical and 6 physical cores
                                        Frequency=2864628 Hz, Resolution=349.0855 ns, Timer=TSC
                                        [Host] : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2563.0
                                        DefaultJob : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.2563.0

                                        Method | Mean | Error | StdDev |
                                        -------- |---------:|----------:|----------:|
                                        With | 9.931 ns | 0.2884 ns | 0.4042 ns |
                                        Without | 5.341 ns | 0.1552 ns | 0.1376 ns |

                                        Разница уже не в 4, а примерно в два раза. Имхо, все равно достаточно большая.
                                          0
                                          Благодарю за проведённые бенчмарки!

                                          P.S. Я не уверен, но, возможно, если отметить методы аттрибутом [MethodImpl(MethodImplOptions.AggressiveInlining)], то результаты могут ещё немного улучшиться…
                                            0
                                            Не улучшается — проверял перед тем как послал.
                                            0

                                            Осталось это смасштабировать на типичную такую инициализацию с десятью-пятнадцатью параметрами.

                                              0
                                              Я же написал в статье, как легко масштабировать хоть на сотню параметров…
                                              GetModel().To(out var m)
                                              .With(m.A0 = a0, ... , m.AN = aN)
                                              .With(m.B0 = b0, ... , m.BM = bM).Save();
                                                0

                                                Ну во-первых, это становится менее читаемо (интент перестает быть очевиден). А во-вторых, надо же считать перформанс после такого.

                                                  0
                                                  При аккуратном форматировании читаемость не слишком страдает
                                                  new Person().To(out var p).With(
                                                      PropA = aA,
                                                      ...
                                                      PropN = aN).With(
                                                      PropM = aM,
                                                      ...
                                                      PropZ = aZ).DoSomething();

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

                                                  Производительность можете сами проверить, как показывают мои опыты, подобные конструкции очень близки по скорости выполнения к классическим инициализационным блокам.
                                                    0
                                                    При аккуратном форматировании читаемость не слишком страдает

                                                    Ну то есть мы читаем, внезапно видим между двумя присвоениями With и просто игнорируем его, да?

                                                      0
                                                      Да, так и есть.
                                                        +1

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

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

                                                            Вот только беда в том, что никто не знает, сколько перегрузок надо. А метод With, понятное дело, должен лежать не в моем коде, поэтому каждое добавление — это редеплой.

                                                              0
                                                              Не знаю, как у других, но у меня даже 16 параметров уже покроют не меньше 98% случаев инициализаций, а оставшиеся 2% вполне можно выполнить при помощи конкатенации вызовов.
                                                                0

                                                                (У вас, конечно, есть репрезентативная статистика про "16 параметров покроют"?)


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

                                                                  0
                                                                  Ради бога используйте, никого не призываю переходить полностью на With, сам пользуюсь инициализационными блоками, просто есть сценарии, где они уже неприменимы, зато уместен With.
                                                                    0

                                                                    … а теперь давайте подумаем, сколько таких сценариев (в процентах), и задумаемся — а стоит ли этот оверхед того?

                                                                      –1
                                                                      Таких сценариев достаточно.

                                                                      Во-первых, инициализация.

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

                                                                      В-третьих, цепочная замена конструкций if-else при проверках на null.

                                                                      И наконец, вы же не противитесь внедрению 'is {… }'? Хотя Check паттерн (родственный With) гораздо более гибкий и интуитивный.

                                                                      Даже у меня в простеньком текстовом редакторе нашлось несколько мест, где With весьма органично вписался. Поэтому было бы желание — применение найдётся.
                                                                        +1
                                                                        Во-первых, инициализация.

                                                                        Для инициализации есть инициализационные блоки (а еще лучше — конструкторы и разного рода билдеры, потому что immutabity упрощает рассуждение).


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

                                                                        Продемонстрируйте.


                                                                        В-третьих, цепочная замена конструкций if-else при проверках на null.

                                                                        … и как вы для этого используете With?


                                                                        И наконец, вы же не противитесь внедрению 'is {… }'? Хотя Check паттерн (родственный With) гораздо более гибкий и интуитивный.

                                                                        А кто вам сказал, что ваш Check более интуитивный? Особенно опять-таки в части порядка и условности выполнения.


                                                                        Я вам больше того скажу, меня is волнует сильно во вторую очередь. Меня волнует "большой" паттерн-матчинг в виде switch (особенно когда наконец сделают switch expressions), а is — это его побочный эффект, сделанный для симметрии.


                                                                        Поэтому было бы желание — применение найдётся.

                                                                        Это плохой подход. "У меня есть молоток, поэтому я поищу что-нибудь, чтобы забить, пусть даже это будет шуруп". Ровно наоборот, применение должно расти из необходимости.

                                                                          0
                                                                          Продемонстрируйте.

                                                                          GetPerson().To(out var p).With(
                                                                              p.Name.To(out var name),
                                                                              p.Age.To(out var age)
                                                                          )./* use of 'name' and 'age' */

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

                                                                          … и как вы для этого используете With?

                                                                          Вы ведь уже видели.
                                                                          (o, e) =>
                                                                          App.Current.MainWindow.To(out var w)?.With(w.Title = "x");


                                                                          А паттерн-матчинг — фича интересная, но некоторые детали реализации в C# оставляют желать лучшего. Мне вообще думается, что 'is {… }', может, и не выйдет в релиз… Поживём — увидим.
                                                                            +2
                                                                            GetPerson().To(out var p).With(
                                                                            p.Name.To(out var name),
                                                                            p.Age.To(out var age)
                                                                            )

                                                                            Серьезно?


                                                                            var p = GetPerson();
                                                                            var name = p.Name;
                                                                            var age = p.Age;

                                                                            Меньше кода, меньше оверхеда, лучше читается, никакого WTF. Деконструкция здесь просто не нужна.


                                                                            (o, e) =>
                                                                            App.Current.MainWindow.To(out var w)?.With(w.Title = "x");

                                                                            Аналогично:


                                                                            (o, e) =>
                                                                            {
                                                                              if (App.Current.MainWindow is Window w)
                                                                                w.Title = "x";
                                                                            };

                                                                            Явно описанные побочные эффекты — наше все.


                                                                            А паттерн-матчинг — фича интересная, но некоторые детали реализации в C# оставляют желать лучшего.

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

                                                                              –1
                                                                              Меньше кода, меньше оверхеда, лучше читается, никакого WTF. Деконструкция здесь просто не нужна.
                                                                              Вы, похоже, не уловили сути. Деконструкция и инициализация с With позволяют выводить, объявлять и обновлять значения где угодно в цепочных вызовах без кардинальной смены структуры кода.
                                                                              string _newName = "abc";
                                                                              
                                                                              bool UpdateData(Person p) => p.With(
                                                                                  p.Name.To(out var oldName),
                                                                                  p.Name = _newName)
                                                                              )
                                                                              .Use(() => Debug.WriteLine($"Name changed  from {oldName} to {_newName}"))
                                                                              .TrySave();

                                                                              Например, тут присутствует временный дебажный вывод, который может быть легко удалён без смены структуры кода.
                                                                              string _newName = "abc";
                                                                              
                                                                              bool UpdateData(Person p) => p.With(
                                                                                  p.Name = _newName)
                                                                              )
                                                                              .TrySave();


                                                                              (o, e) =>
                                                                              {
                                                                              if (App.Current.MainWindow is Window w)
                                                                              w.Title = «x»;
                                                                              };

                                                                              With для того мне и нужен, чтобы избежать скобок и конструкций вида if-else, вы же предлагаете мне вернуться к старому варианту, который всегда был мне не по душе. И да, я хочу использовать вывод типов без всяких хаков, а не указывать их явно как Window w.
                                                                              Это смотря с чем сравнивать. Ну да, F# мне нравится больше. Но F# имеет свои занятные особенности (и с вашей манерой писать совместим, кстати, исключительно плохо).

                                                                              Активно не использую F#, но реализация паттерн-матчинга там чище, чем в C#. Хотя и он меня вполне устраивает, особенно с кастомными To-With-Is-Check.
                                                                                0
                                                                                Вы, похоже, не уловили сути. Деконструкция и инициализация с With позволяют выводить, объявлять и обновлять значения где угодно в цепочных вызовах

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


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

                                                                                Я не вижу проблемы поменять структуру кода здесь — это секунд 15 в нормальной IDE.


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

                                                                                Если вам не по душе императивное программирование, зачем вы издеваетесь над императивным языком?

                                                                                  0
                                                                                  Если вам не по душе императивное программирование, зачем вы издеваетесь над императивным языком?

                                                                                  Только лишь приспасабливаю язык под свои индивидуальные нужды, а заодно исследую нестандартные сценарии использования базовых конструкций.
                                0
                                Что мешает сделать
                                [MethodImpl(AgressiveInlining)]
                                ?
                            +7
                            var ((x, y, z), t, n) = (1, 5, "xyz");

                            и


                            if (GetPerson() is Person p && p.Check
                                (
                                    ...
                                    p.City.To(out var city).Put(true), /* always true */
                                    p.Age.To(out var age) > 23
                                ).All(true)) ...

                            Очень сложно увязать с

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

                              var (x, y, z) = 1;
                              +2
                              Почему бы для случая To-with не использовать банально:

                              public static T With<T>(this T obj, Action<T> initializer) {
                                  initializer(obj);
                                  return obj;
                              }
                              ?
                              То есть:
                              var manager = GetPerson().With(o => {
                                  o.FirstName = "Иван";
                                  o.LastName = "Пупкин";
                              });
                              

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

                              Так и до NLombok недалеко...
                                0
                                У такого подхода есть ряд недостатков:

                                — нельзя без замыканий выводить значения в новые переменные (производить деконструкцию объекта)
                                var manager = GetPerson().To(out var p).With(
                                    p.FirstName.To(out var oldFirstName),
                                    p.FirstName = "Иван",
                                    p.LastName = "Пупкин",
                                    p.ToString().To(out var personStringView),
                                });


                                — нет возможности удобно модифицировать структуры
                                struct Person { ... }
                                
                                var manager = GetPerson().With(o => {
                                    o.FirstName = "Иван";
                                    o.LastName = "Пупкин";
                                });
                                
                                Console.WriteLine(manager.FirstName); // получим исходное значение вместо "Иван" 


                                — больше скобок и меньше похоже на инициализационные блоки

                                Хотя, конечно, никто не отменяет и этот подход, кому что ближе. :)
                                  0
                                  Всего-то нужна перегрузка которая принимает ref.
                                    +1
                                        p.FirstName.To(out var oldFirstName),
                                        p.FirstName = "Иван",
                                    


                                    А как вы решаете где применять левое присваивание, а где правое? Не от наличия With, надеюсь?
                                      0
                                      Хороший вопрос.

                                      Правое присваивание очень уместно в таких случаях:
                                      — expression-bodied методах
                                      — при деконструкции объектов
                                      — в цепочных вызовах

                                      Левое присваивание на данный момент больше подходит для:
                                      — арифметических выражений
                                      — при присваивании свойств (к ним нельзя применить правое присваивание в текущей реализации, хотя если бы существовал на уровне языка оператор 'to', то можно было бы применять и для них)

                                      В остальных случаях ориентируюсь по контексту, где что лучше смотреться будет.
                                        0
                                        — expression-bodied методах

                                        А зачем? Вас кто-то заставляет expression-bodied использовать?
                                        Или вы на столько не любите фигурные скобочки, что готовы внедрить лишний оператор?

                                        — в цепочных вызовах

                                        тогда надо сразу делать .Then(expr) вместо; и .Return(expr) вместо return — можно что угодно в expression-bodied и цепочку запихнуть.
                                          0
                                          Скажу так… насчёт expression-bodied стиля:

                                          • провоцирует оформлять код мелкими методами с раздельной отвественностью
                                          • меньше скобок и лишних слов вроде return
                                          • эстетически красиво и лаконично
                                          • развивает чувство прекрасного
                                          • учит писать чистый и общённый код

                                          И лично для меня последние пункты самые важные. :)
                                          Если бы не эта математическая красота, то давно бы уже забросил программирование!
                                            +2
                                            • эстетически красиво и лаконично
                                            • развивает чувство прекрасного
                                            Или писать адовые однострочники в стиле: «Смотри, как я могу!» ?) Всё в меру хорошо, но к красивому коду это склоняет настолько же, насколько и к говнокоду.
                                              –1
                                              Скорее: «Смотри, ты тоже так можешь!»

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

                                              Но я думаю, что есть и такие люди, которым интересен новаторский взгляд в программировании, а эта статья поможет им взглянуть на знакомые вещи с другого ракурса…
                                                +1
                                                Я не против хакерства как такового, лишь критикую некоторые ваши аргументы, которые подаются как абсолютная истина) Не прививают синтаксические конструкции чувства прекрасного так, как это делает, скажем, юнит-тестирование (да и с этим можно спорить) или специальные статические проверки с ошибками и ворнингами.
                                                  0
                                                  Как говорится: «Любое категоричное суждение ошибочно, даже это». Поэтому не стоит воспринимать мои слова как абсолютную истину :)

                                                  Здорово, что вы критикуете и обдумываете аргументы, не принимая их сразу на веру, ведь я тоже могу ошибаться, заблуждаться и быть слишком субъективным.
                                              +2
                                              > насчёт expression-bodied стиля:

                                              Вы про «expression-bodied» или про «expression-bodied с правым присваиванием и другим хламом»? С первым я может и согласился бы…

                                              Но я плохо понимаю, например, как у вас «провоцирует оформлять код мелкими методами с раздельной отвественностью» привело к совмещению деконструкции, update, и генерации view в одном выражении.
                                                0
                                                Если для вас это выглядит «хламом», то не пользуйтесь. )

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

                                                По ссылке можно увидеть пример стиля, в котором я предпочитаю писать код.
                                                  +1
                                                  По ссылке можно увидеть пример стиля, в котором я предпочитаю писать код.

                                                  Дадада.


                                                  Documents.ForEach(d => d.Expose()).ForEach(async d => await d.Load());

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


                                                  Одна строчка, одна.

                                                    –2
                                                    Всё просто — в первом цикле инициализируем вью-модели документов, а во втором асинхронно загружаем в каждую информацию из файлов.

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

                                                    Можете даже скомпилировать проект и убедиться, что всё работает вполне себе живо. :)
                                                      0
                                                      в первом цикле инициализируем вью-модели документов
                                                      Ну то есть ForEach(d => d.Expose()) меняет состояние объектов в коллекции.

                                                      а во втором асинхронно загружаем в каждую информацию из файлов.

                                                      Асинхронно с ожиданием или без? Параллельно или последовательно?

                                                        0
                                                        Без ожидания параллельно.
                                                          +3

                                                          Без ожидания. Серьезно.


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


                                                          Круто, да.

                                                            –1
                                                            Ошибки загрузки из файла обрабатываются в самих дочерних вью-моделях.

                                                            На момет окончания метода 'CoreViewModel.Expose' важно выполнить лишь Expose у коллекции документов, а загрузкой данных из файла заведует сам документ.
                                                              0
                                                              Ошибки загрузки из файла обрабатываются в самих дочерних вью-моделях.

                                                              Это тоже очень очевидно в вашем коде. Особенно учитывая, что ADocument — абстрактный класс, и там может быть что угодно в коде.


                                                              а загрузкой данных из файла заведует сам документ.

                                                              Тогда почему этот код вызывается из обсуждаемого метода, а не из самого документа?

                                                                0
                                                                Все документы реализуют следующий интерфейс (ADocument) доступный для использования в CoreViewModel
                                                                public abstract Task<bool> Load();
                                                                public abstract Task<bool> Save();
                                                                public abstract Task<bool> SaveAs();
                                                                public abstract Task<bool> Close();

                                                                Подразумевается, что в случае успешного выполнения возвращается true, иначе false.

                                                                Когда основная вью-модель вызывает у дочерней один из этих методов, то это выглядит, словно руководитель выдаёт подчинённому команду что-то сделать, ставит задачу.

                                                                Это не забота руководителя, каким образом подчинённый будет выполнять задание и обрабатывать возникающие трудности (исключения), важен лишь конечный результат и то, что руководитель выполнил свою работу по постановке задачи. Классическое разделение ответственности.
                                                                  +2
                                                                  Подразумевается, что в случае успешного выполнения возвращается true, иначе false.

                                                                  Исключения? Нет, не слышали.


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


                                                                  Когда основная вью-модель вызывает у дочерней один из этих методов, то это выглядит, словно руководитель выдаёт подчинённому команду что-то сделать, ставит задачу.

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


                                                                  Классическое разделение ответственности.

                                                                  В "классическом разделении ответственности", если сказано "важно выполнить лишь Expose у коллекции документов, а загрузкой данных из файла заведует сам документ", загрузка вызывается "самим документом". А если внешний код вызывает и Expose, и Load, значит, ему важны оба.


                                                                  Я не против разделения ответственностей, я против неконсистентности и игнорирования ошибок.

                                                                    0
                                                                    Ошибки (исключения) не игнорируются, вы можете в этом убедиться сами, запустив приложение.

                                                                    В первую очередь я отталкиваюсь от логики самой программы, сейчас она функционирует отлично.

                                                                    Главной вью-модели вообще ни к чему знать об исключениях, возникающих в работе докуметов, там произойти может, что угодно: нет доступа к файлу, формат не тот… Пускай документы сами разбираются, что с этим делать. Задача руководителя лишь создавать их и закрывать, когда нужно, попутно уведомляя о загрузке, закрытии и сохранении.
                                                                      0
                                                                      Ошибки (исключения) не игнорируются, вы можете в этом убедиться сами, запустив приложение.

                                                                      Documents.ForEach(d => d.Expose()).ForEach(async d => await d.Load());

                                                                      Хорошо видно, что результат выполнения игнорируется. При этом, кстати, совершенно не понятно, зачем вам там async-await, хотя без него можно (дважды) обойтись.


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


                                                                      В первую очередь я отталкиваюсь от логики самой программы, сейчас она функционирует отлично.

                                                                      Ну вот видите: от логики, а не от читаемости. Об этом и речь.

                                                                        –1
                                                                        Хотите меня в чём-то переубедить, напишите-ка простенький текстовый редактор с аналогичным функционалом. И не забудьте про полное сохранение логического и визуального состояния при перезапуске приложения…

                                                                        А потом сравните количество и читаемость получившегося кода. Получится лучше — поделитесь с сообществом своими наработками и видением. )
                                                                          0

                                                                          "Сперва добейся"? Спасибо, но нет.

                                                                            0
                                                                            Не знаю, что вы подразумеваете под «добейся».

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

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

                                                                                0
                                                                                Для меня эта строчка выглядит вполне ясно и естественно.

                                                                                1. Подготовили докуметы к работе
                                                                                2. Асинхронно вызвали параллельную загрузку данных в каждый

                                                                                Всё. Документы уже готовы к работе и сами разберутся, что делать при возникновении ошибок.
                                                                                  0

                                                                                  Во-первых, из строчки не очевидно, что загрузка параллельна.
                                                                                  Во-вторых, из строчки не очевидно, что будет с результатом загрузки.
                                                                                  И в-третьих, зачем там async...await?


                                                                                  Для меня эта строчка выглядит вполне ясно и естественно.

                                                                                  Потому что вы знаете, что внутри каждого из методов, который в ней вызывается.

                                                                                    0
                                                                                    Когда вы впервые видите какой-то метод, то зачастую не знаете деталей его имплементации — это нормально, вы просто смотрите код или читаете описание в документации.

                                                                                    public static IList<T> ForEach<T, TR>(
                                                                                    	this IList<T> collection,
                                                                                    	Func<T, TR> action)
                                                                                    {
                                                                                    	foreach (var item in collection)
                                                                                    		action(item);
                                                                                    	return collection;
                                                                                    }


                                                                                    После этого многие вопросы пропадают, и вас уже не смущает этот же вызов в другом месте программы.
                                                                                      +1

                                                                                      Нет, это не нормально. Я не хочу лазить за деталями имплементации, я хочу, чтобы происходящее было понятно из написанного кода. Это и называется "читаемостью".


                                                                                      Заметим, кстати, что даже из приведенного вами кода не очевидно, что загрузка параллельна (и, собственно, она совершенно не обязательно будет параллельной), и все так же не понятно, зачем нужен async...await.

                                                                                        0
                                                                                        Пропустил вопрос.

                                                                                        Ваши претензии насчёт «читаемости» кода лучше адресовать к архитекторам C#, которые ввели именно такую реализацию async...await с немалым количеством подводных камней.

                                                                                        Что касается написанного именно мной кода, то он весьма тривиален — это всего лишь цепочные вариации метода ForEach очень схожие с одноименным методом у класса List. То есть запросто без всяких дополнительных расширений сейчас можно писать такой код
                                                                                        new List<ADocument>() {...}
                                                                                            .ForEach(async d => await d.DoSomething());

                                                                                        Насколько понимаю, вы хотите сказать, что интуитивное добавление async...await ломает его читабельность?

                                                                                        Да, я ошибся с тем, что он должен выполняться параллельно, но при искуственном добавлении задержки в метод Load ничего в моей программе не сломалось и не заблокировалось, просто текст из файла загрузился чуть позже, из чего делаю вывод, что интуиция меня не подвела и работает код, по крайней мере, асинхронно, как и хотелось.
                                                                                          +1
                                                                                          Ваши претензии насчёт «читаемости» кода лучше адресовать к архитекторам C#, которые ввели именно такую реализацию async...await с немалым количеством подводных камней.

                                                                                          Эээ, а при чем тут это, учитывая, что в вашем коде async...await не нужен?


                                                                                          Насколько понимаю, вы хотите сказать, что интуитивное добавление async...await ломает его читабельность?

                                                                                          Ну да, потому что нет ничего интуитивного в асинхронии в цикле, а особенно — в итераторе. И тем более нет ничего интуитивного в запихивании асинхронного метода внутрь метода, выглядящего синхронным (если, конечно, он не называется Run... или Queue...).

                                                                                            0
                                                                                            И на что вы мне предлагаете его заменить? Особенно в случае с Close, где мне важен результат выполнения…

                                                                                            Тасками я пользуюсь по большей части интуитивно и стараюсь избегать дебрей с контекстами синхронизации.
                                                                                              +1
                                                                                              И на что вы мне предлагаете его заменить?

                                                                                              Вы не поверите, просто убрать. Вы серьезно мне хотите сказать, что вы не знаете, как поведет себя система, если вместо async () => await SomeAsync() написать () => SomeAsync()?


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

                                                                                              Не надо так делать, надо документацию читать. Или вот того же Клири очень полезно.

                                                                                                –1
                                                                                                Насколько я понимаю, вызов async () => await SomeAsync() запускает таск, а вот () => SomeAsync() просто его возвращает, не запуская.

                                                                                                Task task
                                                                                                ...
                                                                                                () => task = SomeAsync()


                                                                                                И как вообще я могу убрать await в таком случае?
                                                                                                ...ForEach(async d => await d.Close() && Documents.Remove(d))
                                                                                                  +2
                                                                                                  Насколько я понимаю, вызов async () => await SomeAsync() запускает таск, а вот () => SomeAsync() просто его возвращает, не запуская.

                                                                                                  Вы понимаете неправильно. В обоих случаях "запуск" таска зависит исключительно от поведения SomeAsync (хотя на самом деле, нет такой вещи как "запуск" таска, и это вообще некорректная постановка вопроса).


                                                                                                  И как вообще я могу убрать await в таком случае?

                                                                                                  В таком — не можете, но в приведенном мной примере написано не так (другое дело, что в новом примере — новые проблемы, но в эту кроличью нору можно бесконечно спускаться).

                                                                                                    –2
                                                                                                    В моём случае с Load я не могу убрать await, поскольку метод возвращает таск, который мне нужно запустить. Если бы это был асинхронный воид метод, то тогда да, можно было бы написать так

                                                                                                    async void Load() => await ...
                                                                                                    ...ForEach(d => d.Load());
                                                                                                      +2
                                                                                                      В моём случае с Load я не могу убрать await, поскольку метод возвращает таск, который мне нужно запустить.

                                                                                                      Что значит "запустить таск"? Нет такой вещи в TPL.


                                                                                                      Ваш Load в его живой имплементации рано или поздно долетает до банального Task.Factory.StartNew, который, собственно, и кладет задачу в диспетчер, безотносительно того, делали вы await или нет (а вы его сделали сразу, кстати, и тоже совершенно незачем).


                                                                                                      (отдельно, конечно, прекрасно то, что вы кладете IO-bound-задачу в отдельную задачу вместо того, чтобы взять IOCP-bound ReadToEndAsync)


                                                                                                      Интуиция такая интуиция, да.

                                                                                                        0
                                                                                                        Что ж, теперь я понял, о чём вы. Спасибо, что уделили немного времени на ревью.

                                                                                                        Насколько понимаю, если вместо StartNew буду использовать ReadToEndAsync, то тогда await убрать не смогу, верно? Или интуиция опять подводит?
                                                                                                          0

                                                                                                          Опять подводит.

                                                                                                            0
                                                                                                            Хорошо, два случая
                                                                                                            var tasks = docs.Select(d => d.AnyTask()).ToArray();
                                                                                                            docs.ForEach(async d => await d.AnyTask());


                                                                                                            В первом случае хочу просто собрать таски и выполнить их потом. Во втором хочу начать выполнять немедленно. Как мне различить эти ситуации? В первом случае таски начнут выполняться сейчас же?

                                                                                                              0

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

                                                                                                          +1
                                                                                                          Что значит "запустить таск"? Нет такой вещи в TPL.

                                                                                                          Ну вообще есть, но ей никто не пользуется


                                                                                                          По умолчанию таски в TPL горячие.


                                                                                                          фиксанул ссылку

                                                                                                            +1

                                                                                                            Спасибо за напоминание, я уже и забыл, что так бывает. Был не прав.


                                                                                                            Другое дело, что это очень и очень редкий случай, и — как уже писали ниже — конвенция предполагает, что Task, возвращенный из метода, соответствует запущенному коду, а не коду, который ожидает, что его запустят (еще и потому, кстати, что Task.Start не идемпотентен).

                                                                                                              0
                                                                                                              По-видимому, вы оказались правы в том, что await ничего не запускает, даже холодный таск. Такой код у меня вывел только Start. Хотя, может, я что-то и упустил. )

                                                                                                              static async Task LoadAsync() => await new Task(() => System.Console.WriteLine("Load Async"));
                                                                                                              		
                                                                                                              static Task Load() => new Task(() => System.Console.WriteLine("Load"));
                                                                                                              
                                                                                                              static async void Test()
                                                                                                              {
                                                                                                              	System.Console.WriteLine("Start");
                                                                                                              	await LoadAsync();
                                                                                                              	await Load();
                                                                                                              	System.Console.WriteLine("Finish");
                                                                                                              }
                                                                                                              
                                                                                                              public static void Main()
                                                                                                              {
                                                                                                              	Test();
                                                                                                              	var i = 0;
                                                                                                              	while (true)
                                                                                                              	{
                                                                                                              		i++;
                                                                                                              	}
                                                                                                              }
                                                                                                              
                                                                                                                0
                                                                                                                По-видимому, вы оказались правы в том, что await ничего не запускает, даже холодный таск.

                                                                                                                Кэп.

                                                                                                        0
                                                                                                        В таком — не можете

                                                                                                        На самом деле, конечно, можете, для этого нужен простой набор комбинаторов поверх ContinueWith

                                                                                                          0
                                                                                                          То есть ReadToEndAsync тоже сам начинает выполнять таск, как у меня при StartNew, не дожидаясь явного await?
                                                                                                            0

                                                                                                            ReadToEndAsync начинает чтение из подлежащего ридера не дожидаясь никакого await.

                                                                                                              –2
                                                                                                              Никак вам не различить эти ситуации. То, когда начнется выполнение, зависит от того, что внутри AnyTask, а не снаружи.
                                                                                                              Если правильно понимаю вас, то при наличии интерфейса, как у меня, и абстрагируясь от конкретной имплементации документа, мне нужно явно указывать await у Load, чтобы гарантированно выполнить таск, верно? Или чего-то ещё не понимаю?
                                                                                                                +1
                                                                                                                Вам уже три раза написали: в C# await никак не влияет на то будет ли таск запущен (в отличие от Python и С++).

                                                                                                                А если он не влияет — то и гарантировать ничего не может.
                                                                                                                  0
                                                                                                                  Тогда вопрос, как мне быть уверенным, что таск у меня вообще начнёт выполняться, а не просто вернётся в вызывающий метод?
                                                                                                                    0
                                                                                                                    Если код свой — то просто не писать глупого кода.

                                                                                                                    Если код чужой — смотреть в документацию.
                                                                                                                      0
                                                                                                                      А что в документации? Если её нет, а просто интерфейс с таском?
                                                                                                                        0
                                                                                                                        По умолчанию принято считать что любой таск когда-нибудь выполнится если только обратное не написано в документации.

                                                                                                                        Если таск вернули но он никогда не выполнился — ну что поделать, баг однако. Иногда баги случаются.
                                                                                                                          0
                                                                                                                          Благодарю за ответы! Узнал для себя что-то новое.

                                                                                                                          Последнее уточнение, для случая
                                                                                                                          ...ForEach(async d => await d.Load())
                                                                                                                          компилятор сгененирует ощутимо менее оптимальный код, чем для
                                                                                                                          ...ForEach(d => d.Load()), поэтому второй вариант предпочтительнее? Или дело в другом?
                                                                                                                            0
                                                                                                                            Именно так и есть. Не то чтобы «ощутимо» менее оптимальный — но все-таки второй вариант содержит на 1 класс, 2 Interlocked-операции и несколько кадров стека меньше. Но поскольку для написания более оптимального кода нужно не добавлять что-то в код, а наоборот — стереть два слова — этого достаточно чтобы бесить перфекционистов вроде меня :-)
                                                                                                                              +1

                                                                                                                              Там еще может случиться захват и возврат на контекст синхронизации, а вот это уже больно.

                                                                                                                                0
                                                                                                                                Просто дело вот в чём, когда я вижу в коде конструкцию (например, на гитхабе)
                                                                                                                                ...ForEach(async d => await d.Load())

                                                                                                                                мне срузу становится ясно, что загрузка идёт асинхронная, а вот
                                                                                                                                ...ForEach(d => d.Load())

                                                                                                                                ни о чём не говорит, нужно лезть в имплементацию или заранее именовать методы, как LoadAsync (при условии, что это мой код, а не чужой).

                                                                                                                                Из этих соображений читаемости для меня сейчас всё равно предпочтительным остаётся первый вариант…
                                                                                                                                  0
                                                                                                                                  Просто дело вот в чём, когда я вижу в коде конструкцию (например, на гитхабе) ForEach(async d => await d.Load()) мне срузу становится ясно, что загрузка идёт асинхронная, а вот

                                                                                                                                  Вот только она не обязательно асинхронная.


                                                                                                                                  а вот ForEach(d => d.Load()) ни о чём не говорит

                                                                                                                                  Именно поэтому асинхронные методы следует именовать Async.


                                                                                                                                  Из этих соображений читаемости для меня сейчас всё равно предпочтительным остаётся первый вариант

                                                                                                                                  Я не удивлен.

                                                                                                                                    0
                                                                                                                                    Ну, не все же так хорошо понимают работу тасков, как вы, например. Как-никак увидев async...await человек понимает, что с тасками идёт работа.

                                                                                                                                    В случае же LoadAsync, можно подумать, что я вообще по старинке вручную создаю поток без всяких тасков.
                                                                                                                                      0
                                                                                                                                      Как-никак увидев async...await человек понимает, что с тасками идёт работа.

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


                                                                                                                                      В случае же LoadAsync, можно подумать, что я вообще по старинке вручную создаю поток без всяких тасков.

                                                                                                                                      А какая разница, если наблюдаемое поведение неотличимо?

                                                                                                                                        0
                                                                                                                                        Если учесть правило
                                                                                                                                        По умолчанию принято считать что любой таск когда-нибудь выполнится если только обратное не написано в документации.

                                                                                                                                        то для меня вдвойне очевидно, что загрузка выполнится асинхронно в случае работы с тасками, потому что никаких WaitAll я не делаю. А поскольку и в других местах используются подобные конструкции, например, с Close (где её убрать нельзя), то для общности мне хочется оставить её и с Load, путь даже это чуть менее оптимально с точки зрения компиляции.

                                                                                                                                        Конечно, если вы видите более серьёзные потенциальные проблемы в виде блокировок, например, то поделитесь…
                                                                                                                                          0
                                                                                                                                          то для меня вдвойне очевидно, что загрузка выполнится асинхронно в случае работы с тасками

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

                                                                                                                                            0
                                                                                                                                            Конечно, мне интересно узнать, какие ещё могут варианты произойти.
                                                                                                                                              +2
                                                                                                                                              static Task ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                                                              {
                                                                                                                                                return Task.WhenAll(source.Select(f));
                                                                                                                                              }
                                                                                                                                              
                                                                                                                                              static async Task ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                                                              {
                                                                                                                                                foreach(var s in source)
                                                                                                                                                  await f(s);
                                                                                                                                              }
                                                                                                                                              
                                                                                                                                              static void ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                                                              {
                                                                                                                                                Task.WhenAll(source.Select(f)).Wait();
                                                                                                                                              }
                                                                                                                                              
                                                                                                                                              static void ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                                                              {
                                                                                                                                                foreach(var s in source)
                                                                                                                                                  f(s).Wait();
                                                                                                                                              }
                                                                                                                                              
                                                                                                                                                +2

                                                                                                                                                … а, и это еще не считая пофигистичного варианта:


                                                                                                                                                static void ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                                                                {
                                                                                                                                                  foreach(var s in source)
                                                                                                                                                    f(s);
                                                                                                                                                }
                                                                                                                                                  0
                                                                                                                                                  Понял вас. На самом деле, идея расширения более простая. Это, во-первых, замена обычно цикла foreach для коллекций на метод, как у List'а, а, во-вторых, возможность вернуть коллекцию дальше в цепочку вызовов.

                                                                                                                                                  Конкретно таски она затрагивает лишь косвенно. С таким же успехом я мог бы использовать стандартный метод класса List и писать в нём async...await.

                                                                                                                                                  Я учёл ваши замечания насчёт Load и переписал через WhenAll. Теперь всё работает параллельно и асинхронно (при добавлении рандомных задержек в метод, прогресс-бары скрываются в разные моменты времени для каждого документа). Одобряете такой код?
                                                                                                                                                    +1
                                                                                                                                                    С таким же успехом я мог бы использовать стандартный метод класса List и писать в нём async...await.

                                                                                                                                                    … и они бы там игнорировались.


                                                                                                                                                    Одобряете такой код?

                                                                                                                                                    С async void методом-то? Спасибо, нет.

                                                                                                                                                      0
                                                                                                                                                      В каком смысле игнорировались?

                                                                                                                                                      Кстати, на практике асинхронно и параллельно у меня работают и все вариации с ForEach (c async и без, у List и как своё расширение).
                                                                                                                                                        0
                                                                                                                                                        В каком смысле игнорировались?

                                                                                                                                                        На самом деле, я не прав, и у вас бы просто не скомпилировалось — насколько я помню, Func<T,Task> не приводим к Action<T>.


                                                                                                                                                        Кстати, на практике асинхронно и параллельно у меня работают и все вариации с ForEach

                                                                                                                                                        Это ровно до тех пор, пока у вас вызываемые внутри методы ведут себя определенным образом.

                                                                                                                                                          0
                                                                                                                                                          Чтобы уж наверняка, я переименовал свои методы как Foreach1, после чего следующие вариации скомпилировались и заработали асинхронно и параллельно

                                                                                                                                                          Documents.ForEach1(d => d.Expose())
                                                                                                                                                          .ToList().ForEach(async d => await d.Load());

                                                                                                                                                          Documents.ForEach1(d => d.Expose())
                                                                                                                                                          .ToList().ForEach(d => d.Load());


                                                                                                                                                          Асинхронность отслеживаю визуально по состоянию прогресс-баров. Задержки добавляю случайно в классе PlainTextDocument

                                                                                                                                                          private static Random rand = new Random();
                                                                                                                                                          private async Task<bool> AsyncLoad()
                                                                                                                                                          {
                                                                                                                                                          	await Task.Delay(3000 + rand.Next()%10000);
                                                                                                                                                          	return (Model = _originalModel = (await GetKey()).Is(out var key)
                                                                                                                                                          		? await Wrap.Storage.LoadText(key, EncodingModel.Encoding)
                                                                                                                                                          		: null).Put(key.Is());
                                                                                                                                                          }


                                                                                                                                                          Можете сами перепроверить, если мои результаты не внушают доверия. Для этого скомпилируйте и запустите приложение, создайте пять-десять документов, закройте через команду «Выход», а затем переоткройте и понаблюдайте за прогресс-барами, которые отображают процесс загрузки на вкладке каждого документа.
                                                                                                                                                            –1
                                                                                                                                                            await Task.Delay(3000 + rand.Next()%10000);

                                                                                                                                                            Муа-ха-ха. Замените это же на Thread.Sleep.

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

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

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

                                                                                                                                                                  Вы делайте, как хотите, а я лучше сразу обнаружу проблему и исправлю.
                                                                                                                                                                    0
                                                                                                                                                                    А вы мне предлагаете как ни в чём ни бывало игнорировать зависший или по ошибке медленно работающий таск

                                                                                                                                                                    Эээ… нет, не предлагаю.


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

                                                                                                                                                                    Это говорит человек, который как не проверял статус тасков, так и не проверяет?

                                                                                                                                                                      –1
                                                                                                                                                                      Если кто-то по ошибке добавит мне в асинхронный метод Task Load() блокирующий вызов Thread.Sleep(2000) или вызов с долгими вычислениями, например, то статус таска мне мало о чём скажет.

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

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

                                                                                                                                                                      Резюмируя всё вышесказанное, я действительно заблуждался в том, что await гарантирует запуск даже холодного таска и что его нельзя убрать в критикуемом случае с Load. В остальных моментах моё интуитивное понимание тасков позволило мне написать вполне рабочий код, пусть не самый оптимальный, но и не такой уж медленный.
                                                                                                                                                                        0
                                                                                                                                                                        При вашем подходе

                                                                                                                                                                        Это при каком конкретно?


                                                                                                                                                                        При моём подходе сразу начнёт тормозить UI

                                                                                                                                                                        Это при каком конкретно? А то вы их меняете на ходу.

                                                                                                                                                                          0
                                                                                                                                                                          Ваш подход с WhenAll, мой с ForEach.

                                                                                                                                                                          Хотя, проверил, и с WhenAll интерфейс тоже блокируется при Thread.Sleep, просто я не был уверен в имплементации этого метода, думал, что он обернёт всё в новый таск и Thread.Sleep не будет заметен.
                                                                                                                                                                            0

                                                                                                                                                                            Ошиблись оба раза, ни подход с WhenAll, ни подход с вызовом без ожидания не гарантируют ни параллельного выполнения, ни блокировки при задаче, в которой есть неэффективный код — для нарушения первого достаточно поставить неэффективный код до возвращения Task из задачи, для нарушения второго — после.

                                                                                                                                                                              0
                                                                                                                                                                              Чем больше информации узнаю из дискуссии про таски, тем больше поражаюсь их ненадёжности — никаких гарантий! )

                                                                                                                                                                              Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации… или, если код уже работает как надо, всё же решать проблемы с тасками по мере их поступления, а не заботиться о гиптетических случаях со сменами контекста или внезапными Thread.Sleep…
                                                                                                                                                                                +1
                                                                                                                                                                                Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации…

                                                                                                                                                                                Лучше вряд ли получится, но в .Net есть другие либы для работы с асинхронностью, где всё чуть более детерминированно — F# Async (и к нему в нагрузку AsyncSeq), Hopac (и туда же Hopac.Streams)


                                                                                                                                                                                Ещё как вариант Rx.Net, для UI норм


                                                                                                                                                                                И упомяну Akka.Net (и Akka.Streams) — универсальный солдат, работает на тасках, шедулеры там вроде свои написаны, но модель акторов абстрагирует от большей части дичи связанной с TPL

                                                                                                                                                                                  0
                                                                                                                                                                                  Спасибо! Возьму на замету эти решения. Если вдруг с тасками не управлюсь, то обращусь к альтернативам. )
                                                                                                                                                                                    +1

                                                                                                                                                                                    В акке своей дичи хватает, особенно поначалу.

                                                                                                                                                                                    0
                                                                                                                                                                                    Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации…

                                                                                                                                                                                    На самом деле, если нужна гарантия выполнения в другом потоке — достаточно вызвать Task.Run(...)


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

                                                                                                                                                                                      +1
                                                                                                                                                                                      Чем больше информации узнаю из дискуссии про таски, тем больше поражаюсь их ненадёжности — никаких гарантий!

                                                                                                                                                                                      Они не более ненадежны, чем тот код, который их возвращает. Сами-то таски весьма просты.


                                                                                                                                                                                      Уже думаю, может, лучше вернуться к старому доброму ручному созданию потоков с примитивами синхронизации…

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


                                                                                                                                                                                      гиптетических случаях со сменами контекста

                                                                                                                                                                                      Знаете ли, если у вас в UI-приложении смена контекста — это гипотетический случай, то я даже и не знаю, что сказать. Я-то думал, там это происходит регулярно.


                                                                                                                                                                                      внезапными Thread.Sleep

                                                                                                                                                                                      … и только что вы писали "я лучше сразу обнаружу проблему и исправлю". Вы уж определитесь.

                                                                                                                                                                                        0
                                                                                                                                                                                        … и только что вы писали «я лучше сразу обнаружу проблему и исправлю». Вы уж определитесь.

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

                                                                                                                                                                                        Проще говоря, критикуемый
                                                                                                                                                                                        .ForEach(async d => await Load())
                                                                                                                                                                                        вполне работоспособное и безопасное решение, содержащее не больше подводных камней, чем сама реализация тасков. Если для кого-то такое решение выглядит неочевидным, то можно применить любое другое на свой вкус.

                                                                                                                                                                                        Да, в конкретном случае ключевые слова async...await опциональны, но для общности с другим кодом и лучшей читаемости допустим и такой вариант.
                                                                                                                                                                                          +1
                                                                                                                                                                                          > для общности с другим кодом

                                                                                                                                                                                          Ересь. У вас другой код решает другую проблему и решает её иначе — нет между ними никакой общности, кроме вашего желания копипастить.
                                                                                                                                                                                            0
                                                                                                                                                                                            Ересь. У вас другой код решает другую проблему и решает её иначе — нет между ними никакой общности, кроме вашего желания копипастить.

                                                                                                                                                                                            У меня отличное мнение от вашего. Сейчас мне не важен результат выполнения метода Load, но в обозримой перспективе он вполне может пригодиться, как показано в примере ниже. Поэтому некоторую общность я наблюдаю и стараюсь сохранить.

                                                                                                                                                                                            .ForEach(async d => await d.Close() && Documents.Remove(d))
                                                                                                                                                                                            .ForEach(async d => await d.Load())
                                                                                                                                                                                            .ForEach(async d => await d.Load() && RecentDocuments.Add(d))
                                                                                                                                                                                              +2
                                                                                                                                                                                              но в обозримой перспективе

                                                                                                                                                                                              YAGNI


                                                                                                                                                                                              Пока Вы будете результат вкручивать, все равно пять раз перепишите. То, что вы написали работать не будет

                                                                                                                                                                                                0
                                                                                                                                                                                                То, что вы написали работать не будет

                                                                                                                                                                                                Отчего же не будет?

                                                                                                                                                                                                .ForEach(async d => await d.Close() && Documents.Remove(d))
                                                                                                                                                                                                уже прекрасно работает.

                                                                                                                                                                                                Если вы имеете в виду, что Add возвращает не bool, то да, здесь я поторопился, но суть примера это не сильно меняет, код больше для того, чтобы проиллюстрировать некоторую общность обоих случаев, а понадобится ли подобное расширение в дальнейшем или нет, уже другой вопрос.
                                                                                                                                                                                                  +1
                                                                                                                                                                                                  Можно я не буду обсуждать недостатки второпях написанного кода? Спасибо.

                                                                                                                                                                                                  Суть в том, что когда (и если) вы это напишите (и протестируете), тогда, может быть(!), там будет какая-то общность. И дописать async-await там — самое простое.

                                                                                                                                                                                                  А пока там копипаста с WTF-эффектом и оправданием.
                                                                                                                                                                                                    0
                                                                                                                                                                                                    Код, что на гитхабе, написан обдуманно и нет там копипасты с WTF-эффектами. Я перепроверил критикуемые моменты и пришёл к выводу, что ничего ошибочного в этом коде нет. Да, его можно переписать чуть иначе, но в данном контексте он функционирует полностью исправно в соответствии с моими ожиданиями и интуицией, поэтому реальной необходимости в его переписывании сейчас у меня нет.

                                                                                                                                                                                                    При ответах же на комментарии, которых довольно много, я не всегда компилирую примеры, поэтому могут возникнуть огрехи, но суть примеров всё же стараюсь сохранять.

                                                                                                                                                                                                    Конечно, здорово, что люди критикуют, ведь сам я много не знаю и могу упустить важное, но на некоторые вопросы могут сосуществовать разные точки зрения. И тут нет правильных и неправильных, просто есть варианты, — хотите, применяйте WhenAll, хотите, ForEach, что вам ближе.
                                                                                                                                                                                                      0
                                                                                                                                                                                                      функционирует полностью исправно в соответствии с моими ожиданиями и интуицией

                                                                                                                                                                                                      WTF эффект, конечно, не на собственном коде проявляется.


                                                                                                                                                                                                      У кода есть 2 функции (минимум) — исполняться в соответствии с вашими ожиданиями, и быть читаемым другими людьми. А «применяйте что вам ближе» второй части, мягко говоря, не помагает.


                                                                                                                                                                                                      А код, написанный «для себя» по определению хуже.

                                                                                                                                                                                            +1
                                                                                                                                                                                            Проще говоря, критикуемый
                                                                                                                                                                                            .ForEach(async d => await Load())
                                                                                                                                                                                            вполне работоспособное и безопасное решение,

                                                                                                                                                                                            Оно "безопасно" тогда и только тогда, когда код внутри Load не бросает эксепшн. И вы об этом, опять-таки, никогда не узнаете, потому что никакого "сразу падает" или "сразу зависает" не будет.

                                                                                                                                                                                              0

                                                                                                                                                                                              На самом деле как раз "сразу падает" и будет если параметр ForEach — Action, а не Func<Task>

                                                                                                                                                                                                0

                                                                                                                                                                                                Не совсем так.


                                                                                                                                                                                                Код
                                                                                                                                                                                                static void Main(string[] args)
                                                                                                                                                                                                {
                                                                                                                                                                                                    Console.WriteLine("Starting");
                                                                                                                                                                                                    try
                                                                                                                                                                                                    {
                                                                                                                                                                                                        new []{"1", "2"}.ForEach(async i => await Some(i));
                                                                                                                                                                                                        Console.WriteLine("After ForEach");
                                                                                                                                                                                                
                                                                                                                                                                                                    }
                                                                                                                                                                                                    catch (Exception e)
                                                                                                                                                                                                    {
                                                                                                                                                                                                        Console.WriteLine("Exception in ForEach: " + e);
                                                                                                                                                                                                        throw;
                                                                                                                                                                                                    }
                                                                                                                                                                                                    Console.WriteLine("After catch");
                                                                                                                                                                                                    Console.ReadLine();
                                                                                                                                                                                                    Console.WriteLine("Finished");
                                                                                                                                                                                                }
                                                                                                                                                                                                
                                                                                                                                                                                                static async Task Some(string item)
                                                                                                                                                                                                {
                                                                                                                                                                                                    Console.WriteLine($"{item}: starting");
                                                                                                                                                                                                    await Task.Yield();
                                                                                                                                                                                                    Console.WriteLine($"{item}: after Yield");
                                                                                                                                                                                                    await Task.Delay(3000);
                                                                                                                                                                                                    Console.WriteLine($"{item}: after Delay, throwing");
                                                                                                                                                                                                    throw new InvalidOperationException(item);
                                                                                                                                                                                                }

                                                                                                                                                                                                Результат:


                                                                                                                                                                                                Starting
                                                                                                                                                                                                1: starting
                                                                                                                                                                                                2: starting
                                                                                                                                                                                                After ForEach
                                                                                                                                                                                                After catch
                                                                                                                                                                                                1: after Yield
                                                                                                                                                                                                2: after Yield
                                                                                                                                                                                                2: after Delay, throwing
                                                                                                                                                                                                1: after Delay, throwing
                                                                                                                                                                                                
                                                                                                                                                                                                Unhandled Exception

                                                                                                                                                                                                Обратите внимание: ForEach уже прошел, и try...catch, в который он завернут — тоже. Эксепшн бросился где-то и когда-то, и то, будет ли он пойман системой, зависит от контекста и диспетчера (что и видно в комменте ниже, где эксепшн никак не проявился в таком сценарии).


                                                                                                                                                                                                А теперь заменим ForEach(async i => await Some(i)) на ForEach(i => Some(i)):


                                                                                                                                                                                                Starting
                                                                                                                                                                                                1: starting
                                                                                                                                                                                                2: starting
                                                                                                                                                                                                After ForEach
                                                                                                                                                                                                After catch
                                                                                                                                                                                                1: after Yield
                                                                                                                                                                                                2: after Yield
                                                                                                                                                                                                2: after Delay, throwing
                                                                                                                                                                                                1: after Delay, throwing
                                                                                                                                                                                                
                                                                                                                                                                                                Finished

                                                                                                                                                                                                Эксепшн вообще не пойман.

                                                                                                                                                                                                0
                                                                                                                                                                                                Оно «безопасно» тогда и только тогда, когда код внутри Load не бросает эксепшн. И вы об этом, опять-таки, никогда не узнаете, потому что никакого «сразу падает» или «сразу зависает» не будет.

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

                                                                                                                                                                                                Во-вторых, если всё же нужно обработать исключение, то тут как раз-таки обязательно нужны async...await, чтобы оно не прошло незамеченным мимо
                                                                                                                                                                                                .ForEach(async d => 
                                                                                                                                                                                                {
                                                                                                                                                                                                    try { await Load(); }
                                                                                                                                                                                                    catch (Exception e) { Debug.WriteLine(e); }
                                                                                                                                                                                                })
                                                                                                                                                                                                

                                                                                                                                                                                                что, вероятно, подразумевалось в комметарии
                                                                                                                                                                                                На самом деле как раз «сразу падает» и будет если параметр ForEach — Action, а не Func

                                                                                                                                                                                                Однако отмечу, что в обоих случаях
                                                                                                                                                                                                .ForEach(async d => await Load())
                                                                                                                                                                                                .ForEach(d => Load())

                                                                                                                                                                                                у меня исключение проигнорировалось.

                                                                                                                                                                                                Если же необходимо обрабатывать сразу агрегированную группу исключений, то стоит использовать WhenAll, в то время как ForEach позволяет сосредоточиться на обработке каждого исключения индивидуально. Где это может иметь значение? Например, у нас ряд долговыполняющихся тасков, WhenAll выбросит исключение, лишь когда все они завершатся, что может занять продолжительное время, ForEach же будет выбрасывать исключения по мере выполнения каждого таска отдельно, не дожидаясь остальных.
                                                                                                                                                                                                  0
                                                                                                                                                                                                  Во-первых, в конкретном случае архитектурно задумано

                                                                                                                                                                                                  Я и говорю: ваш метод сам по себе не "безопасен", он рассчитывает, что вызываемые им методы "безопасны".


                                                                                                                                                                                                  Во-вторых, если всё же нужно обработать исключение, то тут как раз-таки обязательно нужны async...await

                                                                                                                                                                                                  Нет, достаточно сделать так, чтобы ForEach собирал результаты выполнения.


                                                                                                                                                                                                  ForEach позволяет сосредоточиться на обработке каждого исключения индивидуально

                                                                                                                                                                                                  Я не могу придумать сценария, где это нужно генерично (т.е., на уровне метода-расширения).

                                                                                                                                                                                                    0
                                                                                                                                                                                                    Я и говорю: ваш метод сам по себе не «безопасен», он рассчитывает, что вызываемые им методы «безопасны».

                                                                                                                                                                                                    Согласен. Но это нормальная ситация в программировании иногда расчитывать на то, что метод безопасен, если так задумано изначально. Ведь никто же, как правило, не оборачивает ToString() в try-catch, хотя исключение вполне может произойти даже при таком безобидном вызове.

                                                                                                                                                                                                    Нет, достаточно сделать так, чтобы ForEach собирал результаты выполнения.

                                                                                                                                                                                                    В своём естественном применении ForEach ничего не собирает — это просто альтернатива классическому циклу foreach с некоторыми вариациями и бонусами. Конечно, можно исхитриться и что-то им собрать, но тогда уж лучше использовать другие методы-расширения из Linq.

                                                                                                                                                                                                    Я не могу придумать сценария, где это нужно генерично (т.е., на уровне метода-расширения).

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

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

                                                                                                                                                                                                      И чем это отличается от "расчитывать, что метод корректно ведет себя с точки зрения асинхронии"? Понимаете, вы пишете код, в котором есть некоторые неявные, никак не видимые из контракта предположения, но при этом считаете, что ваши предположения — уместны, а мои — нет. Хотя и те, и те вырастают из чтения кода.


                                                                                                                                                                                                      В своём естественном применении ForEach ничего не собирает — это просто альтернатива классическому циклу foreach с некоторыми вариациями и бонусами.

                                                                                                                                                                                                      Это неправда. В своем естественном применении ForEach выполняет примитивнейшую стратегию обработки ошибок "упади на первой ошибке". Вы применяете его для асинхронии (чего уже делать не стоит) и при этом ничего не делаете, чтобы сохранить эту стратегию.


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

                                                                                                                                                                                                      Или надо не запускать остальные, потому что исключение формата "у нас нет сети", и они все равно упадут. You never know.


                                                                                                                                                                                                      (ну и да, такие вещи делаются без async по-хорошему, а на сообщениях или их имитации)

                                                                                                                                                                                                        0
                                                                                                                                                                                                        Спор у нас больше филосовского характера, вы стремитесь к «канонам», мне же интересны нестандартные пути и решения, даже, я бы сказал, неожиданные.

                                                                                                                                                                                                        Вот вы видите впервые в коде With или ForEach с асинхронным делеготом, сразу вопрос — что это вообще такое и как оно работает? Начинаете разбираться, копаться, спорить, попутно узнаёте для себя что-то новое или вспоминаете позабытое… Разве это плохо?

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

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

                                                                                                                                                                                                        Моя задача — просто делиться такими решениями с людьми, а каждый самостоятельно решит, насколько они ему подходят.
                                                                                                                                                                                                          0
                                                                                                                                                                                                          Спор у нас больше филосовского характера, вы стремитесь к «канонам»

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


                                                                                                                                                                                                          Вот вы видите впервые в коде With или ForEach с асинхронным делеготом, сразу вопрос — что это вообще такое и как оно работает? [...] Разве это плохо?

                                                                                                                                                                                                          Да, это очень плохо, если речь идет о production-коде, при чтении которого вопросов "что это и как оно работает" должно быть как можно меньше и еще меньше.


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

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


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

                                                                                                                                                                                                          … точнее, то, что вы считаете вкусом и чувством прекрасного. Как можно видеть в дискуссии, не все с вами согласны.

                                                                                                                                                                                                            +3
                                                                                                                                                                                                            сразу вопрос — что это вообще такое и как оно работает?

                                                                                                                                                                                                            Ну вот, а вы говорите в вашем коде нет WTF-эффекта...