.NET Разработчик
6.57K subscribers
443 photos
4 videos
14 files
2.13K links
Дневник сертифицированного .NET разработчика. Заметки, советы, новости из мира .NET и C#.

Для связи: @SBenzenko

Поддержать канал:
- https://boosty.to/netdeveloperdiary
- https://patreon.com/user?u=52551826
- https://pay.cloudtips.ru/p/70df3b3b
Download Telegram
День пятьсот одиннадцатый. #CSharp9
C# 9: Упрощённая Проверка Параметров на Null
Эта функция конкурировала с несколькими другими предложениями: атрибуты, флаги компилятора и т.п. Все они были отброшены в пользу узкоспециализированного предложения, которое уменьшает объем кода проверки параметра на null до одного символа.

Оператор ! может быть расположен после любого идентификатора в списке параметров, что заставит компилятор C# выдать стандартный код проверки на null для этого параметра:
void M(string name!) {…}
будет скомпилировано как:
void M(string name) {
if (name is null)
throw new ArgumentNullException(nameof(name));

}

Правила довольно очевидны:
1. Проверки на null вводятся в начале функции перед любым другим кодом и выполняются в том же порядке, что и параметры в сигнатуре функции.
2. Выполняется именно проверка на равенство null, игнорируя любые перегрузки оператора ==.
3. В конструкторе проверка произойдет после вызова конструктора базового класса. В Microsoft рассматривали возможность проверки параметров перед вызовом конструктора базового класса. Это всегда было разрешено в .NET и, возможно, это предпочтительнее. Тем не менее, синтаксис C# не позволяет этого, и было решено не генерировать код, который не может быть воссоздан без использования этой функции.
4. В итераторе проверка всегда выполняется при первоначальном вызове.
5. Ошибки компиляции возникнут в случаях:
- проверки параметра, который не может быть null (например, struct, unmanaged),
- проверки параметра метода без реализации (например, абстрактного или частичного метода, делегата, метода интерфейса),
- проверки out параметра.
6. Предупреждения компилятора возникнут в случаях:
- Проверки явно обнуляемого параметра (string? x!). Предположительно это не приводит к ошибке, чтобы позволить подклассу быть более строгим, чем базовый класс.
- Проверки необязательного параметра со значением null по умолчанию.

Предполагаются следующие сценарии для дженериков:
void M<T>(T value!) { } // OK
void M<T>(T value!) where T : struct { } // ошибка
void M<T>(T value!) where T : unmanaged { } // ошибка
void M<T>(T value!) where T : notnull { } // OK
void M<T>(T value!) where T : class { } // OK
void M<T>(T value!) where T : SomeStruct { } // ошибка
void M<T>(T value!) where T : SomeClass { } // OK

Недостаток этого подхода в том, что он не поддерживает проверку свойств, поскольку параметр value не задаётся явно, а только подразумевается. Пользователи предложили обходной путь, в котором оператор ! применяется к ключевому слову set:
public Foo Foo {get; set!;}
public Bar Bar {
get { return bar; }
set! { bar = value; DoSomethingElse(); }
}

Источник: https://www.infoq.com/news/2020/06/CSharp-9-Null/
День пятьсот тридцать девятый. #ЧтоНовенького #CSharp9
C#9: Новые Ключевые слова and, or и not для Сопоставления по Шаблону
Хотя это может звучать как первоапрельская шутка, в C#9 хотят добавить and, or и not в список ключевых слов для использования в сопоставлении по шаблону.

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

В этом и проблема. Если вы работаете с логическими значениями, то операторы && и || будут неоднозначными. Компилятор не сможет определить, относятся они к значениям или к шаблонам. Чтобы проиллюстрировать эту идею, рассмотрим дизъюнктивный шаблон:
if (myBool is true or false)
Это будет интерпретировано как «истина, если myBool равно true или если myBool равно false».

Если бы мы использовали операторы && и || для объединения шаблонов, получился бы следующий код:
if (myBool is true || false)
Но это буквально означает «истина, если myBool равняется результату логического выражения (true или false)», что можно упростить до «истина, если myBool равняется true». А это совершенно не то, что мы хотели бы получить в дизъюнктивном шаблоне.

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

