Обновить

Комментарии 13

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

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

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

Да и студия тоже

за 25 лет разработки я могу сказать лишь одно: нет ничего лучшего, чем когда ты видишь код формата int с = a + b;

Ибо когда пройдет десятилетие и бизнес попросит не складывать две суммы затрат, а вычитать, то любой человек, на каком бы языке программирования он ни писал в этом прекрасном 2036 году, наверняка найдёт эту строку и просто поменяет плюс на минус, а не свихнётся от этого творчества фанатов brainfuck'a в виде [0 or 1, <= 2, >= 3]

Больное спасибо за статью, очень круто сделано.

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

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

Да, слева код сложно и витиевато написан. Гораздо читабельнее было бы выделить в нем быстрый путь (известные диапазоны символов) в начале и медленный путь с IsLetter/IsDigit в конце, без множества else и вложенности. Модной конструкцией вы просто замели плохой код под ковер.

В этом вся трагедия C#... Язык сам себя убивает непомерной штамповкой фичей.

да даже просто убрать все else и уже будет лучше чем правый вариант, в который надо вчитываться и голову ломать)

Добрался до редактора, вот что имею в виду:

		if (c is >= '0' && c <= '9')
        	return TokenDigits;
		
		if (c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z')
			return TokenLetters;
		
		if (c <= 128)
			return TokenOther;
		
		if (char.IsLetter(c))
			return TokenLetters;
		
		if (char.IsDigit(c))
			return TokenDigits;
		
		return TokenOther;

Подобный код можно встретить во множестве языков, он легко сканируется сверху вниз глазами даже незнакомого с C# разработчика. Я не настаиваю на том, что он однозначно лучше читается чем switch:

        return c switch
        {
            >= 'a' and <= 'z' => TokenLetters,
            >= 'a' when c < 128 => TokenOther,
            >= 'a' when char.IsLetter(c) => TokenLetters,
            >= 'a' when char.IsDigit(c) => TokenDigits,
            >= 'a' => TokenOther,
            >= 'A' and <= 'Z' => TokenLetters,
            >= 'A' => TokenOther,
            >= '0' and <= '9' => TokenDigits,
            _ => TokenOther,
        };

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

Я что-то попробовал и у меня для net8 нагенерённые курсором тесты показали что ваши варианты медленнее его версии. И только для net10 стали быстрее. Но в итоге всё равно как-то так выглядит лучше:

        if ((uint)(c - '0') <= 9)
            return TokenDigits;

        if ((uint)(c | 0x20) - 'a' <= 'z' - 'a')
            return TokenLetters;

        if (c >= 128)
        {
            if (char.IsLetter(c))
                return TokenLetters;

            if (char.IsDigit(c))
                return TokenDigits;
        }

        return TokenOther;

Дальше только PGO и прочий хардкор

Очень классная статья)
Митя, ты красавчик)

Интересный разбор, спасибо! Но всё же стоит отметить, что полноценный exhaustive matching в .NET пока недостижим. Даже при использовании всех доступных паттернов мы не можем гарантированно покрыть все варианты без явного default/_. Это делает конструкции менее строгими по сравнению с языками, где компилятор действительно проверяет исчерпываемость (например, F# или Rust).

Простой пример: у нас есть разные цветовые модели, заданные как record:

public record Rgb(int R, int G, int B);
public record Cmyk(int C, int M, int Y, int K);

string ToPrintString(object color) => color switch
{
    Rgb rgb   => $"RGB({rgb.R},{rgb.G},{rgb.B})",
    Cmyk cmyk => $"CMYK({cmyk.C},{cmyk.M},{cmyk.Y},{cmyk.K})",
    _         => throw new ArgumentException("Unknown color model")
};

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

Моё решение — использовать паттерн Посетитель:

public abstract class Color
{
    public abstract T Switch<T>(
        Func<Rgb, T> rgbCase,
        Func<Cmyk, T> cmykCase);
}

public record Rgb(int R, int G, int B) : Color
{
    public override T Switch<T>(
        Func<Rgb, T> rgbCase,
        Func<Cmyk, T> cmykCase) => rgbCase(this);
}

public record Cmyk(int C, int M, int Y, int K) : Color
{
    public override T Switch<T>(
        Func<Rgb, T> rgbCase,
        Func<Cmyk, T> cmykCase) => cmykCase(this);
}

// Использование:
string ToPrintString(Color color) =>
    color.Switch(
        rgb  => $"RGB({rgb.R},{rgb.G},{rgb.B})",
        cmyk => $"CMYK({cmyk.C},{cmyk.M},{cmyk.Y},{cmyk.K})");

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

Есть штука, сильно упрощающая иногда жизнь - https://github.com/mcintyre321/OneOf

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Информация

Сайт
tech.kontur.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия
Представитель
Диана