02 июля 2025
В этой статье мы рассмотрим некоторые новые функции, представленные в 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 — эти улучшения решают реальные проблемы в повседневном программировании. Они устраняют ненужный синтаксис, повышают читаемость и внедряют более безопасные шаблоны, напрямую улучшая рабочие процессы в разработке программного обеспечения и проектах по преобразованию кода. Каждое новшество — будь то принудительная инициализация обязательных членов или упрощение настройки коллекций — сокращает количество нажатий клавиш, одновременно максимизируя ясность и минимизируя риск ошибок.