В данной статье мы рассмотрим новую версию языка 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.
Комментарии (57)
rsashka
20.10.2021 11:52-3Бывает такое, что пространство имён необходимо использовать во всём файле, но это может немного сдвинуть отступы текста программы.
А что мешает настроить отступы так, чтобы namespace не сдвигал их в тексте программы?
SvyatoslavMC
20.10.2021 11:56+3Раз это доработали, значит что-то мешает. Скорее всего, вручную сдвиги никто не трогает, т.к. в Visual Studio хороший режим автоформатирования. А настроить сдвиги для разных элементов вроде невозможно, да и будет странно выглядеть.
rsashka
20.10.2021 13:47-10Странно назвать режим автоформатирования хорошим, если он не позволяет настраивать отступы в namespace и для этого потребовалось вносить доработки аж в сам синтаксис языка. Как по мне, очень своеобразная логика :-)
mayorovp
20.10.2021 21:20+11Кроме редактора, который не настраивается таким образом?
Наверное, мешает то, что пара фигурных скобок без отступа выглядит уродливо.
lostmsu
21.10.2021 00:28+1Спесь инженеров Microsoft и желание переусложнить всё и вся во имя собственного Эго:
korsetlr473
20.10.2021 13:23+4вот эти record ктото сумел применять?
я чет сколько пишу так и не могу переключится на них или не понимаю моменты когда нужно их использовать ....
fransua
20.10.2021 13:38+10Да, перешли на них для DTO. Удобно клонировать и сравнивать
zerg903
21.10.2021 01:29+6Да, с record код стал проще, но насчет "сравнивать" - есть нюансы:
private record Dto(string Name, List<int> Values); public static void Main() { var dto1 = new Dto("name", new List<int>() { 1 }); var dto2 = new Dto("name", new List<int>() { 1 }); Console.WriteLine(dto1 == dto2); // false !!! }
Чуда нет… и зная .net понимаешь, почему так происходит (для ссылочных типов, если не переопределен метод Equals(), то сравнение делается по ReferenceEquals()). Но все же хотелось бы получить иной результат, раз уж везде пишут о том, как удобно сравнивать record-ы.
orthoxerox
21.10.2021 10:55+2Да, для коллекций в рекордах надо использовать свои обёртки с переопределённым сравнением. Или неизменяемые типы из System.Collections.Immutable
Saladin
21.10.2021 11:26А можете развернуть мысль касательно System.Collections.Immutable?
Я сейчас быстро проверил, и похоже, что обыкновенное сравнение неизменяемых коллекций даёт такой же результат как и сравнение обычных коллекцийvar a = ImmutableList<int>.Empty.Add(1); var b = ImmutableList<int>.Empty.Add(1); Console.WriteLine(a.Equals(b)); // False
orthoxerox
25.10.2021 10:37+1Тогда скорее "завернуть мысль". Перепутал с коллекциями из language-ext.
VanKrock
28.10.2021 18:26Тут будет обратный эффект, так как коллекция неизменяемая, то после добавления элемента у вас по сути будет новый экземпляр коллекции.
anonymous
00.00.0000 00:00egorozh
20.10.2021 16:29+1Походу это тоже работает) По крайней мере, сейчас попробовал убрать "namespace MyApp;" в файле класса - всё собралось нормально с глобальным неймспейсом проекта.
beskaravaev
22.10.2021 12:53+1Такое и раньше работало, но так делать не стоит
maxkatz6
30.10.2021 13:37-1Почему нет? Разве не давно была пропертя в msbuild/csproj для определения дефолтного неймспейса? С приходом нового упрощённого csproj эта пропертя теперь не включена в проект и имеет значение имени проекта по умолчанию.
Не вижу разницы, если это было бы особым ключевым словом. Делать так не стоит только потому что проще видеть явное имя.
mayorovp
30.10.2021 14:12Эта самая "пропертя" использовалась при сборке ресурсов, а также при создании новых файлов с кодом. Но на процесс компиляции она не влияла никогда.
pankraty
20.10.2021 21:59+4Но при наследовании есть одна особенность: переопределенный вами метод ToString не будет унаследован потомками от родительской записи. Для того чтобы потомки записей наследовали метод ToString, стало доступно использование ключевого слова sealed, которое запрещает компилятору генерировать имплементацию метода ToString у дочерних записей.
Выглядит как баг, который оказалось настолько сложно исправить, что пришлось запилить костыль, работающий для некоторых случаев. А если я хочу использовать родительскую имплементацию ToString во всех наследниках кроме одного-двух? Тупик, метод запечатан, его уже не переопределить.
Интересный факт. Данное нововведение относится к интерполированию строк, то есть добавление константного символа не допускается:
Очень странная, ничем не обоснованная особенность. Недоделка, которую исправят в С# 11?
Naf2000
20.10.2021 23:22+2В запечатанном ToString вызовите свой виртуальный ToCustomString. Переопределяйте его по необходимости. Он то не будет автоматом переопределяться
mayorovp
20.10.2021 23:53Вообще-то особенность ещё как обоснованная: форматирование объектов, в общем случае, зависит от текущей культуры потока и не может быть выполнено во время компиляции, так что как ни крути — придётся вводить белый список того что может быть сделано компилятором. Сейчас в этот белый список вошли строки. Да, символы можно было включить тоже, но зачем?
pankraty
21.10.2021 08:48+2В общем-то незачем, конечно (впрочем, и без интерполированнных строк в константах можно было жить), просто выглядит неконсистентно. Это как разрешить операцию сложения только для многозначных чисел, но не для однозначных
mvv-rus
20.10.2021 22:05+2Мое личное мнение: пара полезных семантических вещей, которых раньше не хватало, и которые удобно реализовать было нельзя: это инициализация элеметов структур и конструктор стурктуры по умолчанию…
… и толстый слоймолочного шоколадасинтаксического сахара.
NeoCode
20.10.2021 22:38А «одновременное использование объявленных и инициализируемых переменных при деконструировании» в операторе switch еще не ввели?
По идее это как раз нужно для полноценного паттерн матчинга по составным объектам (кортежам и т.п.), когда в case-паттерне задается часть полей, а другая часть объявляется как новые переменные. Если заданные поля совпадают с соответствующими полями аргумента switch, то поля, объявленные как переменные, инициализируются соответствующими полями из аргумента switch.
Naf2000
20.10.2021 23:24Расширять работу с expression планируют? Например with в лямбдах?
AgentFire
23.10.2021 00:27а поподробнее пример-пожелание можно?
Naf2000
30.10.2021 13:30Сколько я понимаю, выражение копирование с with https://docs.microsoft.com/ru-ru/dotnet/csharp/language-reference/operators/with-expression недоступно в expression
ArchiDNA
21.10.2021 01:26+1Да исключите наконец слово new из создания объекта. Точнее сделайте его факультативным.
pankraty
21.10.2021 09:18+1Это будет ломающим изменением, т.к. могут существовать методы с таким же названием в вызывающем классе, и после обновления внезапно начнёт вызываться конструктор. Вряд ли на такое пойдут.
Пример (абсолютно синтетический):
private MyClass MyClass(long x) => new MyClass((int)x+1);
var p = MyClass(1) ;
Сейчас вызывается метод, а если отказаться от new - будет вызываться конструктор, как более подходящая перегрузка.
pankraty
21.10.2021 09:33+2Хотел написать, что для анонимных объектов это могло бы сработать, но там могут быть неприятные эффекты, когда блок кода с присвоением переменной из-за неправильно поставленной точки с запятой будет трактоваться как создание анонимного объекта без присваивания куда-то.
{ x = 1 };
{ x = 1; }
marshinov
31.10.2021 15:55В Котлин пришлось object сделать ключевым словом, чтобы new убить. Хотя там конечно специфика JVM еще, например functionName ClassName - сразу видно где конструктор, где функция. Ну и сама среда немного другое разрешает.
orthoxerox
21.10.2021 10:59+1Самое интересное в C#10 - абстрактные статические методы в интерфейсах, но они пока в preview, чтобы Микрософт смог собрать обратную связь и зарелизить их в C#11 с учётом пожеланий программистов. Так что пользуйтесь ими в своих любительских проектах и жалуйтесь.
khabib
21.10.2021 11:27+3Смотрю на нововведения, как они читаются/выглядят, и сравниваю новинками C++. Плюсы делают реально инопланетяне.
KaiOvas
21.10.2021 16:21-4Нововведения ниочем.
Последнее существенное - record все остальное это просто присыпка сахаром и побрызгивание дезодорантом.... Язык по сложности скатывается к C++ но конечно до этого еще далеко, хотя тенденцию видно.
vabka
21.10.2021 16:30+2Всегда можно попробовать F# :)
KaiOvas
21.10.2021 17:38+1Это было бы прекрасно если бы этот язык продвигали в продакшен но к сожалению найти вакансию с C# гораздо легче чем с F# несмотря на то что после F# обратно уже не хочется. Я использую F# для своих внутренних утилит но последний раз когда я попытался исходники утилиты написанную на нем залить в репозиторий то выслушал много идиоматических выражений от кастомера что я "исказил и обфусцировал исходники С#".
marshinov
31.10.2021 15:49На самом деле не так уж и больно с F# на C# возвращаться. С Котлин на Java больнее:)
d_ilyich
21.10.2021 21:08Прошу прощения, что не совсем по теме. Посоветуйте, пожалуйста, актуальную литературу/курсы по C# (.NET). Желательно на русском, хотя бы для начала. Сейчас пока по сайту Метанит осваиваю.
SvyatoslavMC
22.10.2021 10:34+2Если Вы находитесь на начальном уровне, Вам подойдёт любая книга, где освещены все возможности языка, начиная с базовых. Обычно они очень толстые. Берите самое свежее издание, какое найдёте. С ростом опыта подход к обучению надо изменить, но пока так.
VanKrock
28.10.2021 18:37+1Советую еще посмотреть курсы на ulearn от УрФУ, по моему один из лучших курсов по C# для начинающих
SWATOPLUS
22.10.2021 17:11+2Required properties не добавили, расходимся.
beskaravaev
23.10.2021 10:44+1Можно небольшое описание того, что это, для чего и как должно работать? Заранее спасибо.
SWATOPLUS
23.10.2021 20:45+1Это киллер фича, которая обязывает инициализировать указанные свойства в DTO.
Казалось бы есть конструктор для этого, но систаксис с инициализатором свойств намного удобнее. Это ещё один шаг в борьбе с NullReferreceException.
https://github.com/dotnet/csharplang/blob/main/proposals/required-members.md
https://github.com/dotnet/csharplang/issues/3630
И в догонку Simplified parameters null validation. Которое гарантирует null-безапасность и пишет бойлерплейт за вас.
// Before void Insert(string s) { if (s is null) throw new ArgumentNullException(nameof(s)); ... } // After void Insert(string s!) { ... }
SvyatoslavMC
24.10.2021 19:57+2В оригинальном репозитории богатый список киллер фич. Абсолютно без сарказма. Но из предложений в реальные обновления попадают единицы.
marshinov
31.10.2021 15:47Жалко Static abstract members in interfaces только в preview.
mvv-rus
01.11.2021 18:02А зачем static нужны именно в интефейсах?
IMHO им место в ограничениях для класса, в where.
Интерфейс (точнее — ссылка на него) — это то, к чему можно привести (ссылку на) экземпляр типа, и вызвать его экземплярные методы как виртуальные (AKA через позднее связывание), т.е. — некая дополнительная виртуальная таблица, котоорая может присутвовать во многих классах. А статические методы экземпляров не требуют.
Тем более, что ограничения по наличию методов, реализованных в классе, в языке уже реально присутствуют (например — ограничения на то, что стоит после оператора await). Так что IMHO лучше бы их включить в синтаксис в явном виде (вроде, было когда-то такое предложение, что с ним сейчас — не в курсе) И ограничения на обязательность реализации статических методов войдут туда вполне органично.marshinov
01.11.2021 18:20Они рассматривали несколько вариантов, в итоге решили, что проще разрешить статику в интерфейсах, чем вводить дополнительные синтаксические сахарюшки. Т.е. если в generic'е хочется +, то нужно объявить:
Add<T>(T x, T y) where T: IAddable interface IAddable { static + }
В proposal'е это коротко написано. Если другие proposal'ы посмотреть, то че только не предлагали: начиная с тайпклассов, заканчивая трейтами. ИМХО, у C# прослеживается довольно выигрышная стратегия вводить хорошие частные решения для типовых задач. Когда Бреслав говорит, что "у нас async/await - это не ключевые слова, а функции, а ключевое слово у нас suspend" я не вполне уверен, что это преимущество Котлина, потому что чтобы использовать async/await без понимания внутренностей достаточно не использовать .Result и всегда предпочитать асинхронные версии методов и не оборачивать синхронные в Task.Run. С корутинами все несколько сложнее на мой вкус...
mvv-rus
01.11.2021 20:48-1Они рассматривали несколько вариантов, в итоге решили
Ну, решать-то, конечно. Им, но вот «особенности», которые в общую схему языка не вписываются — типа await, выражение после которого должно просто иметь в описании некоторые буковки — один интерфейс и пару методов с фиксироваными названиями — я считаю, неплохо было бы таки вписать в общую схему языка.
Такая же фича есть и в LINQ query syntax — типа, что стоит во множественных from, должно реализовывать SelectMany с определенными параметрами — а там оно может быть что угодно. Но там хоть язык формально другой, отличающийся от основного массива C#
Тут проблема IMHO в том, что такие вот исключения из общих правил делают язык громоздким. Чем их больше — тем более громоздким он получается. А это — повышение требований к квалификации разработчика — при том, что индустрия сейчас и так страдает от завышенных требований, т.е. нехватки достаточно кавлифицированного персонала.
Но, конечно, пусть они делают там что хотят.
darkagent
Deja vu www.youtube.com/watch?v=Vft4QDUpyWY