Комментарии 13
Основная проблема применения "недавних улучшений" - это вспомнить о них в процессе работы.
Вроде читал, и не раз, старался запомнить. А потом смотришь, - код опять близок к тому, что SharpLab показывает...
за 25 лет разработки я могу сказать лишь одно: нет ничего лучшего, чем когда ты видишь код формата int с = a + b;
Ибо когда пройдет десятилетие и бизнес попросит не складывать две суммы затрат, а вычитать, то любой человек, на каком бы языке программирования он ни писал в этом прекрасном 2036 году, наверняка найдёт эту строку и просто поменяет плюс на минус, а не свихнётся от этого творчества фанатов brainfuck'a в виде [0 or 1, <= 2, >= 3]
Больное спасибо за статью, очень круто сделано.
Но можно подробнее про подводные камни, когда эти матчинги не стоит применять и, главное бенчмарки (перфоманс, аллокации).
Ну и если кто готовых рулов для editorconfig да прочих стайлкопов подкинет, было бы идеально.
Как раз недавно делал пулреквест, заменил нечитаемый метод на switch expression и получил не только легко поддерживаемый код, но и небольшое улучшение производительности.


Да, слева код сложно и витиевато написан. Гораздо читабельнее было бы выделить в нем быстрый путь (известные диапазоны символов) в начале и медленный путь с IsLetter/IsDigit в конце, без множества else и вложенности. Модной конструкцией вы просто замели плохой код под ковер.
В этом вся трагедия C#... Язык сам себя убивает непомерной штамповкой фичей.
Добрался до редактора, вот что имею в виду:
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 человек
- Местоположение
- Россия
- Представитель
- Диана
Pattern matching в .NET