Pull to refresh

Comments 369

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.

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

IModel GetModel() => new AnyModel();
Спасибо, буду называть правильно.
Работает по аналогии с инициализационными блоками
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 методы.
В случае с 'With' мы декларируем контекст явно

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

Локальная переменная в методе
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}");
}

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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>();
    }
}
Спасибо! Но когда речь идёт о стомиллионных долях секунды, то разница в пять и даже сто раз просто не ощутима на практике, за исключением очень редких случаев, где важна производительность или очень много данных.

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

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

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


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

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

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

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

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

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

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

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


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

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

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

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

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


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

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


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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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


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


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

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

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

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

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

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

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


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

Серьезно?


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

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


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

Аналогично:


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

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


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

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

Меньше кода, меньше оверхеда, лучше читается, никакого WTF. Деконструкция здесь просто не нужна.
Вы, похоже, не уловили сути. Деконструкция и инициализация с With позволяют выводить, объявлять и обновлять значения где угодно в цепочных вызовах без кардинальной смены структуры кода.
string _newName = "abc";

bool UpdateData(Person p) => p.With(
    p.Name.To(out var oldName),
    p.Name = _newName)
)
.Use(() => Debug.WriteLine($"Name changed  from {oldName} to {_newName}"))
.TrySave();

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

bool UpdateData(Person p) => p.With(
    p.Name = _newName)
)
.TrySave();


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

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

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

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


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

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


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

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

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

Только лишь приспасабливаю язык под свои индивидуальные нужды, а заодно исследую нестандартные сценарии использования базовых конструкций.
UFO landed and left these words here
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)) ...

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

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

var (x, y, z) = 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 недалеко...
У такого подхода есть ряд недостатков:

— нельзя без замыканий выводить значения в новые переменные (производить деконструкцию объекта)
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); // получим исходное значение вместо "Иван" 


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Дадада.


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

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


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

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

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

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

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

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

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

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


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


Круто, да.

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

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

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


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

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

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

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

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

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

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


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


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

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


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

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


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

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

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

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

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

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


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


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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

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

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


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

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

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

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

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


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

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

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

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


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

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


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

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

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

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

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


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


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


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

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

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

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

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


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

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

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

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


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


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

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


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

По-видимому, вы оказались правы в том, что 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++;
	}
}
По-видимому, вы оказались правы в том, что await ничего не запускает, даже холодный таск.

Кэп.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

Я не удивлен.

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

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

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


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

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

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

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

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

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

Конечно, мне интересно узнать, какие ещё могут варианты произойти.
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();
}

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


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

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

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

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


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

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

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

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

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


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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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


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


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

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

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

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

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


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

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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

YAGNI


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

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


Код
static void Main(string[] args)
{
    Console.WriteLine("Starting");
    try
    {
        new []{"1", "2"}.ForEach(async i => await Some(i));
        Console.WriteLine("After ForEach");

    }
    catch (Exception e)
    {
        Console.WriteLine("Exception in ForEach: " + e);
        throw;
    }
    Console.WriteLine("After catch");
    Console.ReadLine();
    Console.WriteLine("Finished");
}

static async Task Some(string item)
{
    Console.WriteLine($"{item}: starting");
    await Task.Yield();
    Console.WriteLine($"{item}: after Yield");
    await Task.Delay(3000);
    Console.WriteLine($"{item}: after Delay, throwing");
    throw new InvalidOperationException(item);
}

Результат:


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

Unhandled Exception

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


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


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

Finished

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

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

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

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

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

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

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

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

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


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

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

Для меня WTF-эффект — это когда встречаешь что-то новое, начинаешь с этим разбираться, и вдруг выясняется, что тут очень-очень перемудрили…

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

Я бы мог потратить время на изучение вашего диалекта, но я его потратил на изучение тасков. И, простите, не жалею.
Если вы изучили таски достаточно глубоко, то со стандартными конструкциями вида
items.ToList().ForEach(async i =>
    await DoSomething(i));

items.ToList().ForEach(async i =>
{
    try { await DoSomething(i); }
    catch (Exception e) { Debug.WriteLine(e); }
});

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

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

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

Решений было предложено множество (To, With, Check, Put и другие паттерны), более того, в публикации и вовсе не шло речи про ForEach, но почему-то в расширенных примерах кода зацепились именно за него. :) Если же вам ничего из этого списка не пришлось по вкусу, то пользуйтесь своими решениями.

