В данной статье мы рассмотрим новую версию языка C# 10, которая включает в себя небольшой список изменений относительно C# 9. Ниже приведены их описания вместе с поясняющими фрагментами кода. Давайте их рассмотрим.
Изменения структур
Инициализация полей структуры
Теперь в структурах можно задать инициализацию полей и свойств:
public struct User
{
public User(string name, int age)
{
Name = name;
Age = age;
}
string Name { get; set; } = string.Empty;
int Age { get; set; } = 18;
}
Объявление конструктора структуры без параметров
В C# 10 станет доступно создание конструктора без параметров для структур:
public struct User
{
public User()
{
}
public User(string name, int age)
{
Name = name;
Age = age;
}
string Name { get; set; } = string.Empty;
int Age { get; set; } = 18;
}
Важно. Использовать конструктор без параметров можно только тогда, когда все поля и/или свойства имеют инициализаторы. Например, если не задать инициализатор Age, возникнет ошибка:
Error CS0843: Auto-implemented property 'User.Age' must be fully assigned before control is returned to the caller.
Применение выражения with к структуре
Новая версия C# делает доступным использование выражения with со структурами, как ранее это можно было сделать с записями. Пример:
public struct User
{
public User()
{
}
public User(string name, int age)
{
Name = name;
Age = age;
}
public string Name { get; set; } = string.Empty;
public int Age { get; set; } = 18;
}
User myUser = new("Chris", 21);
User otherUser = myUser with { Name = "David" };
Очевидно, что изменяемое свойство (в данном случае поле Name) должно быть с публичным модификатором доступа.
Глобальное использование using
Данное нововведение позволит использовать директиву using с охватом всего проекта. Для этого необходимо перед фразой using добавить ключевое слово global:
global using "Пространство имен"
Таким образом, можно не дублировать в разных файлах одни и те же пространства имён, используя директиву using.
Важно. Применять данную конструкцию следует ПЕРЕД включениями using, которые объявлены без использования global. Пример:
global using System.Text;
using System;
using System.Linq;
using System.Threading.Tasks;
// Корректная запись
В ином случае:
using System;
using System.Linq;
using System.Threading.Tasks;
global using System.Text;
// Error CS8915
// A global using directive must precede
// all non-global using directives.
Если произошло так, что вы повторно включили одно пространство имён при условии, что оно было ранее включено с использованием ключевого слова global, то об этом будет сообщено средой разработки (IDE: 0005: Using directive is unnecessary).
namespace для всего файла
Бывает такое, что пространство имён необходимо использовать во всём файле, но это может немного сдвинуть отступы текста программы. Теперь для решения этой проблемы добавлена возможность использования namespace для всего файла. Для этого необходимо написать namespace без использования блочных фигурных скобок:
using System;
using System.Linq;
using System.Threading.Tasks;
namespace TestC10;
public class TestClass
{
....
}
Ранее необходимо было тянуть на весь файл открытый блок namespace:
using System;
using System.Linq;
using System.Threading.Tasks;
namespace TestC10
{
public class TestClass
{
....
}
}
Очевидно, указать в файле можно лишь один такого рода namespace. То есть данная запись некорректна:
namespace TestC10;
namespace MyDir;
// Error CS8954
// Source file can only contain
// one file-scoped namespace declaration
как и запись такого вида:
namespace TestC10;
namespace MyDir
{
....
}
// Error CS8955
// Source file can not contain both
// file-scoped and normal namespace declarations.
Изменение записей
Ключевое слово class
В версии языка C# 10.0 было добавлено необязательное ключевое слово class у записей, которое лишь помогает при чтении кода определить принадлежность записи к ссылочному типу.
То есть две следующие записи идентичны:
public record class Test(string Name, string Surname);
public record Test(string Name, string Surname);
Структуры записей
Также теперь можно создавать структуры записей:
record struct Test(string Name, string Surname)
По умолчанию, свойства данной записи можно изменять, в отличие от стандартной record, в которой модификатор изменения полей – init:
string Name { get; set; }
string Surname { get; set; }
К данной структуре можно применить свойство readonly, тогда доступ к полям будет подобен обычной записи:
readonly record struct Test(string Name, string Surname);
где свойства записаны как:
string Name { get; init; }
string Surname { get; init; }
Равенство двух объектов структуры записей схоже с равенством двух обычных структур – равенство истинно, если эти два объекта хранят одни и те же данные:
var firstRecord = new Person("Nick", "Smith");
var secondRecord = new Person("Robert", "Smith");
var thirdRecord = new Person("Nick", "Smith");
Console.WriteLine(firstRecord == secondRecord);
// False
Console.WriteLine(firstRecord == thirdRecord);
// True
Стоит отметить, что для структур записей не генерируется конструктор копирования. Если определить его и использовать ключевое слово *with *при инициализации нового объекта, то вызываться будет оператор присваивания, вместо конструктора копирования (как это происходит при работе с record class).
Запечатывание метода ToString
Как писал мой коллега в статье о нововведениях в C# 9, у записей имеется метод ToString, который можно переопределить. Но при наследовании есть одна особенность: переопределенный вами метод ToString не будет унаследован потомками от родительской записи. Для того чтобы потомки записей наследовали метод ToString, стало доступно использование ключевого слова sealed, которое запрещает компилятору генерировать имплементацию метода ToString у дочерних записей. Данное ключевое слово указывается при переопределении метода ToString:
public sealed override string ToString()
{
....
}
Создадим следующую запись, включая переопределение метода ToString:
public record TestRec(string name, string surname)
{
public override string ToString()
{
return $"{name} {surname}";
}
}
И наследуем от него вторую запись:
public record InheritedRecord : TestRec
{
public InheritedRecord(string name, string surname)
:base(name, surname)
{
}
}
Теперь создадим по экземпляру каждой из записей и напечатаем результат в консоль:
TestRec myObj = new("Alex", "Johnson");
Console.WriteLine(myObj.ToString());
// Alex Johnson
InheritedRecord mySecObj = new("Thomas", "Brown");
Console.WriteLine(mySecObj.ToString());
// inheritedRecord { name = Thomas, surname = Brown}
Как видно из результата выше, метод ToString не был наследован записью InheritedRecord.
Немного изменим запись TestRec, добавив ключевое слово sealed:
public record TestRec(string name, string surname)
{
public sealed override string ToString()
{
return $"{name} {surname}";
}
}
Заново создадим два экземпляра записей и напечатаем результат в консоль:
TestRec myObj = new("Alex", "Johnson");
Console.WriteLine(myObj.ToString());
// Alex Johnson
InheritedRecord mySecObj = new("Thomas", "Brown");
Console.WriteLine(mySecObj.ToString());
// Thomas Brown
И.. ура! Метод ToString был наследован в записи InheritedRecord.
Упрощение доступа к вложенным полям и свойствам шаблонных свойств
Версия языка C# 8.0 предоставила возможность использования шаблонных свойств, с помощью которых можно удобно сопоставлять поля и/или свойства какого-либо объекта с необходимыми значениями.
Ранее, если необходимо было проверить какое-либо вложенное свойство, то приходилось громоздить конструкцию:
....{property: {subProperty}: pattern}....
Сейчас достаточно лишь привычного добавления точек между свойствами:
....{property.subProperty: pattern}....
Рассмотрим изменение на примере метода взятия 4 первых символов имени.
public record TestRec(string name, string surname);
string TakeFourSymbols(TestRec obj) => obj switch
{
// старый способ:
//TestRec { name: {Length: > 4} } rec => rec.name.Substring(0,4),
// новый способ:
TestRec { name.Length: > 4 } rec => rec.name.Substring(0,4),
TestRec rec => rec.name,
};
Как видно из примера выше, новый тип обращения к свойствам проще и понятнее, чем был раньше.
Интерполирование константных строк
Ранее данная возможность не поддерживалась, но в новой версии языка C# можно будет интерполировать и константные строки:
const string constStrFirst = "FirstStr";
const string summaryConstStr = $"SecondStr {constStrFirst}";
Интересный факт. Данное нововведение относится к интерполированию строк, то есть добавление константного символа не допускается:
const char a = 'a';
const string constStrFirst = "FirstStr";
const string summaryConstStr = $"SecondStr {constStrFirst} {a}";
// Error CS0133
// The expression being assigned to
// 'summaryConstStr' must be constant
Одновременное использование объявленных и инициализируемых переменных при деконструировании
В старом варианте вызова деконструктора можно использовать ИЛИ объявленные переменные (все объявлены), ИЛИ переменные, которые мы инициализируем прямо при вызове (все НЕ объявлены):
Car car = new("VAZ 2114", "Blue");
var (model, color) = car;
// Инициализация
string model = string.Empty;
string color = string.Empty;
(model, color) = car;
// Присваивание
Новая версия языка допускает одновременное использование как объявленных ранее, так и не объявленных переменных при деконструировании:
string model = string.Empty;
(model, var color) = car;
// Инициализация и присваивание
В версии C# 9 возникала ошибка:
Error CS8184: A deconstruction cannot mix declarations and expressions on the left-hand-side.
Заключение
Как говорилось ранее, список изменений не столь велик, как в версии C# 9. Одни изменения упрощают работу, а другие разблокируют ранее недоступные возможности. Развитие не стоит на месте, а мы будем ждать новых обновлений языка C#.
Если вы еще не ознакомлены с нововведениями версии C# 9, то о них мы тоже подробно рассказывали в отдельной статье.
Если вы хотите ознакомиться с первоисточником, можете прочесть документацию Microsoft.
Если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: Valentin Prokofiev. What's new in C# 10: overview.