Источник: https://www.infoq.com/news/2020/07/CSharp-And-Or-Not/
День пятьсот сорок второй. #ЧтоНовенького #CSharp9
C#9: Операторы Диапазона в Конструкциях Switch и Сопоставлениях по Шаблону

С момента первого выхода C# разработчики жаловались на отсутствие операторов диапазона в конструкциях switch. Как часть улучшений сопоставлений по шаблону в C#9, это ограничение было устранено.
Следующие шаблоны будут разрешены после ключевых слов case или is:
- < константа
- > константа
- <= константа
- >= константа

Шаблоны всё ещё ограничены только константами. Это становится проблемой, когда вы имеете дело с датой, временем или другими аналогичными структурами, поскольку они не имеют константного представления в C#.

В обсуждении предложения на GitHub Нил Гафтер написал по этому поводу:
«Смысл этой функциональности был в расширении сопоставления по шаблону. В частности, ограничение только константами сделано, чтобы компилятор мог проанализировать и оптимизировать весь набор операций сопоставления по шаблону. Если компилятор не знает значения, с которым он сопоставляет, он не сможет этого сделать. В конце концов, чем плохи обычные выражения?»

Мариуш Павельски отвечает, почему ограничение может стать проблемой:
«Я просто думаю, что будет много людей, которые захотят использовать ключевые слова is, and и or просто как ещё один способ написания логических выражений. Ещё одна небольшая функция C#, которая делает код более лаконичным. Они не будут знать, что это часть большой функциональности сопоставления по шаблону, которая «была разработана для работы с константами». Они будут просто сбиты с толку, когда вместо константы будут использовать имя переменной и получат ошибку «CS0150: A constant value is expected» (Ожидается константное значение).»

Как бы то ни было, операторы and, or и not также могут быть использованы в сочетании с диапазонами. Например,
bool IsLetter(char c) =>
c is >= 'a' and <= 'z' or >= 'A' and <= 'Z';


Это немного трудно читать, поэтому в выражениях можно использовать круглые скобки:
bool IsLetter(char c) =>
c is (>= 'a' and <= 'z') or (>= 'A' and <= 'Z');


Источник: https://www.infoq.com/news/2020/07/CSharp-9-Range-Patterns/
День пятьсот сорок пятый. #ЧтоНовенького #CSharp9
C#9: Незначительные Улучшения в Лямбдах
В C#9 лямбда-функции получат два обновления. Ни одно из них не изменит способ написания кода, но они помогут разработчику прояснить свои намерения.

Дискард-Параметры Лямбда Функций
Позволят разработчикам явно указать, что некоторые параметры не нужны, что предотвратит ошибочные предупреждения компилятора о неиспользуемых параметрах. Это может быть полезно, например, в обработчиках событий, когда не нужны параметры sender и eventArgs:
button1.Click += (s, e) => ShowDialog();
Замена параметров, как показано ниже, позволит явно указать, что они не используются:
button1.Click += (_, _) => ShowDialog();
При необходимости могут быть использованы типы:
var handler = (object _, EventArgs _) => ShowDialog();

Статические Анонимные Функции
Будут использоваться, чтобы обозначить, что лямбда или анонимная функция не может захватывать локальные переменные (включая параметры). Вот пример из оригинального предложения функционала:
int y = 10;
someMethod(x => x + y);
//захватывает 'y', приводя к неявному выделению памяти

Проблема в том, что это неявно приводит к увеличению времени жизни локальной переменной, т.к. анонимная функция может существовать дольше, чем окружающий её метод.
Чтобы избежать случайного захвата любого локального состояния при предоставлении лямбда-функций в качестве аргумента метода, предлагается добавить к лямбда-объявлению ключевое слово static. Это сделает лямбда-функцию похожей на статический метод, который не может захватывать локальные объекты и не имеет доступа к this или base:
int y = 10;
someMethod(static x => x + y); //ошибка!

Чтобы исправить эту ошибку, переменная y должна быть изменена на константу или статическое поле:
const int y = 10;
someMethod(static x => x + y);

