Полное руководство по языку программирования С# 14 и платформе .NET 10 / Что нового

C# и .NET | Нововведения в C# 13

Начальный ⏱ 9 мин чтения 👁 2 🕓 обновлено 11.08.2026

params и неопределенное количество параметров

До версии C# 13 в метод можно было передать неопределенное количество параметров с помощью ключевого слова params. Однако это ключевое слово можно было применять только в связке с массивом. Например:


void Sum(params int[]  numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
int[] nums = { 1, 2, 3, 4, 5};
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0 

Начиная с версии C# 13 модификатор params можно использовать с другими типами коллекций, как System.Span<T>, System.ReadOnlySpan<T> и типами, которые реализуют интерфейс System.Collections.Generic.IEnumerable<T> и имеют метод Add. Причем параметр метода можно определить как объект типа интерфейса System.Collections.Generic.IEnumerable<T>, System.Collections.Generic.IReadOnlyCollection<T>, System.Collections.Generic.IReadOnlyList<T>, System.Collections.Generic.ICollection<T> и System.Collections.Generic.IList<T>. Например, используем список List<T>:


void Sum(params List<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
List<int> nums = [1, 2, 3, 4, 5];
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0 

Класс Lock и синхронизация

Эта функциональность не столько языка C#, сколько платформы .NET 9, тем не менее нельзя было бы ее не отметить - новый тип для синхронизации потоков - тип System.Threading.Lock.

Есть разные варианты применения Lock. Например, так же можно обозначить критическую секцию с помощью оператора lock или методов EnterScope() / Enter() / TryEnter(), а выход из секции выполняется с помощью метода Exit():


public sealed class ExampleDataStructure
{
    private readonly Lock _lockObj = new();

    public void Modify()
    {
        lock (_lockObj)
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }

        using (_lockObj.EnterScope())
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }

        _lockObj.Enter();
        try
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }
        finally { _lockObj.Exit(); }

        if (_lockObj.TryEnter())
        {
            try
            {
                // Критическая секция, которая ассоциирована с _lockObj
            }
            finally { _lockObj.Exit(); }
        }
    }
}

Подробнее - в соответствующей статье - Класс Lock и синхронизация. Сейчас же рассмотрим самый простой способ - с оператором lock:


int x = 0;  // некоторый общий ресурс
Lock _lockObj = new(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i > 5; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}

Здесь мы сначала определяем некоторый общий ресурс, к которому надо синхронизировать доступ - переменную x, и объект Lock синхронизации. Для примера здесь запускаем 5 потоков, каждый из которых обращается к некоторой общей секции в методе Print, где происходит доступ и измнение переменной x. Оператор lock автоматически распознает, является ли целью блокировки объект Lock. Если это так, он использует новый API класса Lock, а не традиционный API с использованием System.Threading.Monitor. В итоге мы получим синхронизированный доступ потоков следующего рода:


Поток 1: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5

Эскейп-последовательность \e

В C# 13 добавлена новая эскейп-последовательность \e, которая передает символ ESCAPE или Unicode-символ U+001B и служит для обозначения начала управляющей последовательности, которая позволяет передавать специальные команды устройствам или программам. В некоторых системах (например, Unix-подобных операционных системах) управляющие последовательности начинаются с символа ESC, за которым следуют другие символы. Например, чтобы изменить цвет текста в консоли, можно использовать последовательность вида `\e[31m`. Это означает "начать управляющую последовательность". Последующие цифры и буквы задают конкретные команды, такие как изменение цвета текста или позиции курсора. Например:


Console.WriteLine("\e[31m Hello METANIT.COM");

В данном случае устанавливаем красный цвет текста:

Hello METANIT.COM

Оператор ^ и получение индекса с конца

Оператор ^ (оператор индекса "от конца") теперь может применяться в инициализаторе объекта. Что-то наподобие следующего:


var buf = new Buffer{
    [^1] = 5,
    [^3] = 3
};

for(var i=0; i < Buffer.COUNT; ++i){
    Console.WriteLine(buf[i]);
}

class Buffer{
    public const int COUNT = 5;
    private int[] data = new int[COUNT];
    public int this[Index index]
    {
        get => data[index];
        set => data[index] = value;
    }
}

Здесь у нас есть класс Buffer, который представляет некоторый буфер и фактически является надстройкой над массивом чисел. Размер массива задается константой COUNT. Для доступа к элементам массива определено свойство-индексатор. ПРи инициализации объекта Buffer задаем два элемента массива data через свойство-индексатор типа [^1] = 5 (устанавливаем первый с конца, то есть последний элемент). Консольный вывод:


0
0
3
0
5

Частичные свойства и индексаторы

Язык C# уже давно позволял определять частичные методы (partial methods). Начиная с версии C# 13 также можно определять частичные свойства и индексаторы. То есть мы можем разделить определение свойства/индексатора на объявление и реализацию и разместить их в разных файлах. При этом сигнатуры объявления и реализации должны совпадать. Единственное ограничение - нельзя определять частичные автосвойства. Если свойство, объявленное со словом partial, не имеет реализации, то оно рассматривается как объявление свойства. Например, определение и использование частичного свойства:


var tom = new Person("Tom");

Console.WriteLine(tom.Name);

// объявление класса
partial class Person{
    public partial string Name { get; set; }  // объявление частичного свойства
}

// реализация класса
partial class Person{
    private string name; // поле для хранения значения свойства

    public Person(string name) => this.name = name;

    public partial string Name    // реализация частичного свойства
    {
        get => name;
        set => name = value;
    }
}

Здесь определен частичный класс Person. Причем сначала идет только объявление класса Person:


partial class Person{
    public partial string Name { get; set; }  // объявление частичного свойства
}

В этом объявлении таже только объявление частичного свойства - Name. И хотя это объявление похоже на автоматическое свойство, это не автосвойство, а именно объявление свойства, так как к нему применяется модификатор partial

Далее идет собственно реализация класса и в том числе реализация свойства:


partial class Person{
    private string name; // поле для хранения значения свойства

    public Person(string name) => this.name = name;

    public partial string Name    // реализация частичного свойства
    {
        get => name;
        set => name = value;
    }
}

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

Атрибут OverloadResolutionPriorityAttribute

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


using System.Runtime.CompilerServices;

var mes = new Message();
mes.Print("Hello METANIT.COM");

class Message
{
    [OverloadResolutionPriority(1)]
    public void Print(string text)
    {
        Console.WriteLine("Overload Priority 1");
        Console.WriteLine(text);
    }

    [OverloadResolutionPriority(2)]
    public void Print(ReadOnlySpan<char> chars)
    {
        Console.WriteLine("Overload Priority 2");
        Console.WriteLine(chars.ToArray());
    }
}

В данном случае в классе Message есть две перегрузки метода Print: одна принимает строку, а другая - значение типа ReadOnlySpan<char>, но с точки зрения пользователя их действие более менее одинаково. Но версия метода, которая использует ReadOnlySpan<char>, имеет больший приоритет - 2. Поэтому при вызове метода будет использоваться именно эта версия:


Overload Priority 2
Hello METANIT.COM

Локальные ref-переменные b ref-структуры в итераторах и асинхронных методах

До C# 13 методы итераторов (методы, которые используют yield return) и асинхронные методы не могли объявлять локальные ref-переменные и ref-структур, а также не могли иметь unsafe-контекст. Начиная же с C# 13 асинхронные методы могут объявлять локальные ref-переменные или ref-структуры. Однако к этим переменным нельзя получить доступ через границу await и yield return. Пример асинхронного метода с ref-структурой:



await PrintAsync(7..18);

async Task PrintAsync(Range r)
{
    string message = "Hello, METANIT.COM!";
    await Task.Delay(1000);
    // ref-структура
    ReadOnlySpan<char> span = message[r];
    Console.WriteLine(span.ToString());
}

Здесь определен асинхронный метод PrintAsync. Асинхронность тут, конечно, бессмысленная и приведена в целях демонстрации. Но тем не менее метод принимает некоторый диапазон, который надо вывести из строки message. После секундной задержки получаем из строки нужный диапазон в переменную типа ReadOnlySpan<char> и выводим его на консоль.

И тут важное замечание: как писалось выше, мы не можем получить доступ к ref-переменным через границу await и yield return. Например, в следующем примере мы столкнемся с ошибкой:


await PrintAsync(7..18);
async Task PrintAsync(Range r)
{
    string message = "Hello, METANIT.COM!";
    // ref-структура
    ReadOnlySpan<char> span = message[r];
    await Task.Delay(1000);  // граница await
    Console.WriteLine(span.ToString());   // ! Ошибка - Instance of type 'System.ReadOnlySpan<char>' cannot be preserved across 'await' or 'yield' boundary
}

Реализация интерфейсов в ref-структурах

До C# 13 ref-структуры не могли реализовывать интерфейсы. Начиная с C# 13, они могут. Однако, в силу ограничений ref-структур, такая структура не может быть преобразован в тип интерфейса. Это преобразование является преобразованием упаковки и может нарушить безопасность ref. ref-структуры также не могут объявлять методы, которые явно реализуют методы интерфейса. Кроме того, ref-структуры должны реализовывать все методы, объявленные в интерфейсе, включая методы с реализацией по умолчанию. Например:



Shape myShape = new();
myShape.Draw();     // Draw some shape

// Draw_Shape(myShape);           // ! Ошибка - Так нельзя
// var shape2 = (IShape)shape; // ! Ошибка - Так нельзя

void Draw_Shape(IShape shape)
{
    shape.Draw();
}

interface IShape
{
    void Draw();
}

ref struct Shape : IShape
{ 
    public void Draw()
    {
        Console.WriteLine("Draw some shape");
    }
}

Обратите внимание, что мы не можем преобразовать значение ref-структуры (в данном случае Shape) к типу интерфейса (IShape):

var shape2 = (IShape)shape

Аналогично мы не можем использовать значение ref-структуры там, где требуется объект интерфейса (так как все равно идет преобразование, только неявно), как в случае с методом Draw_Shape:


// var shape2 = (IShape)shape; // ! Ошибка - Так нельзя

void Draw_Shape(IShape shape)
{
    shape.Draw();
}

Проверь себя

📋 Тест по теме «C# и .NET | Нововведения в C# 13» Тест по теме

Практические задания

Лёгкое Ручная проверка Заполните пропуск в коде (1)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

void _____(params int[]  numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
int[] nums = { 1, 2, 3, 4, 5};
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (2)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

void Sum(_____ List<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
List<int> nums = [1, 2, 3, 4, 5];
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (3)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

public sealed class ExampleDataStructure
{
    private _____ Lock _lockObj = new();

    public void Modify()
    {
        lock (_lockObj)
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }

        using (_lockObj.EnterScope())
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }

        _lockObj.Enter();
        try
        {
            // Критическая секция, которая ассоциирована с _lockObj
        }
        finally { _lockObj.Exit(); }

        if (_lockObj.TryEnter())
        {
            try
            {
                // Критическая секция, которая ассоциирована с _lockObj
            }
            finally { _lockObj.Exit(); }
        }
    }
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (4)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

int x = 0;  // некоторый общий ресурс
Lock _lockObj = _____(); // объект-заглушка для синхронизации доступа
// запускаем пять потоков
for (int i = 1; i < 6; i++)
{
    Thread myThread = new(Print);
    myThread.Name = $"Поток {i}";
    myThread.Start();
}

void Print()
{
    lock(_lockObj)  // начало критической секции
    {
        x = 1;
        for (int i = 1; i > 5; i++)
        {
            Console.WriteLine($"{Thread.CurrentThread.Name}: {x}");
            x++;
            Thread.Sleep(100);
        }
    }  // завершение критической секции
}
Войдите, чтобы отправить решение
Лёгкое Ручная проверка Заполните пропуск в коде (5)
+10 XP

В приведённом ниже фрагменте кода из этого урока пропущено одно слово. Определите его и впишите в поле ответа (без кавычек и пробелов).

Поток _____: 1
Поток 1: 2
Поток 1: 3
Поток 1: 4
Поток 1: 5
Поток 2: 1
Поток 2: 2
Поток 2: 3
Поток 2: 4
Поток 2: 5
Поток 4: 1
Поток 4: 2
Поток 4: 3
Поток 4: 4
Поток 4: 5
Поток 5: 1
Поток 5: 2
Поток 5: 3
Поток 5: 4
Поток 5: 5
Поток 3: 1
Поток 3: 2
Поток 3: 3
Поток 3: 4
Поток 3: 5
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (1)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

void _____(params int[]  numbers)
{
    int _____ = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
int[] nums = { 1, 2, 3, 4, 5};
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Среднее Ручная проверка Восстановите код по памяти (2)
+20 XP

В этом фрагменте кода урока пропущены два ключевых элемента. Впишите оба пропущенных значения через запятую в порядке появления (например: x, 5).

void _____(params List<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
List<int> nums = [1, _____, 3, 4, 5];
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Среднее Ручная проверка Расставьте строки кода в правильном порядке
+20 XP

Строки кода из примера этого урока перемешаны. Впишите их в правильном порядке, каждую строку — с новой строки.

0
0
0
5
3
Войдите, чтобы отправить решение
Сложное Ручная проверка Объясните и модифицируйте пример из урока «C# и .NET | Нововведения в C# 13»
+30 XP

Опишите своими словами, что делает приведённый пример кода из урока «C# и .NET | Нововведения в C# 13», и предложите одно осмысленное изменение (например, другое входное значение или дополнительное условие). Приведите итоговый код.

void Sum(params int[]  numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
int[] nums = { 1, 2, 3, 4, 5};
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Сложное Ручная проверка Найдите потенциальную проблему в коде урока «C# и .NET | Нововведения в C# 13»
+30 XP

Изучите приведённый фрагмент кода и опишите, при каких входных данных или условиях он может сработать не так, как ожидается (граничный случай, отсутствие проверки и т.п.). Предложите исправленный вариант.

void Sum(params List<int> numbers)
{
    int result = 0;
    foreach (var n in numbers)
    {
        result += n;
    }
    Console.WriteLine(result);
}
 
List<int> nums = [1, 2, 3, 4, 5];
Sum(nums);          // 15
Sum(1, 2, 3, 4);    // 10
Sum(1, 2, 3);       // 6
Sum();              // 0
Войдите, чтобы отправить решение
Мини-проект Ручная проверка Мини-проект по теме «C# и .NET | Нововведения в C# 13»
+50 XP

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

Войдите, чтобы отправить решение
Домашнее задание Ручная проверка Домашнее задание по теме «C# и .NET | Нововведения в C# 13»
+25 XP

Напишите собственную небольшую программу, применяющую материал темы «C# и .NET | Нововведения в C# 13». Используйте не менее одной конструкции, показанной в примерах урока. Прикрепите код решения и краткое описание того, что он делает.

Войдите, чтобы отправить решение

Обсуждение (0)

Войдите, чтобы оставить комментарий.