Совсем скоро выйдет новая версия языка C#, а мы продолжаем серию ежегодного обзора нововведений. Изменений в этом году больше, чем в прошлом, что радует. Есть как важные изменения, так и весьма узкоспециализированные. Давайте посмотрим на них более детально.

Инициализация объекта по индексу "от конца"

Начнём освещение новых изменений с довольно простого по смыслу нововведения — инициализация объекта с помощью неявного оператора индекса "от конца" — ^.

Ранее можно было указывать индексы объекта при инициализации только "от начала":

var initialList = Enumerable.Range(1, 10);
var anotherList = new List<int>(initialList)
{
    [9] = 15
};
Console.WriteLine(string.Join(" ", anotherList));
// 1 2 3 4 5 6 7 8 9 15

В новой версии языка можно указать индекс, отсчитывая номер элемента "от конца":

var initialList = Enumerable.Range(1, 10);
var list = new List<int>(initialList)
{
    [^1] = 15
}; 
Console.WriteLine(string.Join(" ", anotherList));
// 1 2 3 4 5 6 7 8 9 15

Очевидно, что данный функционал поддерживается во всех классах, которые реализуют перегрузку индексатора с аргументом типа структуры Index:

void Check()
{ 
    var initTestCollection = new TestCollection<int>(Enumerable.Range(1, 10));
    var anotherTestCollection = new TestCollection<int>(initTestCollection)
    {
        [^5] = 100
    };
    Console.WriteLine(string.Join(" ", anotherTestCollection));
    // 1 2 3 4 5 100 7 8 9 10
}

class TestCollection<T> : IEnumerable<T>
{
    T[] _items;

    public T this[Index index]
    {
        get => _items[index];
        set => _items[index] = value;
    }
    // ....
}

Как говорилось ранее, изменение простое и понятное. Обычно инициализатор используется для непрерывного определения значений индексов. Реже используется указание значений конкретных индексов. Есть предположение, что данный кейс будет использоваться ещё реже.

Partial свойства и индексаторы

Новая версия языка предлагает расширить возможность частичного объявления и реализации содержимого классов. До этого можно было указывать на фактор частичности классам, структурам, интерфейсам и методам. Теперь есть возможность указать модификатор partial свойствам и индексаторам. Логика привычная: в одной части указывается объявление, а в другом — реализация.

Для примера, обратимся к ранее объявленной тестовой коллекции TestCollection<T> и немного модифицируем код:

partial class TestCollection<T> : IEnumerable<T>
{
    private T[] _items;

    public partial int Count { get; } // Объявление свойства
    public partial T this[Index index] { get; set; } // Объявление индексатора
    // ....
}
partial class TestCollection<T>
{
    public partial int Count => _items.Length; // Реализация свойства
    public partial T this[Index index] // Реализация индексатора
    {
        get => _items[index];
        set => _items[index] = value;
    }
}

Теперь данный класс имеет объявление индексатора класса и свойства Count в одной части, а реализацию — в другой.

Не секрет, что partial классы используются для генерации исходного кода. Например, при использовании регулярных выражений, скомпилированных во время сборки с помощью атрибута GeneratedRegexAttribute. Или для валидации данных при наследовании от класса ObservableValidator. Данное небольшое нововведение поможет как расширить область применения кодогенерации, так и в бо́льшей степени разграничивать точки объявления и реализации кода в собственных объёмных классах.

Params коллекции

Невероятно, но факт! В C# версии 13 будет добавлена такая желаемая (автором, как минимум) поддержка коллекций при модификаторе params. Теперь методы, для которых мы так усердно переводили коллекции в массивы, станут удобнее в обращении. Кода станет меньше, и повысится читабельность.

Один из понятных случаев передачи коллекций в такие методы — работа с базами данных. Часто необходимо передать некоторую выборку, полученную с помощью LINQ метода Where, или перечень идентификаторов записей с помощью Select. При их использовании, результат представляет из себя коллекцию IEnumerable<T>, которую приходится преобразовывать в массив T[], так как методы с модификатором params в аргументах ограничивают своё использование. В следующем обновлении языка данного действия не потребуется — можно без проблем писать методы, принимающие в качестве аргумента params коллекции.

В дополнение к массивам, для указания типа при ключевом слове params станут доступны: ReadOnlySpan<T>, Span<T> и наследники, реализующие IEnumerable<T> (List<T>, ReadOnlyCollection<T>, ObservableCollection<T> и подобные им).

Интересно взглянуть на приоритет вызова методов при наличии перегрузок. Рассмотрим ситуацию с несколькими перегрузками одного метода и передачей литерала в качестве аргумента:

void Check()
{
    ParamsMethod(1);
}
void ParamsMethod(params int[] values) // До C# 13
{
    // (1)
}
void ParamsMethod(params IEnumerable<int> values) // После C# 13
{
    // (2)
}
void ParamsMethod(params Span<int> values) // После C# 13
{
    // (3)
}
void ParamsMethod(params ReadOnlySpan<int> values) // После C# 13
{
    // (4) <=
}

Выполнение метода Check() приведёт к вызову перегрузки под номером 4, в качестве типа params которого указано ReadOnlySpan<int>. Есть мысль, что тут имеет место быть оптимизационный момент, дабы избежать дополнительного выделения памяти при работе с коллекцией.

Если же ограничить выборку перегрузок методов до Span<int> и ReadOnlySpan<int>, то передача массива приведёт к вызову метода с типом аргумента Span<int>. Такое поведение вызвано тем, что происходит неявное преобразование массива в Span<int>. Если же передать в метод ParamsMethod массив, который инициализируется при вызове, то произойдёт вызов перегрузки ReadOnlySpan<int>, так как фактически мы не имеем ссылок на коллекцию в коде:

void Check()
{
    int[] array = [1, 2, 3];
    ParamsMethod(array);     // (1)
    ParamsMethod([1, 2, 3]); // (2)
}
void ParamsMethod(params Span<int> values) // <= (1)
{
    // ....
}
void ParamsMethod(params ReadOnlySpan<int> values) // <= (2)
{
    // ....
}

При работе с шаблонными методами приоритет практически всегда отдаётся ReadOnlySpan<T>, если нет реализации под конкретный тип. И даже передача List<T> приведёт к вызову метода params ReadOnlySpan<T>, а не IEnumerable<T>, что выглядит весьма неочевидно и потенциально опасно.

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

Атрибут приоритизации перегрузок

Для добавления приоритета одной из перегрузок был добавлен новый атрибут пространства имён System.Runtime.CompilerServices — OverloadResolutionPriorityAttribute. Название громоздкое, но точно отражает суть. Как говорит сам Microsoft, данный атрибут по большей части нужен для разработчиков API, которые хотят "мягко" перевести своих юзеров с одной перегрузки метода на другую, где может быть лучшая реализация.

Принимая во внимание не самый очевидный приоритет выбора компилятором перегрузок, можно явно указать, какой из методов будет использоваться в первую очередь. Например, в контексте двух перегрузок ParamsMethod с типами аргументов ReadOnlySpan<T> и Span<T>, можно указать компилятору на приоритет метода с типом Span<T>:

void Check()
{
    int[] array = [1, 2, 3];
    ParamsMethod(array);     // (1)
    ParamsMethod([1, 2, 3]); // (2)
}
[OverloadResolutionPriority(1)]
void ParamsMethod(params Span<int> values) <= (1)(2)
{
    // ....
}
void ParamsMethod(params ReadOnlySpan<int> values)
{
    // ....
}

Можно заметить, что атрибут принимает одно значение — приоритет перегрузки. Чем выше данное значение, тем приоритетнее метод. Дефолтное значение у каждого из методов — 0.

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

Вышеописанная особенность области видимости может стать неожиданной проблемой для разработчика. Например, может вызваться устаревший код, с неактуальной логикой, что может повлечь неприятности (особенно в работающем приложении). Для предотвращения подобных ситуаций на рынке существуют инструменты, помогающие разработчикам находить неочевидные ошибки — статические анализаторы кода. Данное нововведение натолкнуло нас на идею добавления нового диагностического правила для нашего C# анализатора PVS-Studio вдобавок к сотням уже существующим.

Новый класс Lock

