Pull to refresh

Comments 24

Это как то стрёмно =)

У DelayedOk и DelayedError нет никакой информации о втором возвращаемом типе и в итоге они могут приводиться так же неявно к любой комбинации и своего типа и любого другого:

Result<string, int> example = new DelayedOk<string>("123");

Не знаю, правда, мешает ли это хоть в чём то, на простых примерах в статье - да, смотрится удобно. Стоит ли оно того - хз =)

Если это генерируемый код - вроде какая разница, генерировать с явным типом или оставлять выведение на компилятор. Если пишется руками - то IDE давно уж отлично подсказывает return new Result<string, string> как раз зная возвращаемый результат =)

UPD: плюсик статье воткнул, в целом забавно всё равно =)

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

public readonly struct DelayedOk<T>
{
    public T Value { get; }

    private DelayedOk(T value) => Value = value;

    public static implicit operator DelayedOk<T>(T value) => new(value);
}

public readonly struct DelayedError<T>
{
    public T Value { get; }

    private DelayedError(T value) => Value = value;

    public static implicit operator DelayedError<T>(T value) => new(value);
}

public static class Result
{
    public static DelayedOk<TOk> Ok<TOk>(TOk ok) => ok;

    public static DelayedError<TError> Error<TError>(TError error) => error;
}

Тогда уж проще и правильнее конструкторы было сделать внутренними internal.

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

А разве тем кто читает IDE не подсказывает тип?

IDE-то подсказывает, но читать приходится не только в IDE. Например, с Github уже сложнее, а с чем-нибудь типа Far или блокнота — ещё сложнее.

А если закрытыми глазами, так вообще очень трудно!

Явное указание типов при чтении мешает.
В коде

var employees = GetEmployeesByDepartment(departmentId);

прекрасно видно бизнес-смысл переменной -- список сотрудников. Какая мне разница, что там за тип: List<Employee>, Employee[] или вообще Dictionary?

Вот если потребуется глубокое погружение в код с целью его модификации (что уже не чтение, а изучение), то тип подскажет IDE.

Мешает для простых типов. У нас в команде есть правило, что для простых значимых типов, а также string, DateTime, Guid и некоторых других нельзя использовать var. Классический пример - есть какое-то математическое выражение: var result = Какая-то формула с вызовом несколькими вызовами Math.*. На рецензировании тяжело определить какой тип в итоге у result и совпадает ли он с предполагаемым автором. Ошибки округления тут точно будут.

Какая мне разница, что там за тип: List, Employee[] или вообще Dictionary?
Хороший пример: в нем, если нет разницы, то явное указание IList<Emloyee> (или вообще IEnumerable<Emloyee>) вместо var подскажет, как именно автор кода собирается с этим работать дальше, т.е. какие именно методы следует ожидать увидеть в дальнейшем. Ну а Dictionary тут вообще не сильно похоже на два предыдущих.
PS Для меня обычно чтение=изучение, не обязательно с целью модификации: иногда надо просто понять, что именно делает код, потому как документация не идеальна.
С другой стороны, c var возможны внезапные сюрпризы. Например, если мы видим в коде настройки контейнера сервисов на ASP.NET что-то типа serviceCollection.что-то-там.AddOptions() (где serviceCollection имеет тип IServiceCollection), то занчение выражения остается имеющим тип IServiceCollection, а если мы видим очень похожее на него выражение serviceCollection.что-то-там.AddOptions<T>() то внезапно тип меняется на OptionsBuilder<T>. Сюрприз!

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

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

надо просто понять, что именно делает код

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

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

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

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

Идея статьи интересная, и возможно даже имеет место применения, но конкретные примеры я бы скорее всего не использовал. Option<T> для случаев когда T - структура полностью заменяется Nullable<T>

int? Parse(string val)
    => int.TryParse(val, out var result)
        ? result
        : null;

Result<TOk, TError> уже выглядит поинтересней, но конкретно финальная реализация немного смущает тем, что static class Result начинает возвращать не Result<TOk, TError>, а DelayedOk, что не всегда позволяет его использовать как create-метод, так как нельзя будет сделать например такое

var result = Result.Ok(1);
result = Parse("1");

а нужно будет явно объявлять

Result<int, int> result = Result.Ok(1);
result = Parse("1");

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

Проблема в том, что тернарный оператор в C# не приводит ветви к "общему знаменателю"

Начиная с C# 9.0 явное приведение типа не требуется.

Как раз в конфигурации с DelayedResult<T> код:

public static Result<int, string> ParseToResult(string input)
{
    return int.TryParse(input, out var value)
        ? Result.Ok(value)
        : Result.Error("Invalid value");
}

не скомпилится
Когда же мы разделяем отложенную инициализацию на два состояния DelayedOk и DelayedError, неявное приведение работает:

Конечно соберётся. SharpLab
Или я не понял каким должен быть код.

Да, вы правы, отметил это в статье. Однако, проблема всё же возникнет при использовании Result<T, T>. В этом основной поинт)

начинаешь читать
"Мне по-настоящему нравитЬся больше чего-либо в разработке ПО делать фреймворки ..."
прекращаешь читать

Да нет, такое принято выделять прямо в тексте и жать Ctrl+Enter: в личко оно напишет само.
Проверено: обычно помогает.
Sign up to leave a comment.

Articles