Правостороннее присваивание и другие необычные приёмы программирования в 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);
    

    С версии C# 7.2 поддерживаются ссылочные методы-расширения для структур `this ref`, поэтому можно использовать их

    public static T WithRef<T, A>(this ref T o, A a) where T: struct => o;
    

    А с версии C# 7.3 допустимо совместное использование перегрузок

    public static T With<T, A>(this ref T o, A a) where T: struct => o;
    public static T With<T, A>(this T o, A a) where T: class => o;
    

    Также `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`методов, а также позволяют сделать код более чистым и выразительным. И если ты тоже ощутил вкус этих расширений, то приятного применения на практике!

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


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

    Ну. И что?
    Реклама
    Комментарии 365
    • +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);
                                                                                                                                                }