Вот основные правила для статических анонимных функций:
- может ссылаться на статические члены из окружающего кода;
- может ссылаться на определения констант из окружающего кода;
- не может захватывать состояние из окружающего кода, т.е. локальные объекты, параметры и this из окружающего кода недоступны;
- не может ссылаться на экземплярные члены окружающего её класса;
- nameof() внутри статической анонимной функции может ссылаться на локальные объекты, параметры, this или base из окружающего кода.

Источник: https://www.infoq.com/news/2020/07/CSharp-Lambdas/
День пятьсот шестьдесят седьмой. #ЧтоНовенького #CSharp9
Первичные Конструкторы в C#9
В C#9 представлен новый упрощённый способ создания классов. Большинство конструкторов очень избыточны, поскольку аргументы конструктора чаще всего используются только для инициализации свойств с тем же именем. Теперь есть способ избавиться от этого лишнего кода. Сейчас мы пишем что-то вроде этого:
public class Person {
public Person(string firstName, string lastName) {
this.FirstName = firstName;
this.LastName = lastName;
}
public string FirstName { get; }
public string LastName { get; }
}
8 строк кода (11, если переносить открывающую скобку на новую строку), только чтобы создать простой POCO (Plain Old CLR Object). Теперь это можно сделать в одну строку:
public record Person(string firstName, string lastName);
На момент написания первичные конструкторы доступны только для записей. Классы и структуры пока не поддерживаются, но должны быть реализованы в окончательной версии.

Инициализаторы
Первичные конструкторы также используют новые init-свойства. Это означает, что мы можем установить значение свойства с помощью инициализатора объекта:
var p = new Person("Miguel", "Bernard") { lastName = "test" };
Это работает, как и ожидалось. После выполнения конструктора значение "Bernard" заменяется на "test". Однако вы не можете использовать инициализатор объекта для задания обязательных свойств первичного конструктора:
// Это не скомпилируется
var p = new Person("Miguel") { lastName = "test" };

Значения по умолчанию
В первичный конструктор можно передавать значения по умолчанию:
public record Person (string firstName, string lastName = "lastName");
var p = new Person("Miguel");

Я вижу большой потенциал в этой новой функции, особенно для POCO/DTO и простых объектов. Удаление всего этого бесполезного шаблонного кода сократит издержки при создании новых типов и упростит кодовую базу.

Источник: https://blog.miguelbernard.com/c-9-0-primary-constructors/
👍2
День пятьсот шестьдесят восьмой. #ЧтоНовенького #CSharp9
Подробнее про Записи в C#9
Записи в C#9 помогут нам создавать неизменяемые типы, которые очень полезны в большой распределенной архитектуре. Поскольку запись ведет себя иначе, чем класс или структура, Microsoft ввели новое ключевое слово record.
public record Chicken { public string Name {get;init;} }
var c = new Chicken { Name = "test" };
// c.Name = "t"; - изменение значения недопустимо

Первичные конструкторы
Ещё интереснее использование записей с первичными конструкторами:
public record Chicken(string name);
Фактически есть большой соблазн использовать только первичные конструкторы для объявления записей, чтобы отличать их от объявления классов.

With
С введением записей добавится ещё одно ключевое слово with, которое позволит «клонировать» запись, изменив только некоторые свойства.
var c2 = c with { Name = "myNewName" };

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

Деконструкция
Записи также поддерживают деконструкцию, что позволяет неявно преобразовать запись в кортеж, содержащий все свойства:
public record Decons(int qty, string name, DateTime time);
var r = new Decons(42, "name", DateTime.Now);
var(x, y, z) = r;
Того же результата можно достичь с помощью метода Deconstruct(), доступного для всех типов записей.

Наследование
Поскольку C# - объектно-ориентированный язык, наследование является частью ООП, и его следует поддерживать. Должно быть, это самая серьёзная проблема, которую пришлось преодолеть команде C#, - сделать записи частью языка, сохранив при этом обратную совместимость.
public abstract record Food (int Cal);
public record Milk(int C, double Fat) : Food(C);
var m = new Milk(1, 3.25);
При этом можно обратиться как к m.C, так и к m.Cal (они будут равны 1). Но что, если объявить свойства одинаково?
public record Milk(int Cal, double Fat) : Food(Cal);
У вас останется одно свойство Cal, причём оно будет наследовано от Food. Думаю, это наиболее логичное поведение.
Заметьте, что невозможно смешивать классы и записи в цепочке наследования (наследовать класс от записи или наоборот). Однако, вполне возможно, чтобы запись реализовывала интерфейс:
public interface IRecord { }
public record RecordType : IRecord { }

