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

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

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

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

За дело!

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

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


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

Вы можете помочь и перевести немного средств на развитие сайта

Теги:

.net, c#


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

  1. lair
    /#10775072 / +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.

    • petuhov_k
      /#10775310

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

      • lair
        /#10775384

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

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

        • Makeman
          /#10775436

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

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

          • lair
            /#10775440

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

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

            • Makeman
              /#10775512

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

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

              • lair
                /#10775520

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

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


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


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

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

                • Makeman
                  /#10775564

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

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

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

                  • lair
                    /#10775574

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

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

      • Makeman
        /#10775426

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

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

        • lair
          /#10775444

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

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

          • Makeman
            /#10775466

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

            IModel GetModel() => new AnyModel();

    • Makeman
      /#10775386

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

      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 методы.

      • lair
        /#10775400

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

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

        • Makeman
          /#10775446

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

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

          • lair
            /#10775454

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

            • Makeman
              /#10775474

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

              • lair
                /#10775478

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

                • Makeman
                  /#10775542

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

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

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

                  • lair
                    /#10775566 / +1

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


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

                    • Makeman
                      /#10775602

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

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

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

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

                      • lair
                        /#10775610

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

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


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

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

                        • Makeman
                          /#10775634

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

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

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

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

                          • lair
                            /#10775636

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

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


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

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

                            • Makeman
                              /#10775652

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

                              • lair
                                /#10775718

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

                                • Makeman
                                  /#10775772 / -1

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

                                  • lair
                                    /#10775786

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

                                    • Makeman
                                      /#10775824 / -1

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

                          • nporaMep
                            /#10776480

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

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

                            • Makeman
                              /#10776624

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

                              • nporaMep
                                /#10776724

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

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

                                • Makeman
                                  /#10778144

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

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

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

  2. forcewake
    /#10775598

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

    • lair
      /#10775606

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

    • Makeman
      /#10775612

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

      • petuhov_k
        /#10777770

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

        • Makeman
          /#10777906

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

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

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

          • lair
            /#10777920

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

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

            • Makeman
              /#10778002

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

              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', вызов этот занимает порядка одной стомиллионной секунды на среднем компьютере. Уверен, что даже для мобильных устройств эта цифра будет вполне адекватная.

              • lair
                /#10778014

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

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

                • Makeman
                  /#10778044 / -1

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

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

                  • lair
                    /#10778132 / +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>();
                        }
                    }

                    • Makeman
                      /#10778154

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

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

                      • lair
                        /#10778166

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

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


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

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

                        • Makeman
                          /#10778196 / -1

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

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

                          • lair
                            /#10778414

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

                            • areht
                              /#10778426

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

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

                              • lair
                                /#10778432

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


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

                            • Makeman
                              /#10778516

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

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

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

                              • lair
                                /#10778524 / +1

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

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


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

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


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

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

                                • Makeman
                                  /#10778548

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

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

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

                          • petuhov_k
                            /#10778626 / +1

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


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

                            • Makeman
                              /#10779388

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

                      • petuhov_k
                        /#10778622

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

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

                        • Makeman
                          /#10779384 / -2

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

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

                          :)

  3. SlavniyTeo
    /#10775644 / +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)) ...

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

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

    • Makeman
      /#10775666

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

      var (x, y, z) = 1;

  4. Free_ze
    /#10776008 / +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 недалеко...

    • Makeman
      /#10776154

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

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

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


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

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

      • mayorovp
        /#10776354

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

      • areht
        /#10776520

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


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

        • Makeman
          /#10776654

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

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

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

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

          • areht
            /#10777118

            — expression-bodied методах

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

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

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

            • Makeman
              /#10777956

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

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

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

              • Free_ze
                /#10777986 / +2

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

                • Makeman
                  /#10778062 / -1

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

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

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

                  • Free_ze
                    /#10778168 / +1

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

                    • Makeman
                      /#10778206

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

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

              • areht
                /#10777998 / +2

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

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

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

                • Makeman
                  /#10778030

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

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

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

                  • lair
                    /#10778042 / +1

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

                    Дадада.


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

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


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

                    • Makeman
                      /#10778076 / -2

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

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

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

                      • lair
                        /#10778136

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

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

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

                        • Makeman
                          /#10778158

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

                          • lair
                            /#10778170 / +3

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


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


                            Круто, да.

                            • Makeman
                              /#10778212 / -1

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

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

                              • lair
                                /#10778416

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

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


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

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

                                • Makeman
                                  /#10778530

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

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

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

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

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

                                  • lair
                                    /#10778536 / +2

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

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


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


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

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


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

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


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

                                    • Makeman
                                      /#10778558

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

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

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

                                      • lair
                                        /#10779758

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

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

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


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


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

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

                                        • Makeman
                                          /#10779886 / -1

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

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

                                          • lair
                                            /#10779888

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

                                            • Makeman
                                              /#10779898

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

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

                                              • lair
                                                /#10779928

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

                                                • Makeman
                                                  /#10779960

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

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

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

                                                  • lair
                                                    /#10779980

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


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

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

                                                    • Makeman
                                                      /#10780018

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

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


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

                                                      • lair
                                                        /#10780040 / +1

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


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

                                                        • Makeman
                                                          /#10780826

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

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

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

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

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

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

                                                          • lair
                                                            /#10780834 / +1

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

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


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

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

                                                            • Makeman
                                                              /#10780838

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

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

                                                              • lair
                                                                /#10780842 / +1

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

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


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

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

                                                                • Makeman
                                                                  /#10780858 / -1

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

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


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

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

                                                                  • lair
                                                                    /#10780864 / +2

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

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


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

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

                                                                    • Makeman
                                                                      /#10780880 / -2

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

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

                                                                      • lair
                                                                        /#10780888 / +2

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

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


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


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


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

                                                                        • Makeman
                                                                          /#10780906

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

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

                                                                          • lair
                                                                            /#10780908

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

                                                                            • Makeman
                                                                              /#10780920

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


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

                                                                              • lair
                                                                                /#10780924

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

                                                                        • Szer
                                                                          /#10781558 / +1

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

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


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


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

                                                                          • lair
                                                                            /#10781576 / +1

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


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

                                                                            • Makeman
                                                                              /#10781618

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

                                                                              • lair
                                                                                /#10781622

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

                                                                                Кэп.

                                                                    • lair
                                                                      /#10780912

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

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

                                                                      • Makeman
                                                                        /#10780926

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

                                                                        • lair
                                                                          /#10780928

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

                                                                          • Makeman
                                                                            /#10780932 / -2

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

                                                                            • mayorovp
                                                                              /#10780986

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

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

                                                                              • Makeman
                                                                                /#10781286

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

                                                                                • mayorovp
                                                                                  /#10781292

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

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

                                                                                  • Makeman
                                                                                    /#10781304

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

                                                                                    • mayorovp
                                                                                      /#10781306

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

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

                                                                                      • Makeman
                                                                                        /#10781400

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

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

                                                                                        • mayorovp
                                                                                          /#10781410

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

                                                                                          • lair
                                                                                            /#10781426 / +1

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

                                                                                          • Makeman
                                                                                            /#10781436

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

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

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

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

                                                                                            • lair
                                                                                              /#10781474

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

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


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

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


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

                                                                                              Я не удивлен.

                                                                                              • Makeman
                                                                                                /#10781498

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

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

                                                                                                • lair
                                                                                                  /#10781508

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

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


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

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

                                                                                                  • Makeman
                                                                                                    /#10781546

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

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

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

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

                                                                                                    • lair
                                                                                                      /#10781568

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

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

                                                                                                      • Makeman
                                                                                                        /#10781624

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

                                                                                                        • lair
                                                                                                          /#10781632 / +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();
                                                                                                          }
                                                                                                          

                                                                                                          • lair
                                                                                                            /#10781662 / +1

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


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

                                                                                                          • Makeman
                                                                                                            /#10781756

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

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

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

                                                                                                            • lair
                                                                                                              /#10781780

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

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


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

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

                                                                                                              • Makeman
                                                                                                                /#10781796

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

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

                                                                                                                • lair
                                                                                                                  /#10781802

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

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


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

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

                                                                                                                  • Makeman
                                                                                                                    /#10781840

                                                                                                                    Чтобы уж наверняка, я переименовал свои методы как 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());
                                                                                                                    }


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

                                                                                                                    • lair
                                                                                                                      /#10781850

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

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

                                                                                                                      • Makeman
                                                                                                                        /#10781878

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

                                                                                                                        • lair
                                                                                                                          /#10781894

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

                                                                                                                          • Makeman
                                                                                                                            /#10781932

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

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

                                                                                                                            • lair
                                                                                                                              /#10781944

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

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


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

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

                                                                                                                              • Makeman
                                                                                                                                /#10781988

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

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

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

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

                                                                                                                                • lair
                                                                                                                                  /#10781990

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

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


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

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

                                                                                                                                  • Makeman
                                                                                                                                    /#10782012

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

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

                                                                                                                                    • lair
                                                                                                                                      /#10782020

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

                                                                                                                                      • Makeman
                                                                                                                                        /#10782052

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

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

                                                                                • lair
                                                                                  /#10781418

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

                                                                            • lair
                                                                              /#10781416

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

                                                              • areht
                                                                /#10780898 / +4

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

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

                                  • lair
                                    /#10778544

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

                                    • Makeman
                                      /#10778566 / -1

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

                                      • lair
                                        /#10779762

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

                                        • Makeman
                                          /#10779874

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

                                          • lair
                                            /#10779878

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

                                            • Makeman
                                              /#10779894

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

                                              • lair
                                                /#10779924

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

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

                                                • Makeman
                                                  /#10779976

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

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

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

                                                  • lair
                                                    /#10779982

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

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

                                                    • Makeman
                                                      /#10780024

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

                                                • Zam_dev
                                                  /#10780086

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

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

                                                  • Makeman
                                                    /#10780134

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

                                                    • Zam_dev
                                                      /#10780160

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

                                                  • lair
                                                    /#10780156

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


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

                                                    • Zam_dev
                                                      /#10780180

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

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

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

                                                      • lair
                                                        /#10780184

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

                                                        • Zam_dev
                                                          /#10780238

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

                                                    • Makeman
                                                      /#10780188

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

                                                      public static void WaitAll(
                                                      	params Task[] tasks
                                                      )

                  • areht
                    /#10778142

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

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

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

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

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

                    ?

                    • lair
                      /#10778148

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

                    • Makeman
                      /#10778174

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

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

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

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

                      • lair
                        /#10778184

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

                        • Makeman
                          /#10778216

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

                          • areht
                            /#10778236 / +2

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

                          • lair
                            /#10778418 / +2

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

                            • Makeman
                              /#10778570

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

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

                              • areht
                                /#10778676

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

                                • Makeman
                                  /#10779622

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

                                  • areht
                                    /#10780376 / +1

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

                              • lair
                                /#10779796

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

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


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

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

                                • Makeman
                                  /#10779902

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

                                  • lair
                                    /#10779934

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

                                    • Makeman
                                      /#10780000 / -1

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

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

                                      • lair
                                        /#10780012

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

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

              • lair
                /#10778018 / +1

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

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


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

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

      • Deosis
        /#10776592

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


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

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

        • Makeman
          /#10776634

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

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

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

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

          • Deosis
            /#10778738

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

            • Makeman
              /#10779474

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

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

              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)

              • areht
                /#10779636 / +1

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


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

                • Makeman
                  /#10779918

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

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

                  • areht
                    /#10780202 / +2

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

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

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

                    • Makeman
                      /#10780832

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

                      • areht
                        /#10780938

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

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

                        • Makeman
                          /#10781298

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

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

                          • lair
                            /#10781430

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


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

                            • Makeman
                              /#10781470

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

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

                              • lair
                                /#10781472

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

                                • Makeman
                                  /#10781482 / -1

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

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

                                  • lair
                                    /#10781484

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

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

                                    • Makeman
                                      /#10781516 / -1

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

              • lair
                /#10779806

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

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


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

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

                • Makeman
                  /#10779854 / -1

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

                • Makeman
                  /#10779868

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

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

                  • lair
                    /#10779876

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

                    • Makeman
                      /#10779912

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

                      • lair
                        /#10779936

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

                        • Makeman
                          /#10780036

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

                          • lair
                            /#10780042

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

                            • Makeman
                              /#10780044

                              Да, на ходу.

                              • lair
                                /#10780048 / +1

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

      • Free_ze
        /#10776852

        нельзя без замыканий выводить значения в новые переменные (производить деконструкцию объекта)
        Это я даже назвал плюсом) Польза появления новых переменных по умолчанию сомнительна, кмк.
        нет возможности удобно модифицировать структуры
        Как уже написал @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 = "Пупкин";
        });

        • Makeman
          /#10778086

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

  5. dralexnone
    /#10776658 / +2

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

    • Makeman
      /#10776670

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

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

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

  6. BalinTomsk
    /#10777632

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

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

    • Free_ze
      /#10777654 / +1

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

      • KvanTTT
        /#10778574

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

  7. sand14
    /#10778026 / +1

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


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


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

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


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


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

    • Makeman
      /#10778126 / +1

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

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

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

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

  8. petuhov_k
    /#10778634

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

    • Makeman
      /#10779498 / -2

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

  9. neco
    /#10779862

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

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


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


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


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


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



    (с)

    • Makeman
      /#10779946 / +1

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

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

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

      • neco
        /#10780186

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

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

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

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

        • Makeman
          /#10780200 / +1

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