Уже середина осени, а это значит, что новая версия 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.

Комментарии (46)


  1. ritorichesky_echpochmak
    20.10.2023 11:07
    +2

    Но ведь первичный конструктор сделан таким именно для того, чтобы не нужно было в тушке и конструкторе руками пересохранять эти свойства, если не нужно отдавать их наружу?!

    public class AuthorizeService(UserRepository repository, 
                                  PasswordHasher<User> hasher)
    {
        //private readonly UserRepository _users = repository; // We don't need this!
        //private readonly PasswordHasher<User> _hasher = hasher;
    
        // ....
    }

    Я как-то больше ждал что-нибудь типа создания новых объектов по прототипу через with для стандартных классов, как это сделано у записей, чтобы не городить размашистые перекладывалки из пустого в порожнее из клиентской модели в бизнесовую, из бизнесовой в БД и не обмазываться мапперами там где не нужно

    var growingDegreeDays = coolingDegreeDays with { BaseTemperature = 41 };

    Либо в MVC каких-нибудь весёлых аттрибутов, позволяющих проконтролировать, что какой-то аттрибут в route совпадает с оным в модели


    1. Volokhovskii Автор
      20.10.2023 11:07

      Спасибо за замечание! Могу поинтересоваться об источнике этого утверждения? Я как-то слишком консервативен чтобы даже помыслить, что объект может оперировать внутри себя не своей частью (this-то не применим), но возможно стоит осмыслить? :)

      А то сходу звучит сомнительно - иногда хочется получить доступ к зависимостям через рефлексию. Тут это (внезапно) возможно, но не традиционным способом.


      1. ritorichesky_echpochmak
        20.10.2023 11:07
        +3

        Что значит не своей частью? Это вполне себе field этого объекта. С невозможностью ткнуть this - это скорее всего баг имплементации конкретно этого сахара. Причём если имена осмысленные, а не везде одно и то же имя (но почему-то с разными значениями?), то this использовать и не приходится. В любом случае нет смысла дублировать приватные штуки свойствами - только перфоманс ухудшает.


        1. Volokhovskii Автор
          20.10.2023 11:07
          +1

          С невозможностью ткнуть this - это скорее всего баг имплементации конкретно этого сахара

          Боюсь что это буквально тот случай, когда не баг, а фича :) Из документации:

          Primary constructor parameters aren't members of the class. For example, a primary constructor parameter named param can't be accessed as this.param.

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

          В любом случае нет смысла дублировать приватные штуки свойствами - только перфоманс ухудшает.

          Обращаясь туда же:

          Primary constructor parameters may not be stored if they aren't needed.

          Перепроверил на rc2 - никакого дублирования нет, если аргумент нигде за пределами инициализации не захватывается.


          1. ritorichesky_echpochmak
            20.10.2023 11:07
            +3

            Очень странно написано, вот же они)

            Не мемберы - это когда они где-то снаружи. А тут они только изнутри и доступны, как и подобает приватным филдам по канону)


            1. Volokhovskii Автор
              20.10.2023 11:07

              Кажется, мы вступаем на холиварную территорию, но по-моему это как раз-таки больше похоже если не на баг, то на костыль). В конце концов, в такой конфигурации даже protected не используешь, не говоря уже о в целом непривычном для C# синтаксисе и поведении.
              В общем, я всё ещё с некоторым опасением смотрю на такую возможность.


            1. funny_falcon
              20.10.2023 11:07
              +1

              “никакого дублирования нет, если аргумент за пределами инициализации не захватывается”

              В вашем примере захватывается. Так что, противоречия нет.


              1. Volokhovskii Автор
                20.10.2023 11:07

                Каком из? Большая их часть не задумывалась как призыв к действию. И как я уже говорил выше - скорее наоборот х)


                1. funny_falcon
                  20.10.2023 11:07

                  Я отвечал вашему оппоненту.


  1. nkozhevnikov
    20.10.2023 11:07
    +31

    Как сказал мой бывший тимлид...

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


    1. hVostt
      20.10.2023 11:07
      +1

      Отторжение "сахара" стало уже притчей во языцех.

      Но давайте посмотрим на следующие тезисы:

      1. Избыточность языковых конструкций была полезна во времена бедных IDE, когда в любом текстовом редакторе без подсветки, подсказок и хинтов текст программы должен был понятен человеку.

      Отсюда, неприятие различных var, let и еже с ними, в Java до сих пор с этим борются.

      1. Всё, что может делать машина, а не человек -- должна делать машина. Поощрение обезьяньего труда я не понимаю. Если можно написать меньше букв с тем же результатом, с учётом продвинутых IDE, то нужно к этому стремиться.

      2. Консервативные языки программирования, в которые разработчики бояться добавить какой-либо сахарок, вытесняются молодыми и прогрессивными.

      Расширить синтаксис, чтобы не пострадало тонна легаси кода, задача отнюдь не тривиальная. Приходится идти на жертвы, иначе как грибы вылезут унылые статьи "это было последней каплей! уйду на PHP/Python/etc." :)


      1. rg_software
        20.10.2023 11:07

        Вы правильно пишете, что всё это "тезисы", и соглашаться с ними вовсе необязательно. Разумный объём сахара полезен, но конкретно в случае C# мы уже вышли за все рамки. Если функцию можно описать 100500 способами, то явно в консерватории что-то не то.

        Соответственно, примеры "консервативных языков", которые были вытеснены именно по причине консерватизма, наверно, можно пересчитать по пальцам одной руки. Язык изучают для того, чтобы с его помощью что-то создавать. А тут получается как с апдейтом Thunderbird: не радуешься новому, а думаешь, чего они сломали на сей раз, и как с этим жить. Даже если эти фичи лично мне не нужны, всё равно приходится разбираться хотя бы на уровне "чтения со словарём".


      1. a-tk
        20.10.2023 11:07
        +1

        «Есть всего два типа языков программирования: те, на которые люди всё время ругаются, и те, которые никто не использует.»

        Bjarne Stroustrup.


  1. isee_sparksfly
    20.10.2023 11:07

    Интересно, спасибо. Расскажите, пожалуйста, как это поможет упростить работу разработчикам?


    1. Volokhovskii Автор
      20.10.2023 11:07
      +5

      Давайте попробуем суммировать самое важное, хотя, кажется, я пытался осветить это в статье :)

      1. Первичные конструкторы - синтаксический сахар, позволяющий уменьшить визуальный шум при инъекциях зависимостей

      2. Выражения коллекций - аналогично, позволяет несколько удобнее и более читаемо производить операции с коллекциями, синтаксис с которыми до этого был немного неуклюжим.

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

      4. Inline массивы и перехватчики - непрямая помощь разработчикам, путём улучшения качества (оптимизации inline массивами) и расширения функционала (перехватчики кода) библиотек.


      1. a-tk
        20.10.2023 11:07
        +1

        Первичный конструктор в виде пропозала появился к C#6, но вошёл только в 12.

        Перехватчики нужны для развития source generators.


  1. ssergs
    20.10.2023 11:07
    +1

    Перехватчики выглядят заманчиво, но привязывать их через файл и номер строки - это скорее для програм работающих с кодом, а не для реальной жизни. А то Enter поставил и "привет".
    Можно ли через перехватчик вызвать код, который перехватили? (сделать обвертку)


    1. Volokhovskii Автор
      20.10.2023 11:07

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

      Конечно! В статье я это опустил. но там ещё и абсолютный путь до файла нужен. Фича нужна для поддержки кодогенераторами и оптимизациями в .NET, а не для обычных проектов.

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


  1. Oceanshiver
    20.10.2023 11:07
    +3

    Первичный конструктор это жесть какая-то, ощущение что фичи делаются просто чтобы сделать, а не действительно что-то полезное


    1. Volokhovskii Автор
      20.10.2023 11:07
      +3

      Не знаю как у вас, но у меня уже глаз начал дёргаться, когда надо три раза подряд написать имя зависимости, 2 из которых дублируя ещё и тип (помощь от Visual Studio помогает, но полностью не спасает). Так что я всё же рад новой возможности - глазу теперь можно дёргаться на один раз меньше X)


      1. Zufir
        20.10.2023 11:07
        +1

        Alt+Enter, Introduce local field


  1. ka4ep
    20.10.2023 11:07
    +1

    Наконец-таки можно class Foo { List<Bar> Bars { get; } }
    new Foo { Bars = [new Bar{...}, new Bar{...}] };


    1. AgentFire
      20.10.2023 11:07
      +1

      А как насчёт new () { Bars = [new (){...}, new (){...}] }; ?


  1. SergioT4
    20.10.2023 11:07

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

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


    1. Volokhovskii Автор
      20.10.2023 11:07

      Там имеются проверки (если позиция не указывает совсем никуда или не совсем туда, оно ругнётся или предложит другой вариант). Дополнительный контроль же кажется ненужным - этот код не для человека (при работе с таким синтаксисом приходилось бы пить успокаивающие), в идеале он его вообще никогда не увидит.


      1. SergioT4
        20.10.2023 11:07

        Я не про то что человек его должен видеть (хотя иногда в сгенерированный код можно посмотреть), а про то что генераторы кода пишут люди и есть шанс что при похожих методах в одном и том же классе случайно будет указан не правильный вариант. Но в принципе это уже конечно частности.

        Сам факт подобной функциональности, уже открывает интересные возможности для AOT на уровне исходных кодов. Я какое-то время назад уже видел статью про AOT на уровне исходных кодов, но там, в силу отсутствия поддержки на уровне языка, было не так удобно. Тогда не хватило времени чтобы попробовать тот подход, надеюсь сейчас это будет полегче.


  1. Ydav359
    20.10.2023 11:07

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

    Вы предлагаете использовать для решения этой проблемы продукт, который это еще не умеет?


    1. Volokhovskii Автор
      20.10.2023 11:07
      +8

      Немного проблематично поддержать версию языка, которая ещё не вышла :(


  1. mikegordan
    20.10.2023 11:07

    Это плохо. Я ожидал что если имена в конструкторе и поля совпадают , то они инициализируются без присвоения.


    1. a-tk
      20.10.2023 11:07
      +1

      Частный случай поведения обычно бывает плохим решением.


  1. Senyaak
    20.10.2023 11:07
    +2

    Спасибо, аж захотелось что-нибудь на сишарпе сварганить, лет 5-6 его уже не трогал???? хотя страшно окунаться в совсем забытую да и скорее всего уже совсем другую экосистему ????


    1. Volokhovskii Автор
      20.10.2023 11:07
      +2

      На мой взгляд - самое время! Многие выражают скепсис по поводу нововведений (да и я немного этому способствую) ), но по-моему с каждым обновлением язык становится всё приятнее для использования. А учитывая что как раз 6 лет как .NET стал работать под Linux - оправданий использовать что-то ещё исчезающе мало :)


  1. ImagineTables
    20.10.2023 11:07

    А в чём преимущество именованных через using кортежей перед обычным типом?


    1. Volokhovskii Автор
      20.10.2023 11:07

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


      1. DrXak
        20.10.2023 11:07

        Не понимаю, а можете ли показать пример где

        using Vector2 = (double X, double Y);

        будет лучше, чем

        record /*struct*/ Vector2(double X, double Y);?

        Кортежи обычно имеют смысл на одноразовое что-то, а если ты создаёшь алиас, то значит используешь её в разных местах, и тогда имеет смысл написать отдельный тип.


        1. Volokhovskii Автор
          20.10.2023 11:07

          На мой взгляд, ответ в вашем же комментарии:

          Кортежи обычно имеют смысл на одноразовое что-то

          Только "одноразовое" стоит воспринимать в контексте одного класса (файла). В таком формате я бы отдал предпочтение кортежу - его семантика однозначно показывает что это bag of data и ничего больше, в то время как к структурам и записям могут быть вопросы (глобальный using сразу выносим за скобки).
          Но это, на самом деле, вкусовщина, просто раньше ограничение на using alias было, из-за чего приходилось выкручиваться вышеописанным образом, а теперь можно действовать напрямую.


          1. a-tk
            20.10.2023 11:07

            Вот ещё бы сделали эти using-и с в любом скоупе - не только файле - было бы совсем хорошо...


        1. pankraty
          20.10.2023 11:07
          +1

          Если вы используете стороннюю библиотеку, в которой такие структуры не определены, а вам хочется свой код сделать более лаконичным и наглядным - то использование алиасов позволит это сделать без введения доп. типов, конвертации, нагрузки на GC и т.п. Понятно, что этот вариант использования является довольно узким, и без него вполне можно же жить, ну так это вообще ко всем алиасам относится (тот же `using static ...` всего лишь позволяет чуточку меньше символов писать. Мелкая, иногда приятная, но точно не необходимая "плюшка")


  1. Evengard
    20.10.2023 11:07
    +5

    inline массивы реализованы как-то прям сверх костыльно. Да и поддержка синтаксиса выражений коллекций через атрибуты для кастомных классов тоже выглядит костылём, что мешало запилить обыкновенный интерфейс отдельного класса-билдера + дженерик интерфейс на сам класс коллекции?


  1. ChessMax
    20.10.2023 11:07
    +2

    Вообще список изменений выглядит так себе. Да есть приятные мелочи (которые давно должны были быть сделаны), но где DU? Похоже не дождемся?


    1. Fedorkov
      20.10.2023 11:07

      Что такое DU?



    1. hVostt
      20.10.2023 11:07

      Сделать адекватный DU поверх легаси-кода может позволить себе только какой-нибудь скриптовый ЯП, где это будет просто приблуда синтаксиса поверх хеш-массива. Здесь же проблем выше крыши, учитывая высокие требования к производительности. Далеко не тривиальная задача.

      Пока в качестве DU можно применять наследование, или generic-кортежи.


      1. ChessMax
        20.10.2023 11:07

        Безусловно скорее всего есть какие-то подводные камни и сложности, учитывая большую кодовую базу языка, возраст и количество уже реализованных фич. Но производительность? Честно говоря, не совсем понимаю, что вы имеете в виду и как она мешает реализовать DU. DU в ООП языках это базовый класс и наследники. И какое-то ключевое слово или атрибут позволяющий указать, что это DU. Плюс exhaustiveness в свитч. И как бы все. Причем здесь производительность не понимаю. Классы и наследование уже давным-давно реализовано. Добавить ключевое слово или атрибут, так же не супер сложная задача. Ну и проверка на полноту тоже не рокет саенс.

        только какой-нибудь скриптовый ЯП

        Спорно. В том же Dart-е реализовали. И возможно, когда-нибудь и в C# реализуют.

        приблуда синтаксиса поверх хеш-массива

        А причем здесь хеш-массивы?


  1. xxxDef
    20.10.2023 11:07

    Автор что то усложнил с первичными конструкторами. Там вроде все просто - если параметр первичного конструктора используется в коде класса, то для него создается приватное поле, а если не используется - не создается. И все.


    1. Volokhovskii Автор
      20.10.2023 11:07

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