Многие эти штуки я использую не только в экспериментальных целях, но и на практике, мне они облегчают работу, поэтому решил поделиться с другими людьми, вот и всё, даже никого не призываю их использовать, просто предлагаю материал, над которым как минимум можно поразмышлять и пофантазировать…
> просто предлагаю материал, над которым как минимум можно поразмышлять и пофантазировать…

Что бы что? Это спортивное фантазирование, оно разработку не ускоряет.
Фантазирование развивает мышление, а от мышления зависит эффективность и качество выполняемой работы. Зачем дети в школе и студенты постоянно решают задачи, казалось бы, лишённые практического значения?

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

Фантазирование развивает фантазирование. От фантазирования эффективность не растет.

Детям не дают заданий «придумайте необычный способ решения»
Детям не дают заданий «придумайте необычный способ решения»

Это и отичает способных учеников — неординарный подход к решению различных задач.

А вот не факт совершенно. Есть разные варианты "способных учеников", и не все из них подразумевают "неординарный подход".


(если не секрет, у вас есть педагогическое образование и/или опыт преподавания в учебном заведении?)

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

Это вам так хочется думать.


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

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

Там надо в журнал формальную оценку поставить, варианта «и уж наверняка» там нет

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

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

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

Не, нет никакого "наверняка". Люди иногда забывают простейшие вещи.

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

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

Некомпетентнее (по сравнению с тем, кто ответил на все те же вопросы и этот) — делает.


хотя формально вы могли завалить тест по C# из-за этого вопроса…

Формально — не мог. TPL не является частью C#.

Ключевые слова async и await формально тоже не являются частью C#? Где границы этой формальности? Может, просто не стоит возводить формальность в культ, а прибегать к ней по мере надобности?
Ключевые слова async и await формально тоже не являются частью C#?

Являются.


Где границы этой формальности?

Очевидно, в спецификации.


Может, просто не стоит возводить формальность в культ, а прибегать к ней по мере надобности?

Так это вы зачем-то сказали о "формально завалить тест", не я.

Фантазирование развивает мышление

Можно увидеть ссылки на исследования, подтверждающие это?

Мой жизненный опыт подтверждает это.

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

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


И ещё такой момент — далеко не все области хорошо поддаются исследованиям и статистическому анализу.

Образование поддается и тому, и другому.


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

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


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

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

Ура. Так можно мне ссылку на исследование, показывающее статистически достоверную корелляцию между фантазированием и развитием мышления?

Вы как себе представляете исследование на тему фантазирования?

Собрали дошколят и говорят: «Дети! Мы будем проводить исследование по влиянию фантазирования на развитие вашего мышления! Теперь вы должны фантазировать строго по нашему расписанию на определённые темы на проятяжении пятнадцати лет, а попутно мы будем проводить разные-разные тесты… Для убедительности одну подгруппу мы ограничим в фантазировании, а другую будем стимулировать к этому… и т. д. и т. п.»

Или, может, сделать опросник для взрослых людей?
Как часто вы фантазированли в возрасте до пяти лет? До десяти?.. В какие игры играли? В какие кружки ходили? Сколько времени уделяли этим занятим? Какие книжки прочитали? Какие мультики посмотрели и по сколько раз? С кем дружили? Как часто гуляли?..

Или лучше возьмём близнецов, которых разлучили в детстве! Гениально! Изучим вляние среды на развитие близнецов…

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

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

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

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

Значит, ваше утверждение выше ("Фантазирование развивает мышление") ничем не подтверждено. О чем и речь.


Если с ребёнком достаточно заниматься, учить его, развивать, играть, стимулировать фантазию, то всё это способствует развитию его мышления!

Может быть. Все вместе. Или только сам факт "заниматься с ребенком". Или вообще восьмой фактор, который вы не учли.


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

… а если у человека (точнее, его семьи) больше денег — тоже шансов, что он вырастет образованный и развитый больше. Делаем вывод, что деньги развивают мышление?

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

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

Конечно, здравый смысл и житейский опыт подсказывают, что второй вариант более благоприятный. Вы можете спорить до умопомрачения, что «здравый смысл» часто не работает, а «житейский опыт» обманывает, искать исследования на тему влияния детских кружков на мышление и прочее, и прочее… Но что это вам даёт? Уверенность? В чём?
Деньги — это косвенный и комплексный фактор фактор.