Ограничения обобщений
На момент написания невозможно использовать ключевое слово record для ограничения обобщения:
public void Method<T>(T t) where T : record
Надеюсь, к моменту выхода 9й версии это будет поддерживаться.

Источник: https://blog.miguelbernard.com/c-9-0-records/
👍3
День шестьсот пятый. #ЧтоНовенького #CSharp9
C#9: Улучшенное Сопоставление с Образцом
В своё время VB стал популярным из-за своей выразительности. Программу можно было читать почти как обычный английский текст. Для сравнения, C# и C++ оптимизированы для краткости синтаксиса и производительности. Это сделало их быстрыми, но в некоторых случаях трудным для чтения. Сравните VB:
If Not a And b Then

Else

EndIf

И C#:
if(!(a) && b)
{

}
else
{

}

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

C#9 теперь поддерживает несколько новых ключевых слов: not, and и or. Это упрощает чтение некоторых выражений, но вы по-прежнему можете использовать более короткий синтаксис с использованием символов:
if(s is not string)
на мой взгляд, проще для чтения, чем
if(!(s is string))

Сопоставление с образцом
Эти новые операторы полезны при сопоставлении с образцом. Кстати, теперь вы также можете использовать реляционные шаблоны, такие как >=, > и т.д., непосредственно в выражениях:
record Person(int? Weight);
Person p = new Person(175);
var category = p.Weight switch
{
< 150 => "лёгкий",
>= 150 and < 200 => "средний",
not null => "неизвестно",
null => "ошибка"
};

До C#9 некоторые шаблоны было невозможно выразить, например, not null. В основном это было ограничением компилятора, потому что !null не был допустимым синтаксическим токеном в C#. То же самое относится к таким выражениям, как
>= 150 and < 200
До C#9 компилятор не знал, как анализировать &&, за которым следует другой логический символ, например <. В выражении сопоставления с образцом компилятор знает, с какой переменной мы сопоставляем, и понимает, что означает это выражение, без повторяющихся объявлений переменных, которые только загромождали выражение. В итоге мы получили синтаксис, который намного проще понять людям, читающим код.

Источник: https://blog.miguelbernard.com/c-9-0-improved-pattern-matching/
День шестьсот сорок четвёртый. #ЧтоНовенького #CSharp9
C# 9 Неизвестные Фишки
В C# 9 появилось много новых возможностей (см. посты по тегу #CSharp9) и несколько очень полезных малоизвестных вещей.

Инициализаторы модулей
Этот серьёзный недостаток, который до сих пор можно было исправить только некоторыми сторонними инструментами, теперь поддерживается в .NET. Инициализаторы модулей позволят библиотекам выполнять инициализацию при загрузке с минимальными накладными расходами и без необходимости явного вызова со стороны пользовательского кода. В основном это встречалось в библиотеках тестов, где вы хотите что-то инициализировать перед запуском одного, нескольких или даже всех тестов в сборке. Например, его можно использовать для переопределения некоторых статических переменных перед запуском тестирования. Чтобы использовать эту функцию, пометьте метод атрибутом ModuleInitializer. Не все методы могут поддерживать эту функцию. Метод должен:
- быть статическим
- без параметров
- возвращать void
- не быть обобщённым
- не содержаться в обобщённом классе
- быть доступным из содержащего его модуля (internal или public).
[ModuleInitializer]
public static void Magic() { … }

Упрощенная проверка на null
Для реализации проверок на null требуется много шаблонного кода и хорошая дисциплина для их последовательного применения. C# 9 решает эту проблему, предоставляя упрощённый синтаксис. Просто добавьте ! в конце имени параметра метода:
public void Before(string name) {
if (name is null)
throw new ArgumentNullException();
}
Следующий метод приведёт к аналогичному сгенерированному IL-коду:
public void Now(string name!) { }

