02 июля 2025

Новые возможности C# 11 и 12: Пишите меньше, делайте больше

В этой статье мы рассмотрим некоторые новые функции, представленные в C# 11 и 12, которые помогают упростить ваш код и сделать разработку более плавной. Эти обновления, возможно, не являются революционными, но они практичны и предназначены для экономии времени за счёт уменьшения ненужной сложности. Мы увидим, как небольшие изменения могут привести к более чистым и эффективным решениям в повседневных задачах программирования.

Необработанные строковые литералы

Создание строк с сложным содержимым исторически представляло проблему в C#. Разработчикам часто приходилось сталкиваться с необходимостью экранирования специальных символов, переносов строк и кавычек, что приводило к громоздкому и часто нечитаемому коду. Этот процесс становился особенно обременительным при работе с форматами, такими как JSON, XML или регулярные выражения, встроенные непосредственно в исходные файлы.

C# 11 представил необработанные строковые литералы, чтобы напрямую решить эту проблему. Эта функция позволяет строкам охватывать несколько строк и содержать практически любые символы, включая встроенные кавычки и обратные слэши, без необходимости использования escape-последовательностей. Необработанный строковый литерал начинается и заканчивается как минимум тремя символами двойных кавычек (""").

До C# 11:

string oldJson = "{\r\n  \"name\": \"Alice\",\r\n  \"age\": 30\r\n}";
Console.WriteLine(oldJson);

С C# 11:

string newJson = """
  {
    "name": "Alice",
    "age": 30
  }
  """;
Console.WriteLine(newJson);

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

Шаблоны списков

Сопоставление с образцом в C# значительно эволюционировало, и C# 11 представил шаблоны списков, которые позволяют выполнять сопоставление последовательностей в массивах или списках. Это улучшение даёт разработчикам возможность кратко и выразительно проверять структуру и содержимое коллекций.

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

До C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers != null && numbers.Length == 3 &&
    numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
    Console.WriteLine("Массив содержит ровно 1, 2, 3.");
}

if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
    Console.WriteLine("Второй элемент массива равен 2.");
}

С C# 11:

int[] numbers = { 1, 2, 3 };

if (numbers is [1, 2, 3])
{
    Console.WriteLine("Массив содержит ровно 1, 2, 3.");
}

if (numbers is [_, 2, ..])
{
    Console.WriteLine("Второй элемент массива равен 2.");
}

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

Обязательные члены

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

C# 11 вводит модификатор required для свойств и полей, представляющий собой механизм принудительного выполнения на этапе компиляции. Когда член помечен как required, компилятор гарантирует, что он получит значение при создании объекта, либо через конструктор, либо через инициализатор объекта. Это обеспечивает, что экземпляры типа всегда находятся в действительном, полностью инициализированном состоянии, предотвращая распространённые ошибки, связанные с отсутствием данных.

До C# 11:

public class OldPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public void DisplayName() => Console.WriteLine($"Имя: {FirstName} {LastName}");
}

// Использование:
var person = new OldPerson(); // Нет ошибки компиляции, но создаётся потенциально недействительный объект
person.DisplayName();

С C# 11:

public class NewPerson
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }

    public void DisplayName() => Console.WriteLine($"Имя: {FirstName} {LastName}");
}

// Использование:
// var person = new NewPerson(); // Ошибка компиляции - отсутствуют обязательные свойства
// var person = new NewPerson { FirstName = "John" }; // Ошибка компиляции - отсутствует LastName
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // Всё в порядке
person.DisplayName();

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

Первичные конструкторы

C# 12 вводит первичные конструкторы для всех классов и структур, расширяя функцию, которая ранее была эксклюзивной для типов записей. Это позволяет объявлять параметры конструктора непосредственно в определении типа, автоматически определяя их как поля или свойства на уровне всего класса. В отличие от традиционных конструкторов, этот подход избавляет от явного объявления полей и ручного присваивания.

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

До C# 12:

public class OldProduct
{
    private readonly int _productId;
    private readonly string _productName;

    public OldProduct(int productId, string productName)
    {
        _productId = productId;
        _productName = productName;
    }

    public string PrintDetails() => $"ID продукта: {_productId}, Название: {_productName}";
}

// Использование:
OldProduct oldProd = new OldProduct(101, "Ноутбук");
oldProd.PrintDetails();

С C# 12:

public class NewProduct(int productId, string productName)
{
    public string PrintDetails() => $"ID продукта: {productId}, Название: {productName}";
}

// Использование:
NewProduct newProd = new NewProduct(102, "Клавиатура");
newProd.PrintDetails();

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

Выражения коллекций

Инициализация коллекций в C# исторически включала различные синтаксисы в зависимости от типа коллекции, такие как new List<T> { ... } для списков или new T[] { ... } для массивов. Объединение или слияние существующих коллекций в новую часто требовало итеративных циклов или методов LINQ, таких как Concat(), что добавляло издержки и громоздкость.

C# 12 вводит выражения коллекций, единый и краткий синтаксис для создания и инициализации широкого спектра типов коллекций. Используя простой синтаксис [...], разработчики могут создавать массивы, списки, Span<T> и другие коллекционные типы. Новый элемент распространения (..) позволяет встраивать элементы из существующих коллекций непосредственно в новое выражение коллекции, устраняя необходимость в ручном объединении.

До C# 12:

// Инициализация различных коллекций
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };

// Объединение коллекций
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);

Console.WriteLine(string.Join(", ", allNumbers));

С C# 12:

// Инициализация различных коллекций
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];

// Объединение коллекций с использованием оператора распространения
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];

Console.WriteLine(string.Join(", ", allNumbers));

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

Параметры лямбда-выражений по умолчанию

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

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

До C# 12:

// Лямбда без параметров по умолчанию.
// Если требовалось значение по умолчанию для 'y', часто использовалась обёртка или условная логика:
Func<int, int, int> addOld = (x, y) => x + y;

Func<int, int> addWithDefaultOld = x => addOld(x, 10); // Распространённое обходное решение

Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));

С C# 12:

// Лямбда с параметрами по умолчанию
Func<int, int, int> addNew = (x, y = 10) => x + y;

Console.WriteLine(addNew(5, 3)); // y равно 3
Console.WriteLine(addNew(5));    // y по умолчанию равно 10

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

Заключение

C# 11 и 12 предлагают впечатляющий набор функций, которые соответствуют обещанию “Пиши меньше, делай больше”. От необработанных строковых литералов и шаблонов списков в C# 11 до первичных конструкторов и выражений коллекций в C# 12 — эти улучшения решают реальные проблемы в повседневном программировании. Они устраняют ненужный синтаксис, повышают читаемость и внедряют более безопасные шаблоны, напрямую улучшая рабочие процессы в разработке программного обеспечения и проектах по преобразованию кода. Каждое новшество — будь то принудительная инициализация обязательных членов или упрощение настройки коллекций — сокращает количество нажатий клавиш, одновременно максимизируя ясность и минимизируя риск ошибок.

Связанные статьи