Поделиться через


Простые лямбда-параметры с модификаторами

Примечание.

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

Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия отражены в соответствующих заметках с заседания по дизайну языка (LDM) .

Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .

Проблема чемпиона: https://github.com/dotnet/csharplang/issues/8637

Сводка

Разрешить объявлять лямбда-параметры с модификаторами, не требуя их имен типов. Например, (ref entry) => вместо (ref FileSystemEntry entry) =>.

В качестве примера, возьмем этого делегата:

delegate bool TryParse<T>(string text, out T result);

Разрешить это упрощенное объявление параметров:

TryParse<int> parse1 = (text, out result) => Int32.TryParse(text, out result);

В настоящее время действительно лишь это:

TryParse<int> parse2 = (string text, out int result) => Int32.TryParse(text, out result);

Подробный дизайн

Грамматика

Никаких изменений. Последняя грамматика лямбда такова:

lambda_expression
  : modifier* identifier '=>' (block | expression)
  | attribute_list* modifier* type? lambda_parameter_list '=>' (block | expression)
  ;

lambda_parameter_list
  : lambda_parameters (',' parameter_array)?
  | parameter_array
  ;

lambda_parameter
  : identifier
  | attribute_list* modifier* type? identifier default_argument?
  ;

Эта грамматика уже считает modifiers* identifier синтаксически законным.

Примечания

  1. Это не относится к лямбде без списка параметров. ref x => x.ToString() не будет законным.
  2. Список лямбда-параметров по-прежнему не может смешивать implicit_anonymous_function_parameter и explicit_anonymous_function_parameter параметры.
  3. (ref readonly p) =>, (scoped ref p) =>, и (scoped ref readonly p) => будут разрешены, так же как и с явными параметрами, из-за:
  4. Наличие или отсутствие типа не влияет на то, является ли модификатор обязательным или необязательным. Другими словами, если модификатор был необходим при наличии типа, он по-прежнему требуется и при его отсутствии. Аналогичным образом, если модификатор был необязательным с типом, он также необязателен и без него.

Семантика

https://learn.microsoft.com/dotnet/csharp/language-reference/language-specification/expressions#12192-anonymous-function-signatures обновляется следующим образом:

В lambda_parameter_list все элементы lambda_parameter должны либо иметь type, либо не иметь type. Первый является "явным типизированным списком параметров", а последний является "неявным типизированным списком параметров".

Параметры в неявном типизированном списке параметров не могут иметь default_argument. Они могут иметь attribute_list.

Следующее изменение требуется для anonymous function conversions:

[...]

Если F имеет явно или неявно типизированный список параметров, каждый параметр в D имеет одинаковый тип и модификаторы, что и соответствующий параметр в F, игнорируя модификаторы парам и значения по умолчанию.

Заметки и уточнения

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

Если анонимная функция имеет explicit_anonymous_function_signature, набор совместимых типов делегатов и типов дерева выражений ограничен теми, которые имеют те же типы параметров и модификаторы в том же порядке.

Единственными модификаторами, ограничивающими совместимые типы делегатов, являются ref, out, in и ref readonly. Например, в явно типизированной лямбде следующее в данный момент неоднозначно.

delegate void D<T>(scoped T t) where T : allows ref struct;
delegate void E<T>(T t) where T : allows ref struct;

class C
{
    void M<T>() where T : allows ref struct
    {
        // error CS0121: The call is ambiguous between the following methods or properties: 'C.M1<T>(D<T>)' and 'C.M1<T>(E<T>)'
        // despite the presence of the `scoped` keyword.
        M1<T>((scoped T t) => { });
    }

    void M1<T>(D<T> d) where T : allows ref struct
    {
    }

    void M1<T>(E<T> d) where T : allows ref struct
    {
    }
}

Это остается верным при использовании лямбд с неявно заданными типами.

delegate void D<T>(scoped T t) where T : allows ref struct;
delegate void E<T>(T t) where T : allows ref struct;

