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

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

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

За дело!

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

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


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

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


  1. lair
    25.04.2018 12:30
    +1

    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. petuhov_k
      25.04.2018 14:17

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


      1. lair
        25.04.2018 14:46

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

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


        1. Makeman Автор
          25.04.2018 15:00

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

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


          1. lair
            25.04.2018 15:03

            Будет явно объявлена локальная переменная в вызывающем методе.

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


            1. Makeman Автор
              25.04.2018 15:20

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

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


              1. lair
                25.04.2018 15:23

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

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


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


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

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


                1. Makeman Автор
                  25.04.2018 15:39

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

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

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


                  1. lair
                    25.04.2018 15:40

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

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


      1. Makeman Автор
        25.04.2018 14:55

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

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


        1. lair
          25.04.2018 15:03

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

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


          1. Makeman Автор
            25.04.2018 15:09

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

            IModel GetModel() => new AnyModel();


            1. lair
              25.04.2018 15:12

              Это называется expression-bodied member.


              1. Makeman Автор
                25.04.2018 15:23

                Спасибо, буду называть правильно.


    1. Makeman Автор
      25.04.2018 14:46

      Работает по аналогии с инициализационными блоками

      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. lair
        25.04.2018 14:49

        В случае с 'With' мы декларируем контекст явно

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


        1. Makeman Автор
          25.04.2018 15:04

          Локальная переменная в методе

          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. lair
            25.04.2018 15:05

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


            1. Makeman Автор
              25.04.2018 15:11

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


              1. lair
                25.04.2018 15:13

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


                1. Makeman Автор
                  25.04.2018 15:30

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

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

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


                  1. lair
                    25.04.2018 15:39
                    +1

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


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


                    1. Makeman Автор
                      25.04.2018 15:59

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

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

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

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


                      1. lair
                        25.04.2018 16:02

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

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


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

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


                        1. Makeman Автор
                          25.04.2018 16:10

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

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

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

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


                          1. lair
                            25.04.2018 16:12

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

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


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

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


                            1. Makeman Автор
                              25.04.2018 16:21

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


                              1. lair
                                25.04.2018 16:45

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


                                1. Makeman Автор
                                  25.04.2018 17:05
                                  -1

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


                                  1. lair
                                    25.04.2018 17:07

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


                                    1. Makeman Автор
                                      25.04.2018 17:18
                                      -1

                                      Замечательно! Рад за вас! )


                          1. nporaMep
                            26.04.2018 01:53

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

                            Выше среднего идут в игроделанье или сток трейдинг какой-нить и фигачут все на С/С++ без границ.


                            1. Makeman Автор
                              26.04.2018 08:13

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


                              1. nporaMep
                                26.04.2018 09:41

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

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


                                1. Makeman Автор
                                  26.04.2018 19:49

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

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

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


  1. forcewake
    25.04.2018 15:55

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


    1. lair
      25.04.2018 16:01

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


    1. Makeman Автор
      25.04.2018 16:02

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


      1. petuhov_k
        26.04.2018 17:05

        Вызов функции — это не только передача аргументов через стек. Это ещё и «сброс» состояния локальных переменных метода, из которого идёт вызов и их восстановление после возврата. Короче говоря, даже в 21-м веке вызов функции — это не самая дешёвая операция.


        1. Makeman Автор
          26.04.2018 18:04

          Что вы подразумеваете под сбросом состояния? Насколько я понимаю, локальные переменные всегда остаются в стеке (во многом это и вызывает stack overflow при крайне глубоком рекурсивном вызове методов). Зачем их куда-то ещё сбрасывать, чтобы потом восстанавливать?

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

          Моё понимание интуитивно, поэтому могу ошибаться, поправьте, если что не так. :)


          1. lair
            26.04.2018 18:10

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

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


            1. Makeman Автор
              26.04.2018 18:44

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

              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. lair
                26.04.2018 18:47

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

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


                1. Makeman Автор
                  26.04.2018 19:02
                  -1

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

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


                  1. lair
                    26.04.2018 19:41
                    +1

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


                    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>();
                        }
                    }


                    1. Makeman Автор
                      26.04.2018 19:57

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

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


                      1. lair
                        26.04.2018 20:05

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

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


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

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


                        1. Makeman Автор
                          26.04.2018 20:20
                          -1

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

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


                          1. lair
                            26.04.2018 23:28

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


                            1. areht
                              26.04.2018 23:34

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

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


                              1. lair
                                26.04.2018 23:37

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


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


                            1. Makeman Автор
                              27.04.2018 01:25

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

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

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


                              1. lair
                                27.04.2018 01:29
                                +1

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

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


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

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


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

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


                                1. Makeman Автор
                                  27.04.2018 01:56

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

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

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


                          1. petuhov_k
                            27.04.2018 04:47
                            +1

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


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


                            1. Makeman Автор
                              27.04.2018 12:57

                              Смотря для каких задач использовать. )


                      1. petuhov_k
                        27.04.2018 04:44

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

                        Весь наш код складывается из «стомилионных долей секунды». Каждый вызов, каждая строчка, каждое выражение. Если вы пишете, например, десктопный UI, то пользователь, скорее всего, не заметит просадки ни в пять ни в сто раз. Но в бэкенде, это окажется критичным. А подход лучше использовать один ко всему коду.


                        1. Makeman Автор
                          27.04.2018 12:55
                          -2

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

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

                          :)


  1. SlavniyTeo
    25.04.2018 16:18
    +1

    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)) ...

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

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


    1. Makeman Автор
      25.04.2018 16:27

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

      var (x, y, z) = 1;


  1. Free_ze
    25.04.2018 18:36
    +1

    Почему бы для случая 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 недалеко...


    1. Makeman Автор
      25.04.2018 19:42

      У такого подхода есть ряд недостатков:

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

      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); // получим исходное значение вместо "Иван" 


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

      Хотя, конечно, никто не отменяет и этот подход, кому что ближе. :)


      1. mayorovp
        25.04.2018 22:27

        Всего-то нужна перегрузка которая принимает ref.


      1. areht
        26.04.2018 03:42

            p.FirstName.To(out var oldFirstName),
            p.FirstName = "Иван",
        


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


        1. Makeman Автор
          26.04.2018 08:30

          Хороший вопрос.

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

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

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


          1. areht
            26.04.2018 12:35

            — expression-bodied методах

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

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

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


            1. Makeman Автор
              26.04.2018 18:21

              Скажу так… насчёт expression-bodied стиля:

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

              И лично для меня последние пункты самые важные. :)
              Если бы не эта математическая красота, то давно бы уже забросил программирование!


              1. Free_ze
                26.04.2018 18:35
                +2

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


                1. Makeman Автор
                  26.04.2018 19:11
                  -1

                  Скорее: «Смотри, ты тоже так можешь!»

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

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


                  1. Free_ze
                    26.04.2018 20:07
                    +1

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


                    1. Makeman Автор
                      26.04.2018 20:28

                      Как говорится: «Любое категоричное суждение ошибочно, даже это». Поэтому не стоит воспринимать мои слова как абсолютную истину :)

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


              1. areht
                26.04.2018 18:43
                +2

                > насчёт expression-bodied стиля:

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

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


                1. Makeman Автор
                  26.04.2018 18:55

                  Если для вас это выглядит «хламом», то не пользуйтесь. )

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

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


                  1. lair
                    26.04.2018 19:02
                    +1

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

                    Дадада.


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

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


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


                    1. Makeman Автор
                      26.04.2018 19:17
                      -2

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

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

                      Можете даже скомпилировать проект и убедиться, что всё работает вполне себе живо. :)


                      1. lair
                        26.04.2018 19:42

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

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

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


                        1. Makeman Автор
                          26.04.2018 19:59

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


                          1. lair
                            26.04.2018 20:08
                            +3

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


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


                            Круто, да.


                            1. Makeman Автор
                              26.04.2018 20:34
                              -1

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

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


                              1. lair
                                26.04.2018 23:30

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

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


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

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


                                1. Makeman Автор
                                  27.04.2018 01:45

                                  Все документы реализуют следующий интерфейс (ADocument) доступный для использования в CoreViewModel

                                  public abstract Task<bool> Load();
                                  public abstract Task<bool> Save();
                                  public abstract Task<bool> SaveAs();
                                  public abstract Task<bool> Close();

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

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

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


                                  1. lair
                                    27.04.2018 01:49
                                    +2

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

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


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


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

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


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

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


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


                                    1. Makeman Автор
                                      27.04.2018 02:11

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

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

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


                                      1. lair
                                        27.04.2018 15:19

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

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

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


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


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

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


                                        1. Makeman Автор
                                          27.04.2018 16:13
                                          -1

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

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


                                          1. lair
                                            27.04.2018 16:14

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


                                            1. Makeman Автор
                                              27.04.2018 16:20

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

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


                                              1. lair
                                                27.04.2018 16:30

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


                                                1. Makeman Автор
                                                  27.04.2018 16:38

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

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

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


                                                  1. lair
                                                    27.04.2018 16:44

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


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

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


                                                    1. Makeman Автор
                                                      27.04.2018 17:03

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

                                                      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. lair
                                                        27.04.2018 17:08
                                                        +1

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


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


                                                        1. Makeman Автор
                                                          28.04.2018 01:44

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

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

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

                                                          new List<ADocument>() {...}
                                                              .ForEach(async d => await d.DoSomething());

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

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


                                                          1. lair
                                                            28.04.2018 01:50
                                                            +1

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

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


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

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


                                                            1. Makeman Автор
                                                              28.04.2018 01:55

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

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


                                                              1. lair
                                                                28.04.2018 01:58
                                                                +1

                                                                И на что вы мне предлагаете его заменить?

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


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

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


                                                                1. Makeman Автор
                                                                  28.04.2018 02:09
                                                                  -1

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

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


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

                                                                  ...ForEach(async d => await d.Close() && Documents.Remove(d))


                                                                  1. lair
                                                                    28.04.2018 02:13
                                                                    +2

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

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


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

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


                                                                    1. Makeman Автор
                                                                      28.04.2018 02:23
                                                                      -2

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

                                                                      async void Load() => await ...
                                                                      ...ForEach(d => d.Load());


                                                                      1. lair
                                                                        28.04.2018 02:29
                                                                        +2

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

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


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


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


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


                                                                        1. Makeman Автор
                                                                          28.04.2018 02:56

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

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


                                                                          1. lair
                                                                            28.04.2018 03:00

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


                                                                            1. Makeman Автор
                                                                              28.04.2018 03:09

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


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


                                                                              1. lair
                                                                                28.04.2018 03:13

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


                                                                        1. Szer
                                                                          28.04.2018 12:18
                                                                          +1

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

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


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


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


                                                                          1. lair
                                                                            28.04.2018 12:26
                                                                            +1

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


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


                                                                            1. Makeman Автор
                                                                              28.04.2018 12:51

                                                                              По-видимому, вы оказались правы в том, что 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++;
                                                                              	}
                                                                              }
                                                                              


                                                                              1. lair
                                                                                28.04.2018 12:53

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

                                                                                Кэп.


                                                                    1. lair
                                                                      28.04.2018 03:02

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

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


                                                                      1. Makeman Автор
                                                                        28.04.2018 03:20

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


                                                                        1. lair
                                                                          28.04.2018 03:21

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


                                                                          1. Makeman Автор
                                                                            28.04.2018 03:29
                                                                            -2

                                                                            Никак вам не различить эти ситуации. То, когда начнется выполнение, зависит от того, что внутри AnyTask, а не снаружи.
                                                                            Если правильно понимаю вас, то при наличии интерфейса, как у меня, и абстрагируясь от конкретной имплементации документа, мне нужно явно указывать await у Load, чтобы гарантированно выполнить таск, верно? Или чего-то ещё не понимаю?


                                                                            1. mayorovp
                                                                              28.04.2018 06:35

                                                                              Вам уже три раза написали: в C# await никак не влияет на то будет ли таск запущен (в отличие от Python и С++).

                                                                              А если он не влияет — то и гарантировать ничего не может.


                                                                              1. Makeman Автор
                                                                                28.04.2018 10:39

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


                                                                                1. mayorovp
                                                                                  28.04.2018 10:41

                                                                                  Если код свой — то просто не писать глупого кода.

                                                                                  Если код чужой — смотреть в документацию.


                                                                                  1. Makeman Автор
                                                                                    28.04.2018 10:44

                                                                                    А что в документации? Если её нет, а просто интерфейс с таском?


                                                                                    1. mayorovp
                                                                                      28.04.2018 10:46

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

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


                                                                                      1. Makeman Автор
                                                                                        28.04.2018 11:14

                                                                                        Благодарю за ответы! Узнал для себя что-то новое.

                                                                                        Последнее уточнение, для случая
                                                                                        ...ForEach(async d => await d.Load())
                                                                                        компилятор сгененирует ощутимо менее оптимальный код, чем для
                                                                                        ...ForEach(d => d.Load()), поэтому второй вариант предпочтительнее? Или дело в другом?


                                                                                        1. mayorovp
                                                                                          28.04.2018 11:19

                                                                                          Именно так и есть. Не то чтобы «ощутимо» менее оптимальный — но все-таки второй вариант содержит на 1 класс, 2 Interlocked-операции и несколько кадров стека меньше. Но поскольку для написания более оптимального кода нужно не добавлять что-то в код, а наоборот — стереть два слова — этого достаточно чтобы бесить перфекционистов вроде меня :-)


                                                                                          1. lair
                                                                                            28.04.2018 11:24
                                                                                            +1

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


                                                                                          1. Makeman Автор
                                                                                            28.04.2018 11:27

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

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

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

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


                                                                                            1. lair
                                                                                              28.04.2018 11:44

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

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


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

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


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

                                                                                              Я не удивлен.


                                                                                              1. Makeman Автор
                                                                                                28.04.2018 11:55

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

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


                                                                                                1. lair
                                                                                                  28.04.2018 11:57

                                                                                                  Как-никак увидев async...await человек понимает, что с тасками идёт работа.

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


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

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


                                                                                                  1. Makeman Автор
                                                                                                    28.04.2018 12:12

                                                                                                    Если учесть правило

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

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

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


                                                                                                    1. lair
                                                                                                      28.04.2018 12:23

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

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


                                                                                                      1. Makeman Автор
                                                                                                        28.04.2018 12:53

                                                                                                        Конечно, мне интересно узнать, какие ещё могут варианты произойти.


                                                                                                        1. lair
                                                                                                          28.04.2018 12:57
                                                                                                          +1

                                                                                                          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();
                                                                                                          }
                                                                                                          


                                                                                                          1. lair
                                                                                                            28.04.2018 13:15
                                                                                                            +1

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


                                                                                                            static void ForEach<T>(IEnumerable<T> source, Func<T,Task> f)
                                                                                                            {
                                                                                                              foreach(var s in source)
                                                                                                                f(s);
                                                                                                            }


                                                                                                          1. Makeman Автор
                                                                                                            28.04.2018 14:20

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

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

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


                                                                                                            1. lair
                                                                                                              28.04.2018 14:28

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

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


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

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


                                                                                                              1. Makeman Автор
                                                                                                                28.04.2018 14:33

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

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


                                                                                                                1. lair
                                                                                                                  28.04.2018 14:36

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

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


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

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


                                                                                                                  1. Makeman Автор
                                                                                                                    28.04.2018 14:51

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

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

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


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

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


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


                                                                                                                    1. lair
                                                                                                                      28.04.2018 14:53

                                                                                                                      await Task.Delay(3000 + rand.Next()%10000);

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


                                                                                                                      1. Makeman Автор
                                                                                                                        28.04.2018 15:05

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


                                                                                                                        1. lair
                                                                                                                          28.04.2018 15:10

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


                                                                                                                          1. Makeman Автор
                                                                                                                            28.04.2018 15:27

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

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


                                                                                                                            1. lair
                                                                                                                              28.04.2018 15:29

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

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


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

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


                                                                                                                              1. Makeman Автор
                                                                                                                                28.04.2018 15:45

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

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

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

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


                                                                                                                                1. lair
                                                                                                                                  28.04.2018 15:47

                                                                                                                                  При вашем подходе

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


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

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


                                                                                                                                  1. Makeman Автор
                                                                                                                                    28.04.2018 15:58

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

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


                                                                                                                                    1. lair
                                                                                                                                      28.04.2018 16:01

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


                                                                                                                                      1. Makeman Автор
                                                                                                                                        28.04.2018 16:17

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

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


                                                                                1. lair
                                                                                  28.04.2018 11:23

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


                                                                            1. lair
                                                                              28.04.2018 11:23

                                                                              Вы явно чего-то не понимаете. Выполнение кода не зависит от того, что вы делаете с полученным объектом Task.


                                                              1. areht
                                                                28.04.2018 02:45
                                                                +4

                                                                > Тасками я пользуюсь по большей части интуитивно

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


                                                                1. Makeman Автор
                                                                  28.04.2018 03:02
                                                                  -1

                                                                  Отличная шутка! :))


                                  1. lair
                                    27.04.2018 01:53

                                    Простой вопрос: что будет, если я вызову CoreViewModel.Expose, а потом немедленно CoreViewModel.Documents.First().SaveAs()?


                                    1. Makeman Автор
                                      27.04.2018 02:18
                                      -1

                                      Откроется диалог сохранения файлов, а при нажатии на «Ок» вызовется «File.WriteAllText». Если документ будет пустой, то возникнет и обработается исключение ArgumentNullException «contents is empty», в ином случае сохранится, если других исключений не будет.


                                      1. lair
                                        27.04.2018 15:20

                                        … и тот милый факт, что документ пустой только потому, что Load еще не завершился, вас никак не смущает?


                                        1. Makeman Автор
                                          27.04.2018 16:06

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


                                          1. lair
                                            27.04.2018 16:08

                                            И нормально, что в процессе загрузки (которая, например, может быть чанками) кто-то вызвал Save?


                                            1. Makeman Автор
                                              27.04.2018 16:16

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


                                              1. lair
                                                27.04.2018 16:28

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

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


                                                1. Makeman Автор
                                                  27.04.2018 16:44

                                                  Не блокировать UI, а работать асинхронно.

                                                  Вся логика работы с файлом (или группой фалов) инкапсулирована внутри самого документа.

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


                                                  1. lair
                                                    27.04.2018 16:45

                                                    Не блокировать UI, а работать асинхронно.

                                                    Я про UI и не говорил ничего, я говорил про остальные операции над документом.


                                                    1. Makeman Автор
                                                      27.04.2018 17:04

                                                      Если нужно, то что-то может быть и заблокировано на время.


                                                1. Zam_dev
                                                  27.04.2018 17:27

                                                  Так и не дождался момента, как все-таки правильно получить завершения обработки)

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


                                                  1. Makeman Автор
                                                    27.04.2018 17:42

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


                                                    1. Zam_dev
                                                      27.04.2018 17:53

                                                      вопрос был к lair, не в «конкретном» случае…
                                                      сколько не воюю в возвратами из await — уверенного восприятия нет.


                                                  1. lair
                                                    27.04.2018 17:52

                                                    Вас мое мнение интересует?


                                                    await Documents
                                                      .Select(d => {
                                                        d.Expose();
                                                        return d.Load();
                                                      })
                                                      .WaitAll();


                                                    1. Zam_dev
                                                      27.04.2018 18:01

                                                      Спасибо. Если требуется по окончании обработки запустить метод и запуск произойдет в другом потоке?

                                                       .ContinueWith((a) =>Application.InvokeOnMain(()=> method())

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


                                                      1. lair
                                                        27.04.2018 18:05

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


                                                        1. Zam_dev
                                                          27.04.2018 18:24

                                                          На счет правильности не уверен, но «выход» нашел в виде костыля с введением нового свойства bool, где один поток меняет его а другой подхватывая запускает метод…
                                                          Где можно почитать подробнее об организации подобного диспетчера?


                                                          1. lair
                                                            27.04.2018 18:35

                                                            Его не надо организовывать, его вам система предоставляет. Рекомендую книгу Stephen Cleary, Concurrency in C# Cookbook.


                                                            1. Zam_dev
                                                              27.04.2018 18:39

                                                              Принял, благодарю.


                                                    1. Makeman Автор
                                                      27.04.2018 18:06

                                                      Если у вас WaitAll — кастомное расширение, то лучше дать ему название AwaitAll, ибо оригинальный WaitAll — это синхронный вызов

                                                      public static void WaitAll(
                                                      	params Task[] tasks
                                                      )


                                                      1. lair
                                                        27.04.2018 18:11

                                                        Лучше назвать его WhenAll.


                  1. areht
                    26.04.2018 19:47

                    > Конечно, я бы мог разбить его на более мелкие части, но в собранном виде мне нравится больше.

                    Да я и не сомневаюсь.

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

                    Так вы говорите

                    Documents.CollectionChangeCompleted += (sender, args) =>
                    args.NewItems?.Cast<ADocument>().LastOrDefault()
                    .To(out var document)?.With(ActiveDocument = document);

                    эстетичнее, прекраснее и чище, чем
                    Documents.CollectionChangeCompleted += (sender, args) => 
                    ActiveDocument = args.NewItems?.Cast<ADocument>().LastOrDefault()

                    ?


                    1. lair
                      26.04.2018 19:50

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


                    1. Makeman Автор
                      26.04.2018 20:10

                      Здесь разное поведение.

                      Думаю, так будет лучше видна разница в логике

                      Documents.CollectionChangeCompleted += (sender, args)
                      {
                          var document = args.NewItems?
                              .Cast<ADocument>().LastOrDefault();
                          if (document != null) ActiveDocument = document;
                      }

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


                      1. lair
                        26.04.2018 20:15

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


                        1. Makeman Автор
                          26.04.2018 20:37

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


                          1. areht
                            26.04.2018 21:01
                            +2

                            Экий у вас эвфемизм для разложенных граблей.


                          1. lair
                            26.04.2018 23:31
                            +2

                            Ну то есть надо прилагать лишние усилия для чтения вашего "читаемого" кода. Спасибо, нет.


                            1. Makeman Автор
                              27.04.2018 02:23

                              Без труда — не вытащишь и рыбку из пруда.

                              Знаете, ваша позиция напоминает мне такую: «Эти интегралы слишком сложные, нужно приложить усилия, чтобы научиться их читать и понимать, лучше я буду пользоваться школьной математикой». )


                              1. areht
                                27.04.2018 06:55

                                Скорее «зачем делить методом галеры»


                                1. Makeman Автор
                                  27.04.2018 14:21

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


                                  1. areht
                                    27.04.2018 19:51
                                    +1

                                    Отвечая «я художник, я так вижу» вы выходите из понятийного пространства инженерии. В инженерии, «фантазия» на выходе должна давать конкретное измеримое преимущество.


                              1. lair
                                27.04.2018 15:33

                                Без труда — не вытащишь и рыбку из пруда.

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


                                Знаете, ваша позиция напоминает мне такую: «Эти интегралы слишком сложные, нужно приложить усилия, чтобы научиться их читать и понимать, лучше я буду пользоваться школьной математикой».

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


                                1. Makeman Автор
                                  27.04.2018 16:21

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


                                  1. lair
                                    27.04.2018 16:30

                                    А если они мне не нужны, то зачем мне их уметь читать и понимать?


                                    1. Makeman Автор
                                      27.04.2018 16:52
                                      -1

                                      Ну так не читайте и не понимайте, никто здесь вас не принуждает чего-то делать. )

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


                                      1. lair
                                        27.04.2018 17:00

                                        Просто ваша призыв выглядит сейчас так

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


                                        1. Makeman Автор
                                          27.04.2018 17:06
                                          -1

                                          Уж как есть. )


              1. lair
                26.04.2018 18:49
                +1

                expression-bodied [...] развивает чувство прекрасного

                Доказательства в студию. Вот прямо начиная с объективного определения чувства прекрасного и метода его определения.


                Если бы не эта математическая красот

                "Математическая красота" — это то отстутствие побочных эффектов (вытекающее из правила подстановок), на которое вы забили?


      1. Deosis
        26.04.2018 07:32

        Ваш код можно переписать и без использования дополнительного сахара:


        var p = GetPerson();
        p.FirstName.To(out var oldFirstName);
        p.FirstName = "Иван";
        p.LastName = "Пупкин";
        var personStringView = p.ToString();
        var manager = p;

        Так что, кроме методов-выражений полезности почти никакой.


        1. Makeman Автор
          26.04.2018 08:23

          Многое в C# можно переписать без дополнительного синтаксического сахара: автоматические свойства, создание делеготов, лямбда-выражения… те же инициализационные блоки, например,

          var p = new Person();
          p.FirstName = "Иван";
          p.LastName = "Пупкин";

          var p = new Person
          {
              FirstName = "Иван",
              LastName = "Пупкин"
          }

          Может, это и не самая остро необходимая функция языка, но мне она весьма по душе. :)


          1. Deosis
            27.04.2018 08:08

            Обычно сахар вводят для уменьшения видимого размера кода.
            Автосвойства позволяют не писать backing field.
            Делегаты создаются неявно.
            async/await позволяет 2 словами заменить целую машину состояний и ручную работу с продолжениями.


            1. Makeman Автор
              27.04.2018 13:28

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

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

              GetPerson()?.With
              {
                  .FirstName = "Иван",
                  .LastName = "Пупкин"
              }.DoSomething();

              но это вопросы к разработчикая языка. Я вносил подобные предложения, но дизайнеры уже приняли ряд весьма неоднозначных реший, первое из которых уже в релизе
              IModel GetModel() => ...
              
              /* true|false */
              GetModel() is IModel m
              
              /* always true */
              GetModel() is var m

              Далее предполагаются
              /* true|false */
              GetModel() is {} m // null check
              
              /* true|false */
              GetModel() is {Name is {} s, City is var c} m

              не знаю, как для вас, но для меня более очевидны такие формы
              /* true|false */
              GetModel().Is(out var m) // null check
              
              /* true|false */
              GetModel().Is(out var m) && m.Check
              (
                  m.Name.Is(out var s), // null check
                  m.City.To(out var c).Put(true)
              ).All(true)


              1. areht
                27.04.2018 14:25
                +1

                GetPerson()?.With
                {
                .FirstName = "Иван",
                .LastName = "Пупкин"
                }.DoSomething();


                А вот это у вас вообще что делает? Ну, в бизнесовом смысле. Вы (не)получаете человека, что-то меняете и, может быть, делаете запрошенное пользователем действие? У вас, кажется, много логики на null без обработки проблем...


                1. Makeman Автор
                  27.04.2018 16:26

                  Работает по аналогии с вызовом обычных методов

                  GetPerson()?.MethodA().MethodB();


                  1. areht
                    27.04.2018 18:10
                    +2

                    Вот вы отвечаете «как работает», а вопрос был «а нахрена такое?»

                    У меня примерно в 100% случаев появление null требует или обработки ошибки, или появления fallback value перед вызовом MethodB. То есть такой цепочки просто не появляется.

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


                    1. Makeman Автор
                      28.04.2018 01:50

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


                      1. areht
                        28.04.2018 04:02

                        И я там тоже не вижу конструкции вида GetPerson()?.With(..).DoSomething();

                        То есть, у вас тоже нет живого примера того, что «было бы здорово получить» — в реальной жизни это не нужно. И, соответственно, в C#, ради такого, сахар добавлять не надо.


                        1. Makeman Автор
                          28.04.2018 10:42

                          Как вы относитесь к потенциальной фиче (частному случаю Check паттерна, родственного With)?

                          /* true|false */
                          GetModel() is {} m // null check
                          
                          /* true|false */
                          GetModel() is {Name is {} s, City is var c} m


                          1. lair
                            28.04.2018 11:26

                            Это явная проверка, а не цепочечный метод.


                            (и это не какой-то "Check-паттерн", а обычный паттерн-матчинг).


                            1. Makeman Автор
                              28.04.2018 11:42

                              Знаете, почему вообще появились многие идеи из текущей статьи? Меня просто обескураживают некоторые детали «обычного паттерн-матчинга», а с помощью кастомных расширений я могу избежать их применения в своём коде, пусть даже ценой небольшого падения производительности (во многих случаях это для меня не критично).

                              Если бы мне в нём всё сразу понравилось, то никаких альтернатив точно бы не изобретал.


                              1. lair
                                28.04.2018 11:43

                                Ну, много кому что не нравится. Иногда из этого "не нравится" получается что-то хорошее. Чаще — NIH.


                                1. Makeman Автор
                                  28.04.2018 11:48
                                  -1

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

                                  Всего лишь делюсь результатами.


                                  1. lair
                                    28.04.2018 11:49

                                    Всего лишь делюсь результатами.

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


                                    1. Makeman Автор
                                      28.04.2018 12:01
                                      -1

                                      Я и не против, отвечаю же вам. )


              1. lair
                27.04.2018 15:36

                Конечно, было бы здорово получить синтаксический сахар на нативном уровне вроде
                GetPerson()?.With
                {
                .FirstName = "Иван",
                .LastName = "Пупкин"
                }.DoSomething();

                Вот только в этот момент непонятно, что такое With с точки зрения синтаксиса.


                не знаю, как для вас, но для меня более очевидны такие формы

                А для меня более очевиден обычный паттерн-матчинг.


                1. Makeman Автор
                  27.04.2018 16:00
                  -1

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


                1. Makeman Автор
                  27.04.2018 16:04

                  Вот только в этот момент непонятно, что такое With с точки зрения синтаксиса.

                  Что-то наподобие инициализационного блока с дополнительным префиксом и доступом к контексту через '.'


                  1. lair
                    27.04.2018 16:08

                    Инициализационный блок не может быть вызван как метод.


                    1. Makeman Автор
                      27.04.2018 16:24

                      По сути, инициализационный блок — это explicit aggressive inlined метод.


                      1. lair
                        27.04.2018 16:31

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


                        1. Makeman Автор
                          27.04.2018 17:08

                          Метод — тоже набор инструкций. А aggressive inlined метод — набор инструкций, который делает вид, что он метод.


                          1. lair
                            27.04.2018 17:09

                            Вы это определение на ходу придумали, да?


                            1. Makeman Автор
                              27.04.2018 17:10

                              Да, на ходу.


                              1. lair
                                27.04.2018 17:12
                                +1

                                Ну вот поэтому и не понятно, что такое With с точки зрения синтаксиса, потому что придуманное вами определение ничему в C# не соответствует.


      1. Free_ze
        26.04.2018 10:49

        нельзя без замыканий выводить значения в новые переменные (производить деконструкцию объекта)
        Это я даже назвал плюсом) Польза появления новых переменных по умолчанию сомнительна, кмк.
        нет возможности удобно модифицировать структуры
        Как уже написал @mayorovp ниже, это решается перегрузками с where T: class/struct
        public delegate void ActionRef<T>(ref T obj);
        
        public static T With<T>(this T obj, ActionRef<T> initializer) where T: struct {
            initializer(ref obj);
            return obj;
        }
        // Как и было
        public static T With<T>(this T obj, Action<T> initializer) where T: class {
            initializer(obj);
            return obj;
        }
        
        ...
        
        struct Person { ... }
        
        var manager = GetPerson().With((ref Person o) => {
            o.FirstName = "Иван";
            o.LastName = "Пупкин";
        });


        1. Makeman Автор
          26.04.2018 19:22

          Смотрите сами, что вам больше по душе. ))


  1. dralexnone
    26.04.2018 08:31
    +2

    Наверное я старомоден, но это какой-то садомазохизм: главное в коде не количество строк, а простота. Когда можно понять код, без чтения 10-страничных пояснений… Все началось с элементарного присваивания, продолжилось интуивно понятным JSON, а закончилось галиматьей. Назвать это синтаксическим сахаром, это мягко говоря, преувеличить. Могу представить подобную простынку кода для сопровождения и не дай бог модификации…


    1. Makeman Автор
      26.04.2018 08:37

      Ну, во-первых, никаких десятистраничных пояснений тут нет. :)

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

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


  1. BalinTomsk
    26.04.2018 16:28

    Я частенько использую конструкцию вида:

    int val = (Int32.TryParse( «12», out val)? val: -1 );


    1. Free_ze
      26.04.2018 16:35
      +1

      Как отличить невалидный ввод от -1?


      1. KvanTTT
        27.04.2018 02:31

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


  1. sand14
    26.04.2018 18:53
    +1

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


    А вот как это обходить — вопрос.
    В приведенных способах есть свои недостатки:


    • Используются конструкции языка, которые предназначены для другого, но особенности которых позволяют, пользуясь определенными паттернами, достичь нужного результата. При этом страдает читаемость и масштабируемость (потому что за эти паттернами еще нужно увидеть модель), хотя ради них — читаемости/масштабируемости все и затевалось.
    • Те паттерны, которые выненесены в свои "велосипеды" — получается, их нужно тащить из проекта в проект, обеспечивать версионность и прочее. А ведь потом все равно поймешь, что можно было сделать лучше или универсальнее, или новые конструкции языка появятся, которые можно использовать напрямую, или которые позволят радикально упростить "велосипед".
    • Есть ли уверенность, что подобные вещи, помещаемые в проектах в сборки типа "Common" (коих легион), действительно обеспечивают поддержку всех кейсов использования (на то они и "Common")?
    • И как в командах обеспечивать соблюдение решений типовых ситуаций всегда с использованием велосипедов? (а если половина будет сделано так, другая эдак, то и смысл теряется)

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


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


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


    1. Makeman Автор
      26.04.2018 19:39
      +1

      По многим пунктам с вами согласен.

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

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

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


  1. petuhov_k
    27.04.2018 04:55

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


    1. Makeman Автор
      27.04.2018 13:36
      -2

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


  1. neco
    27.04.2018 16:01

    Заголовок спойлера

    – Долгая история. Все дело в том, что местные программисты пошли по неверному пути. Этот путь называется объектно ориентированный подход в программировании. На самом деле это мина с часовым механизмом в красивой упаковке. В очень красивой упаковке. Как с этим бороться, я не знаю. Упустил момент.


    – Мастер, ближе к делу.


    – Знаешь анекдот, как программист кипятит чайник. Дано: пустой чайник, кран, спички, газовая плита. Программа действий: наполнить чайник водой из-под кран
    а, поставить на плиту, зажечь газ. Ждать, пока закипит чайник. Эта программа оформляется как объект. Второй случай. Все то же самое, но чайник с водой уже стоит
    на плите. Действия программиста: вылить воду из чайника и выполнить предыдущий
    объект.


    – Грустно. А нырнуть внутрь объекта нельзя? Туда, где надо газ зажечь?


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



    (с)


    1. Makeman Автор
      27.04.2018 16:34
      +1

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

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

      Многообразие — важный механизм эволиции. )


      1. neco
        27.04.2018 18:05

        на самом деле, я имел ввиду примерно то, что описано выше вот в этом комментарии: habr.com/post/354278/#comment_10777920

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

        и да, я специально убрал под спойлер.

        p.s. «я слишком стар для всего этого дерьма...» © )


        1. Makeman Автор
          27.04.2018 18:10
          +1

          я слишком стар для...
          Будьте молоды душой! :)