Обзор нововведений в C# 12

от автора

Уже середина осени, а это значит, что новая версия C# на пороге. Самое время узнать, какие изменения настигнут язык совсем скоро. Хоть количество нововведений в этот раз уступает предыдущим релизам, интересных среди них немало.

1074_csharp12_ru/image1.png

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

Одно из самых заметных quality of life улучшений – возможность определить конструктор прямо в объявлении класса:

class Point(int posX, int posY) {     private int X = posX;     private int Y = posY;      public bool IsInArea(int minX, int maxX, int minY, int maxY)         => X <= maxX && X >= minX && Y <= maxY && Y >= minY; } // .... var point = new Point(100, 50); Console.WriteLine(point.IsInArea(30, 150, 50, 150)); // True 

При этом не использовать такой конструктор не выйдет – он заменяет собой пустой конструктор по умолчанию, а при добавлении других конструкторов обязательно нужно будет добавлять this(….):

class Point(int posX, int posY) {     private int X = posX;     private int Y = posY;     private Color color;      public Point(int posX, int posY, Color color) : this(posX, posY)     {         this.color = color;     }      // .... } 

Из наболевшего – теперь синтаксис инъекции зависимостей при использовании стандартной библиотеки может быть не таким раздутым.

Вместо нескольких повторений одного и того же:

public class AuthorizeService {     private readonly UserRepository _users;     private readonly PasswordHasher<User> _hasher;      public AuthorizeService(UserRepository repository,                             PasswordHasher<User> hasher)     {         _users = repository;         _hasher = hasher;     }      // .... } 

Можно сделать код более лаконичным:

public class AuthorizeService(UserRepository repository,                                PasswordHasher<User> hasher) {     private readonly UserRepository _users = repository;     private readonly PasswordHasher<User> _hasher = hasher;      // .... } 

Впрочем, в очередной раз в комплекте идёт некоторая сумятица. Параметры конструктора могут быть захвачены не только полями и свойствами, но и вообще чем угодно. Это приводит к тому, что можно делать так:

class Point(int posX, int posY) {     private int X { get => posX; }     private int Y { get => posY; }          // .... } 

Или так:

class Point(int posX, int posY) {     public (int X, int Y) GetPosition()         => (posX, posY);      public void Move(int dx, int dy)     {         posX += dx;         posY += dy;     }      // .... } 

Или даже так:

class Point(int posX, int posY) {     private int X = posX; // CS9124     private int Y = posY; // CS9124      public bool IsInArea(int minX, int maxX, int minY, int maxY)         => posX <= maxX && posX >= minX && posY <= maxY && posY >= minY; } 

Да, теперь можно не только случайно использовать поле вместо свойства, но и захваченный параметр конструктора вместо свойства или поля. Благо, такую очевидную ошибку, как сверху, компилятор отметит предупреждением о захвате параметра. Хотя использовать его как поле (но не через this!) всё же возможно:

class Point(int posX, int posY) {     public int X { get => posX; }     public int Y { get => posY; }      public void Move(int dx, int dy)     {         posX += dx;         posY += dy;     }      // .... } 

Никаких предупреждений. Совсем интересно становится, если мы заменим class на record (откуда этот синтаксис и пришёл):

record Point(int posX, int posY) {     public int X { get; } = posX;     public int Y { get; } = posY;      // .... } // .... var point = new Point(10, 20); Console.WriteLine(point); // Point { posX = 10, posY = 20, X = 10, Y = 20 } 

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

Если на первый пример есть предупреждение компилятора, то в этот раз ответственность на себя должен взять разработчик. В этом случае не допустить ошибку помогут более специализированные инструменты – статические анализаторы кода. Например, в PVS-Studio есть несколько сотен диагностических правил поиска дефектов кода на C#. И этот кейс непременно будет нами изучен.

В целом нововведение ощущается очень полезным, но сбить им с толку (особенно новичков), кажется, проще простого.

Краткий синтаксис работы с коллекциями

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

List<char> empty = []; List<string> names = ["John", "Mike", "Bill"]; int[] numbers = [1, 2, 3, 4, 5]; 

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

char[] characters = { 'a', 'b', 'c' }; List<char> characters = { 'a', 'b', 'c' }; // CS0622 

Улучшение коснулось и многомерных массивов (правда, только ступенчатых):

double[][] jagged = [[1.0, 1.5], [2.0, 2.5], [3.0, 3.5, 4.0]]; 

На возможности опустить неуклюжий new изменения не заканчиваются. При помощи оператора расширения «..» появляется возможность конкатенации коллекций:

Color[] lightPalette = [Color.Orange, Color.Pink, Color.White]; Color[] darkPalette = [Color.Brown, Color.DarkRed, Color.Black]; Color[] mixedPalette = [.. lightPalette,                         Color.Grey,                         .. darkPalette]; 

Научить свою коллекцию работать с этим синтаксисом придётся вручную, но большого труда это не представляет. Достаточно добавить метод, принимающий ReadOnlySpan и возвращающий экземпляр собственного класса, после чего добавить атрибут CollectionBuilder к классу:

[CollectionBuilder(typeof(IdCache), nameof(Create))] public class IdCache : IEnumerable<int> {     private readonly int[] _cache = new int[50];     public IEnumerator<int> GetEnumerator()         => _cache.AsEnumerable().GetEnumerator();     IEnumerator IEnumerable.GetEnumerator()         => _cache.GetEnumerator();              public static IdCache Create(ReadOnlySpan<int> source)         => new IdCache(source);     public IdCache(ReadOnlySpan<int> source)     {         for (var i = 0; i < Math.Min(_cache.Length, source.Length); i++)             _cache[i] = source[i];     } } // .... var john = _userRepository.Get(x => x.UserName == "john"); var oldUsersIds = _userRepository     .GetMany(x => x.RegistrationDate <= DateTime.Parse("01.01.2020"))     .Select(x => x.Id); IdCache cache = [.. oldUsersIds, john.Id]; 

Параметры анонимных функций по умолчанию

Ещё одно небольшое улучшение коснулось анонимных функций. Лямбда параметры теперь могут иметь значение по умолчанию:

var concat = (double x, double y, char delimiter = ',')     => string.Join(delimiter, x.ToString(enUsCulture), y.ToString(enUsCulture));  Console.WriteLine(concat(5.42, 3.17)); // 5.42,3.17 Console.WriteLine(concat(1.0, 9.98, ':')); // 1:9.98 

Кроме того, по отношению к ним теперь также можно использовать ключевое слово params:

var buildCsv = (params User[] users) => {     var sb = new StringBuilder();     foreach (var user in users)         sb.AppendLine(string.Join(",",                                   user.FirstName,                                   user.LastName,                                   user.Birthday.ToString("dd.MM.yyyy")));     return sb.ToString(); };  // .... Console.WriteLine(buildCsv(john, mary)); // John,Doe,15.04.1997 // Mary,Sue,28.07.1995 

Псевдонимы для любых типов

В C# 12 использование using для создания псевдонимов типов больше ничем не ограничено. Так что если вам хотелось пошалить, то теперь вы это можете:

using NullableInt = int?; using Objects = object[]; using Vector2 = (double X, double Y); using HappyDebugging = string; 

Во многих случаях использование псевдонимов может сказаться на коде скорее негативно (если вы работаете не один 🙂 ), но полезные сценарии использования определённо имеются. Например, если у вас было подобное безобразие с кортежами:

public class Square {     // ....     public (int X, int Y, int Width, int Height) GetBoundaries()         => new(X, Y, Width, Height);     public void SetBoundaries(         (int X, int Y, int Width, int Height) boundaries) { .... } } 

То ситуацию можно улучшить:

using Boundaries = (int X, int Y, int Width, int Height); // .... public class Square {     // ....     public Boundaries GetBoundaries()         => new (X, Y, Width, Height);     public void SetBoundaries(Boundaries boundaries) { .... } } 

Хоть в целом наличие таких кортежей — это повод призадуматься, но там, где это всё-таки необходимо (либо при рефакторинге), это поможет улучшить читаемость.

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

Сходу придумать кейс, который можно покрыть статическим анализатором кода, не получилось. А это значит, что потенциальные ошибки проявятся позже и будут более изощрёнными и трудноуловимыми, ведь проблема кроется в подходе. Если столкнётесь с чем-нибудь интересным, то присылайте примеры кода нашей команде.

Доработка nameof

Выражение nameof теперь может полностью захватывать экземплярные члены класса из статических методов, инициализаторов и атрибутов. Раньше было странное ограничение, позволяющее получить, например, имя самого поля класса, но не его членов:

public class User {     [Description($"Address format is {         nameof(UserAddress.Street)} {nameof(UserAddress.Building)}")] // CS0120     Address UserAddress { get; set; }     // .... } 

Теперь такой проблемы не стоит, и nameof можно использовать во всех вышеупомянутых контекстах:

public class User {     [Description($"Address format is {         nameof(UserAddress.Street)} {nameof(UserAddress.Building)}")]     Address UserAddress { get; set; }      public string AddressFormat { get; } =            $"{nameof(UserAddress.Street)} {nameof(UserAddress.Building)}"; }      public static string GetAddressFormat()         => $"{nameof(UserAddress.Street)} {nameof(UserAddress.Building)}"; } 

Inline массивы

Переходим к нишевым нововведениям, полезным не всем, но всё же привносящим изменения в язык. В данном случае речь идёт о массивах фиксированного размера, размещающихся на стеке в неразрывном участке памяти. Ожидаемо, понадобится это главным образом для нужд AOT компилятора и тем, кому нужно писать действительно высокопроизводительный код. Чтобы создать такой массив, понадобится немного магии. А именно объявить структуру, у которой будет единственное поле (определяющее тип массива), и отметить её атрибутом InlineArray, в котором указан размер массива.

Вот как это выглядит:

[System.Runtime.CompilerServices.InlineArray(5)] public struct IntBuffer {     private int _element0; } // .... var buf = new IntBuffer(); for (var i = 0; i < 5; i++)     buf[i] = i;              foreach (var e in buf)     Console.Write(e); // 01234 

Перехват кода

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

var worker = new Worker(); worker.Run("hello"); // Worker says: hello worker.Run("hello"); // Interceptor 1 says: hello worker.Run("hello"); // Interceptor 2 says: hello // .... class Worker {     public void Run(string phrase)         => Console.WriteLine($"Worker says: {phrase}"); }  static class Generated {     [InterceptsLocation("Program.cs", line: 3, character: 7)]     public static void Intercept1(this Worker worker, string phrase)         => Console.WriteLine($"Interceptor 1 says: {phrase}");      [InterceptsLocation("Program.cs", line: 4, character: 7)]     public static void Intercept2(this Worker worker, string phrase)         => Console.WriteLine($"Interceptor 2 says: {phrase}"); } 

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

Хоть польза для AOT здесь также имеется, фокус приходится на кодогенерацию. Например, можно было бы помечтать о библиотеках, упрощающих работу с аспектно-ориентированным программированием. Однако ещё более заманчиво звучат фреймворки для юнит-тестов – наконец-то можно будет перестать делать по интерфейсу на каждый класс, просто чтобы замокать его в тестах. По крайней мере, это активно дискутируется в сообществе, что приятно.

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

Заключение

Хоть на первый взгляд список изменений не кажется огромным (особенно сравнивая с предыдущими релизами), лично у меня интерес вызывают почти все, пусть иногда и вместе с опасениями :). Да и говоря начистоту, ещё не все изменения прошлых лет удалось осмыслить и начать вдумчиво применять на практике. Как, кстати, с этим у вас? Ну и C# 12, конечно, тоже давайте обсудим.

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

Если хотите следить за мной и выходом статей на тему качества кода, то подписывайтесь на тви… экс-twitter мой, корпоративный, или ежемесячный дайджест лучших статей.

А если хотите поделиться этой статьей с англоязычной аудиторией, то прошу использовать ссылку на перевод: What’s new in C# 12: overview.


ссылка на оригинал статьи https://habr.com/ru/articles/768802/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *