Уже середина осени, а это значит, что новая версия C# на пороге. Самое время узнать, какие изменения настигнут язык совсем скоро. Хоть количество нововведений в этот раз уступает предыдущим релизам, интересных среди них немало.
Одно из самых заметных 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 теперь может полностью захватывать экземплярные члены класса из статических методов, инициализаторов и атрибутов. Раньше было странное ограничение, позволяющее получить, например, имя самого поля класса, но не его членов:
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)}";
}
Переходим к нишевым нововведениям, полезным не всем, но всё же привносящим изменения в язык. В данном случае речь идёт о массивах фиксированного размера, размещающихся на стеке в неразрывном участке памяти. Ожидаемо, понадобится это главным образом для нужд 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 мой, корпоративный, или ежемесячный дайджест лучших статей.
По моему абсолютно все нововведения C#12 являются дичайшими антипаттернами. Они не дают ничего, а вместо этого провоцируют создавать грязный неочевидный код. Если вдруг придется переходить на C#12, то анализаторы кода должны выдавать предупреждения на новых возможностях языка, а лучше каким то образом запрещать их использовать
Почему вы так считаете? Я могу выразить опасения только насчёт первичных конструкторов и using alias, но игнорировать их преимущества (особенно первого - ох, как мне надоело 3 раза подряд писать имена зависимостей :) ) тоже не хочется. Что касается остальных нововведений - такими спорными они мне не кажутся, просто повышение качества жизни разработчика.
Качество жизни разработчика определённо зависит не от кода.
Поддерживаю. В целом, все было бы не так плохо, если бы не добавление interceptor’ов, которые полностью ломают все столпы ООП. По сути, теперь можно не париться про создание правильной иерархии наследования или реализации интерфейсов в коде, а можно втупую вклиниться в любой чужой код и заменить методы своими как душе угодно, наплевав на абстракцию, инкапсуляцию и полиморфизм! Это же аналог оператора goto, который ломает нормальный дизайн приложений. Этого нельзя было добавлять НИКОГДА.
Всё же это не аналог goto хотя бы потому, что это не штатное средство языка, а механизм для кодогенераторов (кто в своём уме будет пользоваться таким синтаксисом). Это определённо может быть способом выстрелить себе в ногу, но не думаю что особенно критичным. Есть и более простые способы это сделать :)
Français
6.80 K