Работа с синхронизацией потоков была улучшена. На замену object пришёл полноценный класс Lock из пространства имён System.Threading. Данный класс призван сделать код более понятным и эффективным. В дополнение к этому класс имеет следующие методы для работы с ним:

  • Enter() — вход в участок блокировки.

  • TryEnter() — попытка моментального входа в участок блокировки, если это допустимо. Возвращает результат попытки входа в виде bool.

  • EnterScope() — получение структуры Scope, которую можно применить вместе с оператором using.

  • Exit() — выход из участка блокировки.

Также имеется свойство IsHeldByCurrentThread, с помощью которого можно узнать, удерживается ли блокировка текущим потоком.

Если использовать оператор lock в привычном формате, то код принимает следующий вид:

class LockObjectCheck
{
    Lock _lock = new();

    void Do()
    {
        lock (_lock)
        {
            // ....
        }
    }
}

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

class LockObjectCheck
{
    Lock _lock = new();

    void Do()
    {
        _lock.Enter();
        try
        {
            // ....
        }
        // ....
        finally
        {
            if (_lock.IsHeldByCurrentThread)
                _lock.Exit();
        }
    }
}

Как говорилось ранее, можно использовать экземпляр структуры Scope, полученного с помощью вызова метода EnterScope, чтобы обеспечить правильное использование IDisposable экземпляра. Код в таком случае выглядит следующим образом:

using (var scope = _lock.EnterScope())
{
    // ....
}

Добавление класса блокировок кода привносит ясность для новичков и больше элементов управления для опытных C# программистов. Переход на новый формат ожидается простым и безболезненным, так как сложный рефакторинг не потребуется.

Новая escape-последовательность

Разработчики языка представили новый символ escape-последовательности \e. Он был добавлен для замены существующего \x1b, который не рекомендуется использовать. Текущая проблема в том, что стоящие далее символы могут интерпретироваться как валидные шестнадцатеричные значения, из-за чего станут частью указанной escape-последовательности. Данный кейс поможет избегать непредвиденных ситуаций.

Улучшение работы групп методов с естественными типами

C# 13 улучшает алгоритм подбора подходящих кандидатов при работе с группами методов и естественными типами. Естественные типы — типы, определённые компилятором (например, с помощью var).

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

Интерфейсы и ref struct

В далёком C# 7.2 были добавлены ref struct, которые в 13 версии языка получили существенные изменения. Напомним, что основной особенностью такой конструкции является исключительное выделение памяти на стеке, без возможности перехода в управляемую кучу. Это можно использовать для повышения безопасности и производительности приложения (подробнее). Ярким представителем таких структур можно назвать знакомый Span<T> и его Readonly-аналог — ReadOnlySpan<T>.

Наследование

До этого момента у ref struct были свои ограничения, в том числе с наследованием от интерфейсов. Ранее это было запрещено во избежание боксинга. При подобной попытке можно было получить ошибку "ref structs cannot implement interfaces". Сейчас же данное ограничение было снято, что позволяет наследоваться от интерфейсов:

interface IExample
{
    string Text { get; set; }
}
ref struct RefStructExample : IExample
{
    public string Text { get; set; }
}

Но не всё так просто. При попытке каста экземпляра структуры к интерфейсу мы получаем ошибку "Cannot convert type 'RefStructExample' to 'IExample' via a reference conversion, boxing conversion, unboxing conversion, wrapping conversion, or null type conversion". Это является одним из новых ограничений при использовании ref struct, обеспечивающее ссылочную безопасность.

Анти-ограничение allows ref struct

Данное анти-ограничение позволяет передавать в шаблонные методы экземпляры *ref *структур. При попытке передачи подобной структуры в версии C# 12 можно было получить сообщение: "The type 'RefStructExample' may not be a ref struct or a type parameter allowing ref structs in order to use it as parameter 'T' in the generic type or method 'ExampleMethod<T>(T)'". Сейчас же, в месте указания ограничителей метода можно добавить конструкцию, разрешающую использование ref struct:

void Check()
{
    var example = new RefStructExample();
    ExampleMethod(example);
}
void ExampleMethod<T>(T example) where T: IExample, allows ref struct
{
    // ....
}

И да, всё отлично работает. В таком методе можно без проблем описывать всю интересующую обобщённую логику для наследников IExample, включая RefStructExample.

Примечание. allows ref struct — первое анти-ограничение. Ранее подобные конструкции лишь запрещали использование сторонних типов.