Ковариантные Переопределения
Ковариантные переопределения теперь позволяют объявлять более конкретный тип при переопределении метода базового класса, содержащего менее конкретный тип возврата:
abstract class Animal {
public abstract Food GetFood();
}
class Tiger : Animal {
public override Meat GetFood() => …;
}

Это значительное улучшение, поскольку теперь вы можете избежать приведения возвращаемого значения во время выполнения при использовании дочернего типа:
var t = new Tiger();
// Раньше
Meat m = (Meat)t.GetFood();
// Сейчас
Meat m2 = t.GetFood();

Int Нативного Размера
Это новая конструкция, которая позволяет объявлять int, размер которого определяется платформой: 32 или 64 бита. nint и nuint - новые ключевые слова для этого.

Такая возможность уже существовала с IntPtr и UIntPtr. nint и nuint - это просто оболочки над этими типами, предоставляющие дополнительные возможности, такие как преобразование и арифметические операции, которые невозможны с IntPtr. Это может значительно оптимизировать производительность приложения при выполнении интенсивных вычислений, требующих низкоуровневого доступа и где важен каждый байт.

Источник: https://blog.miguelbernard.com/c-9-the-unknown-goodies/
День шестьсот шестидесятый. #ЧтоНовенького #EFCore5
Вместе с .NET 5 выпущено множество обновлений. Про новшества в C#9 я писал в постах с тегом #CSharp9. Теперь рассмотрим, что нового в Entity Framework Core 5.0.

Отношение многие-ко-многим
EF Core 5.0 поддерживает отношения многие-ко-многим без явной привязки вспомогательной таблицы. Рассмотрим следующие сущности постов в блоге и тегов:
public class Post {
public int Id { get; set; }
public string Name { get; set; }
public ICollection<Tag> Tags { get; set; }
}
public class Tag {
public int Id { get; set; }
public string Text { get; set; }
public ICollection<Post> Posts { get; set; }
}

Заметьте, что Post содержит коллекцию элементов Tag и наоборот. EF Core 5.0 по соглашению распознает это как отношение многие-ко-многим. То есть добавлять специальный код в OnModelCreating не требуется. Когда для создания базы данных используются миграции (или EnsureCreated), EF Core автоматически создаст вспомогательную таблицу. Например, в SQL Server для этой модели EF Core сгенерирует:
CREATE TABLE [Posts] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
CONSTRAINT [PK_Posts] PRIMARY KEY ([Id])
);
CREATE TABLE [Tag] (
[Id] int NOT NULL IDENTITY,
[Text] nvarchar(max) NULL,
CONSTRAINT [PK_Tag] PRIMARY KEY ([Id])
);
CREATE TABLE [PostTag] (
[PostsId] int NOT NULL,
[TagsId] int NOT NULL,

);

Создание и связывание экземпляров объектов Tag и Post приведёт к автоматическому обновлению вспомогательной таблицы. После вставки сообщений и тегов EF автоматически создаст строки во вспомогательной таблице. Для запросов Include и другие операции будут работать так же, как и для любых других отношений.

В отличие от EF6, EF Core также позволяет полностью настраивать вспомогательную таблицу. Например, приведенный ниже код настраивает отношение «многие-ко-многим» через вспомогательный объект, в котором вспомогательный объект также содержит свойство с полезными данными (PublicationDate):
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder
.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany()
.HasForeignKey(pt => pt.TagId),
j => j
.HasOne(pt => pt.Post)
.WithMany()
.HasForeignKey(pt => pt.PostId),
j => {
j.Property(pt => pt.PublicationDate)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}