class C
{
    void M<T>() where T : allows ref struct
    {
        // This will remain ambiguous.  'scoped' will not be used to restrict the set of delegates.
        M1<T>((scoped t) => { });
    }

    void M1<T>(D<T> d) where T : allows ref struct
    {
    }

    void M1<T>(E<T> d) where T : allows ref struct
    {
    }
}

Открытые вопросы

  1. Следует ли scopedвсегда быть модификатором в лямбда-коде в C# 14? Это важно для такого дела:

    M((scoped s = default) => { });
    

    В этом случае это не подпадает под спецификацию "простого параметра лямбда", так как "простая лямбда" не может содержать инициализатор (= default). Таким образом, scoped здесь рассматривается как type (как в C# 13). Мы хотим сохранить это? Или просто было бы проще иметь более общее правило, что scoped всегда является модификатором и поэтому будет модификатором даже в случае недопустимого простого параметра?

    Перекомендация: сделайте это модификатором. Мы уже отговариваем людей от использования типов, которые полностью строчными буквами, И мы также сделали незаконным создание типа под названием scoped в C#. Таким образом, это может быть только какой-то случай ссылки на тип из другой библиотеки. Решение проблемы простое, если вы каким-то образом столкнулись с этой проблемой. Просто используйте @scoped, чтобы сделать это имя типа вместо модификатора.

  2. Допустить params в простом параметре lambda? Работы с лямбда-функцией уже добавили поддержку params T[] values в лямбда. Этот модификатор является необязательным, и лямбда и исходный делегат могут иметь несоответствие по этому модификатору (хотя мы предупреждаем, если делегат не имеет модификатора, а лямбда имеет). Если мы продолжаем разрешать это с помощью простого лямбда-параметра. например, M((params values) => { ... })

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

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

  3. Влияет ли 'scoped' на разрешение перегрузки? Например, если существует несколько перегрузок делегата, и одна из них имеет параметр с областью видимости, а другая — нет, повлияет ли наличие параметра с областью видимости на разрешение перегрузки.

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

    delegate void D<T>(scoped T t) where T : allows ref struct;
    delegate void E<T>(T t) where T : allows ref struct;
    
    class C
    {
        void M<T>() where T : allows ref struct
        {
            M1<T>((scoped T t) => { });
        }
    
        void M1<T>(D<T> d) where T : allows ref struct
        {
        }
    
        void M1<T>(E<T> d) where T : allows ref struct
        {
        }
    }
    

    Это неоднозначно сегодня. Несмотря на то, что для D<T> установлен "скопирован" и есть "скопирован" в лямбда-параметре, мы это не разрешаем. Мы не считаем, что это должно измениться с неявно типизированными лямбда-выражениями.

    Вывод 15.01.2025: Приведенный выше вариант будет также справедлив для простых лямбас. 'Scoped' не будет влиять на разрешение перегрузки, в то время как ref и out будут продолжать это делать.

  4. Разрешить лямбды '(scoped x) => ...'?

    Рекомендация: Да. Если мы не разрешим это, мы можем оказаться в ситуациях, где пользователь может написать полную явно типизированную лямбда-версию, но не версию с неявной типизацией. Например:

    delegate ReadOnlySpan<int> D(scoped ReadOnlySpan<int> x);
    
    class C
    {
        static void Main(string[] args)
        {
            D d = (scoped ReadOnlySpan<int> x) => throw null!;
            D d = (ReadOnlySpan<int> x) => throw null!; // error! 'scoped' is required
        }
    }
    

    Удаление "scoped" приведет к ошибке (языку требуется соответствие в этом случае между лямбда-функцией и делегатом). Так как мы хотим, чтобы пользователь мог писать лямбда-коды, не указывая тип явным образом, это означает, что (scoped x) => ... необходимо разрешить.

    Вывод 15.01.2025: Мы позволим (scoped x) => ... лямбды.