Смотря на данное нововведение, можно сказать, что разработчики языка позволяют нам всё больше увлекаться построением иерархий. И это хорошо! Подобные наследования можно использовать как для объединения логики, так и для добавления обязательств реализации для наследуемых сущностей.

Хочется выделить новый тип анти-ограничителей на примере allows ref struct, который меняет подход к указанию спецификации методов. Сама идея включения некоторого функционала выглядит интересно и многообещающе. Интересно будет узнать, какие новые типы анти-ограничителей подготовит для нас в будущем команда разработки C#.

ref и unsafe в итераторах и асинхронных методах

Продолжая тему ref struct нельзя не отметить нововведение, которое расширит места их использования. В асинхронных методах теперь можно объявлять ref локальные переменные и экземпляры ref структур.

Например, при попытке объявления экземпляра структуры ReadOnlySpan<int> в ранней версии C#, компилятор выводит ошибку "Parameters or locals of type 'ReadOnlySpan<int>' cannot be declared in async methods or async lambda expressions". В новой версии языка данной проблемы нет, но стоит учитывать, что осталось ограничение, запрещающее иметь ref в аргументах таких методов.

В методах-итераторах (методы, использующие оператор yield) теперь также можно использовать локальные ref переменные, но есть ограничение по их выводу:

IEnumerable<int> TestIterator()
{
    int[] values = [1, 2, 3, 4, 5];

    ref int firstValue = ref values[0];
    ref int lastValue = ref values[values.Length - 1];
    yield return firstValue;
    yield return lastValue;
    // A 'ref' local cannot be preserved across 'await' or 'yield' boundary
}

Также в новой версии языка итераторы и асинхронные методы станут поддерживать модификатор unsafe, позволяющий производить любые операции с указателями. При этом в итераторах потребуется безопасный контекст для таких конструкций как yield return и yield break.

Заключение

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

Что вы думаете по этому поводу? Идёт ли Microsoft в сторону развития языка или стоит на месте, раскрывая те возможности, которые до этого момента почему-то не были реализованы? Пишите ваше мнение в комментариях.