Источник: https://docs.microsoft.com/ru-ru/ef/core/what-is-new/ef-core-5.0/whatsnew#many-to-many
День шестьсот шестьдесят шестой. #ЗаметкиНаПолях #CSharp9
Атрибуты для Свойств Записей в C# 9
Записи обеспечивают простое создание неизменяемых объектов, особенно при использовании первичного конструктора:
public record User(string Name, DateTime DOB);
Это значительно сокращает код. Рассмотрим ситуацию, когда вы хотите сериализовать запись, чтобы получить следующий результат:
{"User":"Jon Smith","DateOfBirth":"1970-01-01T00:00:00"}
Заметьте, что ключи не совпадают с именами свойств записи. Обычно это потребовало бы добавления к свойствам записи атрибута JsonPropertyAttribute. То есть в полной записи это выглядело бы так:
public record User {  
[JsonProperty("User")]
public string Name{get;init;}

[JsonProperty("DateOfBirth")]
public DateTime DOB{get;init;}
}

При использовании первичного конструктора есть соблазн сделать аналогично:
public record User(
[JsonProperty("User")]
string Name,
[JsonProperty("DateOfBirth")]
DateTime DOB
);

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

Решение заключается в указании цели, к которой применяется атрибут. Как сказано в документации Microsoft:
Атрибуты могут быть применены к синтезированному автоматическому свойству или его вспомогательному полю, используя указатель цели атрибута property: или field: соответственно для атрибутов, синтаксически применяемых к соответствующему параметру записи.

В итоге мы получим следующую запись:
public record User(
[property:JsonProperty("User")]
string Name,
[property:JsonProperty("DateOfBirth")]
DateTime DOB
);

Теперь можно сериализовать нашу запись и получить желаемый результат:
var data = new User("Jon Smith",new DateTime(1970,1,1));  
var serializedData = JsonConvert.SerializeObject(data);

// Вывод
{"User":"Jon Smith","DateOfBirth":"1970-01-01T00:00:00"}

Источник: https://www.c-sharpcorner.com/blogs/attributes-for-record-properties-in-c-sharp-9
День шестьсот восьмидесятый. #ЧтоНовенького #CSharp9
Ещё Раз про Сопоставления с Образцом
Я уже писал об изменениях в сопоставлении с образцом в C#9. Здесь же хочу привести некое саммари всех изменений с примерами:

1. Шаблоны типа используются для сопоставления с типом. Если тип входных данных соответствует типу, указанному в шаблоне, совпадение считается успешным.
object checkType = new int();
var getType = checkType switch {
string => "string",
int => "int",
_ => "obj"
};
Console.WriteLine(getType);
// Вывод: int

2. Реляционные шаблоны позволяют сопоставить входные данные с константами, используя знаки >, < или = (a также >= или <=):
var person = new Person("John", 42);
var person2 = new Person("Jane", 8);
var ageInRange = person switch {
//тип Person указан явно
Person(_, < 18) => "меньше 18",
//тип выводится компилятором
(_, > 18) => "больше 18",
(_, 18) => "18!"
};
Console.WriteLine(ageInRange);
// Вывод: больше 18

3. Комбинаторные шаблоны позволяют комбинировать несколько шаблонов в одной строке:
var person = new Person("John", 42);

- Конъюнктивные представляют собой логическое «и» двух подшаблонов:
var ageInRange = person switch {
(_, < 18) => "меньше 18",
("John", _) and (_, > 18) => "Джону больше 18"
};
Console.WriteLine(ageInRange);
// Вывод: Джону больше 18

- Дизъюнктивные представляют собой логическое «или» двух подшаблонов:
var ageInRange = person switch {
(_, < 18) => "меньше 18",
(_, 18) or (_, > 18) => "18 или больше"
};
Console.WriteLine(ageInRange);
// Вывод: 18 или больше

- Отрицательные требуют несовпадения с заданным шаблоном:
var isJohn = person switch {
not ("John", 42) => "не Джон!",
_ => "Джон :)"
};
Console.WriteLine(isJohn);
// Вывод: Джон :)

4. В шаблонах допустимо использовать скобки:
public record IsNumber(bool IsValid, int Number);
var num = new IsNumber(true, 10);
var zeroToTen = num switch {
((_, >= 0 and <= 5) or (_, > 5 and <= 9))
or (_, 10) => "от 0 до 10",
_ => "больше 10"
};
Console.WriteLine(zeroToTen);
// Вывод: от 0 до 10

Источник: https://www.c-sharpcorner.com/article/c-sharp-9-cheatsheet/