Фантазия — это тоже косвенный и комплексный фактор.


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

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


Но что это вам даёт? Уверенность? В чём?

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


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

> Но что это вам даёт? Уверенность? В чём?

А вам что даёт форсинг часто не работающего «здравого смысла»?
то со стандартными конструкциями вида [...ForEach...] у вас больших проблем возникнуть не должно.

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


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

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


Конечно, на это нужно потратить время, есть риск разочароваться,

… и что делать в этом случае?

Нет, WTF-эффект — это когда читаешь код, и задаешься вопросом "WTF?!". Все.


я новичок и увидел впервые лямбды, конечно, первый вопрос, что это вообще такое?

Есть разница между вопросом "что это такое" и "какого хрена".


По сути, согласно вашему определению, любой код обладает таким эффектом, например,

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

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

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

Вас устраивает и соответствует вашему интуитивному пониманию.


Поэтому никаких обманутых ожиданий такой код у меня не вызывает.

Это неудивительно: его вы написали. Поэтому вас все устраивает, и все соответствует вашей интуиции. То, что ваша интуиция при этом не соответствует интуиции других людей (и, в том числе, реальному поведению системы в ряде ситуаций) — это вас не волнует.


Мне вообще не ясно, чем вам так запал в душу этот пример…

Тем, что он противоречит хорошему тону работы с асинхронией как я его понимаю.

Решил сделать так: перименовал интерфейсные методы по принципу TryLoadAsync

1. Try — не генерирует исключений и возвращает bool
2. Async — асинхронный

Надеюсь, теперь будет понятнее.

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


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

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

Состояние определено после вызова Expose — остальное не имеет значения в данном контексте.

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

Именно это и пугает. Я бы понял, если бы у вас был явный message passing, но у вас же его нет.


Состояние определено после вызова Expose — остальное не имеет значения в данном контексте.

… не имеет значения, но написано. Тоже проблема.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Documents.ForEach(d => d.Expose()).ForEach(async d => await d.Load());
В конкретном случае главной вью-модели вообще не важен результат загрузки. Но, например, важен результат закрытия: в самом конеце файла, 81 строка, метод Close.

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

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


await Documents
  .Select(d => {
    d.Expose();
    return d.Load();
  })
  .WaitAll();
Спасибо. Если требуется по окончании обработки запустить метод и запуск произойдет в другом потоке?
 .ContinueWith((a) =>Application.InvokeOnMain(()=> method())

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

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

На счет правильности не уверен, но «выход» нашел в виде костыля с введением нового свойства bool, где один поток меняет его а другой подхватывая запускает метод…
Где можно почитать подробнее об организации подобного диспетчера?
Если у вас WaitAll — кастомное расширение, то лучше дать ему название AwaitAll, ибо оригинальный WaitAll — это синхронный вызов
public static void WaitAll(
	params Task[] tasks
)
> Конечно, я бы мог разбить его на более мелкие части, но в собранном виде мне нравится больше.

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

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

Так вы говорите
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()

?

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

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

Думаю, так будет лучше видна разница в логике
Documents.CollectionChangeCompleted += (sender, args)
{
    var document = args.NewItems?
        .Cast<ADocument>().LastOrDefault();
    if (document != null) ActiveDocument = document;
}

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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

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


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

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

Многое в C# можно переписать без дополнительного синтаксического сахара: автоматические свойства, создание делеготов, лямбда-выражения… те же инициализационные блоки, например,
var p = new Person();
p.FirstName = "Иван";
p.LastName = "Пупкин";

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

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

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

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

Конечно, было бы здорово получить синтаксический сахар на нативном уровне вроде
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)

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


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

Работает по аналогии с вызовом обычных методов
GetPerson()?.MethodA().MethodB();
Вот вы отвечаете «как работает», а вопрос был «а нахрена такое?»

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

Вот и вопрос, то ли у меня задачи не такие, то ли это ваши эксперименты ради экспериментов.
И я там тоже не вижу конструкции вида GetPerson()?.With(..).DoSomething();

То есть, у вас тоже нет живого примера того, что «было бы здорово получить» — в реальной жизни это не нужно. И, соответственно, в C#, ради такого, сахар добавлять не надо.
Как вы относитесь к потенциальной фиче (частному случаю Check паттерна, родственного With)?

/* true|false */
GetModel() is {} m // null check

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

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


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

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

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

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