Документация Microsoft по списку изменений в C# 13 доступна по ссылке. Если же у вас есть желание прочитать наши предыдущие обзорные статьи по нововведениям в языке C#, то вот список всех статей прошлых лет:

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

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

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


  1. Ydav359
    22.10.2024 12:10

    Не совсем понял, чем Lock принципиально отличается от Monitor


    1. DoctorKrolic
      22.10.2024 12:10

      Принципиально ничем, но, как заявляют разрабы рантайма, использование Lock должно быть немного быстрее старого подхода, а в долгосрочной перспективе, когда большинство перейдёт на Lock, в JIT могут добавить специфические для этого типа оптимизации, чтобы ещё сильнее ускорить код


    1. DstRoses Автор
      22.10.2024 12:10

      Как ответил @DoctorKrolic, по-сути это одно и то же. Думаю, с помощью нового класса Lock будет проще переехать с Monitor на локи (нейминг методов больно схож).


    1. A1lfeG
      22.10.2024 12:10

      Раньше надо было создавать пустой объект, называть его ЧетоТамЛок, сейчас для этого есть специальный класс. Можно использовать в скоупе юзинг. По сути все.


    1. mynameco
      22.10.2024 12:10

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


  1. commanderkid
    22.10.2024 12:10

    Спасибо за обзор. Хотел уточнить, а вот если это фрагмент не выполнится (будет false) , не будет ли выброшено исключение?

    finally
    {
        if (_lock.IsHeldByCurrentThread)
            _lock.Exit;
    }

    Наверное, где-то выше можно цикл разместить, в который эта часть будет включена?


    1. DstRoses Автор
      22.10.2024 12:10

      В теории свойство должно обеспечить надежность в этом плане. Если мы обратимся из другого потока, то у нас и правда может выпасть исключение "SynchronizationLockException", что неохорошо. Я уверен, что можно круче обработать точку выхода из лока, чем показано в статье.
      С другой стороны можно использовать Scope структуру, Dispose() которой будет применен при выходе из области видимости.


  1. nronnie
    22.10.2024 12:10

    "Primary constructors" по-нормальному так и не сделали? :(


    1. DstRoses Автор
      22.10.2024 12:10

      Развития эта фича пока не получила. Я больше жду, когда field добавят для свойств, т.к. выглядит очень удобно и потенциально уменьшит количество кода.


      1. nronnie
        22.10.2024 12:10

        Я больше жду, когда field добавят для свойств

        А что это такое, можно поподробнее, или ссылку? Я что-то раньше никогда не слышал.


        1. Deosis
          22.10.2024 12:10

          Хотят добавить контекстное ключевое слово field для доступа к автосгенерированному полю внутри методов доступа свойства


        1. comradeleet
          22.10.2024 12:10

          Что-то вроде такого

          До:

          private bool _isNecessary
          public bool IsNecessary
          {
            get => _isNecessary;
            set { _isNecessary = value; OnPropertiesChanged(); }
          }

          После:

          public bool IsNecessary
          {
            get => field;
            set { field = value; OnPropertiesChanged(); }
          }


          1. DstRoses Автор
            22.10.2024 12:10

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


            1. comradeleet
              22.10.2024 12:10

              Да, было такое, уже несколько версий проскипала эта фича


    1. A1lfeG
      22.10.2024 12:10

      А чего не хватает? У нас как-то органично весь код потихоньку праймари конструкторы начинает использовать. Гораздо меньше писанины.


      1. nronnie
        22.10.2024 12:10

        А чего не хватает?

        Например:

        1. В него нельзя без костылей прикрутить какую-либо валидацию аргументов (хотя бы банальный null check).

        2. Те "поля" которые он создает невозможно сделать read-only.


        1. A1lfeG
          22.10.2024 12:10

          1. Ну нулл чеки, как мне кажется, решены null reference types
            делать дополнительную проверку можно, но мы в проекте этим не маемся

          2. Ну мне кажется праймари конструктор не для того делали, а для таких как мы - кто использует IOC/DI везде где надо и где ненадо...


          1. nronnie
            22.10.2024 12:10

            Ну нулл чеки, как мне кажется, решены null reference types

            Не совсем. Если код может быть вызван из кода "nullable context" которого вы не контролируете, то null check нужен. И если включить опцию компилятора EnableNETAnalyzers с достаточным AnalysisLevel, то при отсутствии null check будет появляться warning: "CA1062: Validate arguments of public methods".

            Ну мне кажется праймари конструктор не для того делали, а для таких как мы - кто использует IOC/DI везде где надо и где ненадо...

            IOC/DI ведь никак не спасет от выстрела в ногу путем изменения ссылки на "вставленный" сервис в каком-либо из методов. readonly (а позже еще и init-only) для того и придуман, чтобы однажды инициализированное (в конструкторе) гарантированно никогда потом не менялось.

            Еще не особо нравится нарушение общепринятых правил именования, когда в теле метода сходу непонятно, что перед тобой поле ("_fooBarBaz"), а не локальная переменная или параметр ("fooBarBaz").

            Задумка хорошая, мне она самому очень нравится, но раз попробовал, другой раз попробовал, и для себя решил, что пока что она как-то до ума еще не доведенная.


  1. Meloman19
    22.10.2024 12:10

    Ох ёёё... получается теперь в using EnterScope можно будет свободно await вызвать и компилятор слова не скажет. Прям замечательный выстрел в ногу будет для новичков (и не только).


    1. DstRoses Автор
      22.10.2024 12:10

      Структура Scope, которую мы получаем, не реализует асинхронную версию метода Dipose(), поэтому мы получим ошибку от компилятора. Но я нашел веселее момент, в рантайме. Можно в теории что-либо асинхронное указать между методами Enter() и Exit(). Компилятор никак на это не отреагирует, но при Runtime мы получим дичь в работе Lock))
      Замечу, что при использовании скоупа компилятор отлично реагирует на такие попытки (даже при использовании using без блочных скобок).


      1. nronnie
        22.10.2024 12:10

        Там всё дело всего лишь в том, что, by design, Exit() может только тот же поток, который перед этим делал Enter(). Я наступил когда-то на эти грабли, потому что какая-то старая, тогдашняя версия компилятора (за давностью даже и не помню что за версии компилятора и runtime это были) вполне допускала такую конструкцию:

        lock(_lock)
        {
           // ....
          await DoSomethingAsync();
          // ....
        }
        

        Ошибка возникала гнуснейшая, потому что её невозможно было стабильно воспроизвести (поток после await вполне может быть как другой, так и тот же самый).

        Потом это запретили на уровне компилятора ("CS1996: Cannot await in the body of a lock statement").

        Но, вот такое так до сих пор и допускается (компилятор не настолько умный чтобы распознать тут засаду):

        Monitor.Enter(_lock);
        try
        {
            await FooAsync();
        }
        finally
        {
            Monitor.Exit(_lock);
        }
        

        Я тогда выкрутился из этого написав свою собственную реализацию spin lock, а позже наткнулся на то, что в .NET есть уже свой, готовый, который если создавать его с enableThreadOwnerTracking = false соответствующих ограничений не имеет.

        Lock обладает теми же ограничениями (по await) что и Monitor - вот тут про это явно написано.

        Меня еще вот что интересует. Не так давно мне на собеседовании один чувак доказывал, что Monitor это не полностью "user-space synchronization object", а "гибридный". Я с ним тогда спорить не стал, но до сих пор в его правоте очень и очень сильно сомневаюсь. В документации по API как-то совсем явно про это не сказано, но написано следующее:

        The Monitor class is purely managed, fully portable, and might be more efficient in terms of operating-system resource requirements.

        Так что, сдается мне что прав был всё-таки я. Но сомнение всё-таки было посеяно и так и осталось.


        1. DstRoses Автор
          22.10.2024 12:10

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


  1. Weerel
    22.10.2024 12:10

    Раз уж заморочились с новым Lock, то почему было не сделать его асинхронным


    1. DstRoses Автор
      22.10.2024 12:10

      Да, хотелось бы иметь возможность применять асинхронщину внутри. Что тут скажешь... Ждем)


    1. mvv-rus
      22.10.2024 12:10

      Например, потому что Lock наверняка поддерживает семантику потока-владельца блокировки (Monitor - поддерживает, Critical Section и Mutex в Windows - тоже). Такая семантика нужна, чтобы позволить из одного и того же потока захватывать одну и ту же блокировку несколько раз (главное, чтобы блокировка освобождалась потом то же число раз). Нужно это, потому что так программировать сильно проще - внутри кода вызываемого метода не надо заморачиваться проверкой, не захвачена ли уже эта блокировка вызывающим методом и захватывать, а потом освобождать ее только когда она не была захвачена.
      А как вы сделаете такую же семантику для асинхронного выполнения? Поток в процессе асинхронного выполнения-то запросто поменяться может. Делать владельцем Execution Context (он в асинхронном потоке управления не меняется) ? Это громоздко, и поддержка со стороны ОС теряется. А так как как блокировка гораздо чаще накладывается на кусок, в котором асинхронное ожидание отсутствует, то для Lock общего назначнения так лучше не делать. Делать отдельный примитив синхронизации?

      PS Eсли же вам нужна асинхронная блокировка без семантики владения, то уже давно для этого есть SemaphoreSlim с пределом в 1. Но, таки да, тут try-finally придется ручками писать. Недостаточек.


      1. DstRoses Автор
        22.10.2024 12:10

        Спасибо за развернутый ответ по асинхронщине в Lock.
        Мы сейчас используем SemaphoreSlim для подобных случаев) Вот как только переехали в одном из проектов на асинхронность, то сразу же заменили Lock на SemaphoreSlim. Пока полет нормальный.


      1. AgentFire
        22.10.2024 12:10

        Есть AsyncLock из известного пакета.


  1. simplepersonru
    22.10.2024 12:10

    var list = new List<int>(initialList)
    {
        [^1] = 15
    }; 

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


    1. Nivl
      22.10.2024 12:10

      Смущает, что индекс с 1 начинается, а не с 0... Как в прямой индексации


      1. Naf2000
        22.10.2024 12:10

        Не смущает. [^k] аналог [n-k], где n - длина коллекции


        1. Nivl
          22.10.2024 12:10

          Если уж делать сахар, то похожим на нативный аналог, типа [0] - первый элемент с начала, [^0] - первый элемент с конца.

          В случае [^1] - может быть путаница при чтении кода.


          1. olivera507224
            22.10.2024 12:10

            Но n-0 == n, что на 1 превышает максимальный индекс в коллекции. Зачем так делать?


          1. orthoxerox
            22.10.2024 12:10

            [^0] - это как бы конец первого элемента с конца.


          1. a-tk
            22.10.2024 12:10

            Прикол в том, что семантика близка к плюсовым итераторам begin() и end(). Которая в мире C# мягко говоря непривычна.


      1. DstRoses Автор
        22.10.2024 12:10

        База. Думаю, что найдутся ошибки при такой инициализации объекта. Будто 2 разных человека делали :) (думаю, так и есть).


  1. mvv-rus
    22.10.2024 12:10

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

    А что до params-последовательностей (как я понял - предлагаются именно они, а не коллекции, ибо там IEnumerable а не ICollection), то это может оказаться даже вредным: У IEnumerable-то свойства Count нет, и вообще для него нет гарантий что число элементов последовательности считается быстро, как O(1).

    А также я боюсь за интерфейсы для ref struct при использовании их с generic: сейчас-то generic поддерживается напрямую в IL, исполняющей средой в рантайме. И я боюсь, а не придется ли теперь их поддерживать компилятору инстанциированием generic на этапе компиляции, как template в С++. А то мне до сих пор памятны простыни невнятных сообщений об ошибках инстанциирования от компилятора MS VC++ 2005, и видеть такие же в C# я не хочу. Ну и возможное при этом портирование стирания типов из Java мне тоже не нравится. Короче, к этому нововведению я подозрителен.


    1. Deosis
      22.10.2024 12:10

      а не придется ли теперь их поддерживать компилятору инстанциированием generic на этапе компиляции

      Так, для структур раньше так и было, на каждый тип своя версия кода. И одна версия на все классы.


    1. DstRoses Автор
      22.10.2024 12:10

      Хочется сказать: "В первый раз?")


    1. GLeBaTi
      22.10.2024 12:10

      В защиту params для IEnumerable хочу сказать, что это действительно удобно. Не нужно писать new List<MyClass>() { obj } в аргументе, если хочешь закинуть один объект. И не нужно делать .ToArray()


  1. Free_ze
    22.10.2024 12:10

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

    Вызывает не самые приятные флешбеки про SFINAE.


    1. DstRoses Автор
      22.10.2024 12:10

      Слышал про такое "явление" от разработчиков C++. Что-то на тяжелом)


      1. simplepersonru
        22.10.2024 12:10

        Концепция простая, Substitation Failure Is Not An Error. Компилятор, когда ловит прям ошибку подстановки типа, не завершает работу с ошибкой, а просто отбрасывает этот вариант, продолжает работу.

        Скрытый текст
        Вот например трейт на проверку, есть ли в типе метод "hello"
        Вот например трейт на проверку, есть ли в типе метод "hello"

        В данном случае ошибку может вызвать выражение decltype(&T::hello) . Если метода hello нет, то это ошибка подстановки, но SFINAE напрямую говорит нам что это не ошибка :) И этот вариант просто будет отброшен. Останется по умолчанию тот первый вариант, который будет типа false. Если же метод hello есть и ошибки подстановки не произойдет, то для такого типа будет true и метод можно вызвать

        Через такое игольное ушко протянуто почти всё мета-программирование с++. Т.е. у нас в императивном стиле нет мета-информации, мы не можем перебрать в цикле поля, методы и всё такое. (рефлексия в С++26 планируется). Мы можем лишь делать предположения и проверять наши предположения


        1. DstRoses Автор
          22.10.2024 12:10

          Спасибо за подробный ответ. Суть понятна)
          p.s. Рефлексия в C++ звучит интересно. Надо будет глянуть :)


          1. a-tk
            22.10.2024 12:10

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


  1. santjagocorkez
    22.10.2024 12:10

    А какие есть основания полагать, будто IsHeldByCurrentThread будет false, если взятие блокировки за пределами try? Вероятность освобождения внутри блока try? А это не bad design тогда?


    1. DstRoses Автор
      22.10.2024 12:10

      Да, тут больше про вероятность освобождения внутри тела try. Так сказать: "на всякий случай".