С С#, почему параметры «in» нельзя использовать в локальных функциях?

Например,

public int DoSomething(in SomeType something){
  int local(){
     return something.anInt;
  }
  return local();
}

Почему компилятор выдает ошибку, что переменную something нельзя использовать в локальной функции?

«потому что язык разработан таким образом» / «язык не реализовал эту функцию» будет основным ответом. Чего вы на самом деле пытаетесь достичь, что заблокировано этим ограничением?

Pac0 14.08.2022 16:37

@ Pac0 Что-то очень сложно объяснить, что выходит за рамки обычного паркета разработчиков C#. На самом деле я реализую компилятор C#, но он использует предварительно проверенный синтаксис C# с Roslyn в качестве входных данных, и эта конкретная особенность не соответствует моим потребностям. Но это заставляет меня задаться вопросом, чего мне не хватает, как будто здесь есть какой-то пограничный случай, тогда мне, вероятно, нужно будет понять это и для того, что я делаю.

Frank 14.08.2022 16:42

@Pac0 Если бы мне пришлось угадывать, я бы сказал, что это как-то связано с оптимизацией ... что локальной функции не разрешено «видеть» что-то, что осталось в исходной/декларирующей области ...

Frank 14.08.2022 16:44

Достаточно справедливо для вашей потребности понять обоснование. Я не вижу сейчас конкретного теоретического момента, который сделал бы это невозможным (хотя, может быть, и есть). Может быть, на этот вопрос будет получен поучительный ответ! Но имейте в виду, что в языке функции по умолчанию не реализованы. Это некоторая работа по анализу, расстановке приоритетов, дизайну и т. д., чтобы добавить причудливые привилегии к языку. Так что вполне может быть, что C# не позволяет этого просто потому, что... это еще не реализовано в языке (пока?). См. этот ответ/отступление Эрика Липперта здесь stackoverflow.com/a/8673015/479251

Pac0 14.08.2022 16:46

наводит меня на мысль, что было бы неплохо задать вопрос на их github (от roslyn или c#language?). Это может стать запросом функции.

Pac0 14.08.2022 16:52

@Pac0 Я, наверное, спрошу в их Discord. Я мог бы получить ответ типа «потому что Эрик был пьян в ту ночь», который на самом деле был бы чрезвычайно удовлетворительным, если бы был правдой.

Frank 14.08.2022 17:57

«Локальные» функции С# — это просто [вложенные функции], верно? Есть ли какое-то полезное различие между этим тегом и [local-functions] или идея иметь отдельный тег для одной и той же концепции в C# по сравнению с другими языками, например, [c#-local-functions] по сравнению с другими языками? [Вложенные-функции GNU-C] или [вложенные-функции pascal]? В любом случае, я подозреваю, что эти теги следует сделать синонимами, если только я не упустил различия. (@Чарлифейс)

Peter Cordes 15.08.2022 04:42

@PeterCordes Я предлагаю вам поместить это на [meta.so], лично я считаю, что они должны оставаться отдельными, поскольку, как вы можете видеть, локальные функции C# имеют свои особенности.

Charlieface 15.08.2022 05:11

@Charlieface: У каждого языка есть свои особенности. Это тег, который уже должен использоваться с языком, чтобы иметь смысл, например [C#][nested-functions] против [pascal][nested-functions]. Но да, это, вероятно, должно быть обсуждено на meta.SO, чтобы люди могли взвесить более подробно.

Peter Cordes 15.08.2022 05:22
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
10
491
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В документации по локальным функциям указано следующее

Переменный захват

Note that when a local function captures variables in the enclosing scope, the local function is implemented as a delegate type.

И глядя на лямбды:

Захват внешних переменных и область видимости переменных в лямбда-выражениях

A lambda expression can't directly capture an in, ref, or out parameter from the enclosing method.

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

Пример

public Func<int> DoSomething(in SomeType something){
  int local(){
     return something.anInt;
  }
  return local;
}

Предположим, что эта функция вызывается так:

public Func<int> Mystery()
{
    SomeType ghost = new SomeType();
    return DoSomething(ghost);
}

public void Scary()
{
    var later = Mystery();
    Thread.Sleep(5000);
    later(); // oops
}

Функция Mystery создает ghost и передает его как параметр in в DoSomething, что означает, что он передается как ссылка только для чтения на переменную ghost.

Функция DoSomething захватывает эту ссылку в локальную функцию local, а затем возвращает эту функцию как делегат Func<int>.

Когда функция Mystery возвращается, переменной ghost больше не существует. Затем функция Scary использует делегат для вызова функции local, а local попытается прочитать свойство anInt из несуществующей переменной. Упс.

Правило «Вы не можете захватывать эталонные параметры (in, out, ref) в делегатах» предотвращает эту проблему.

Вы можете обойти эту проблему, сделав копию параметра in и захватив копию:

public Func<int> DoSomething2(in SomeType something){
  var copy = something;
  int local(){
     return copy.anInt;
  }
  return local;
}

Обратите внимание, что возвращенный делегат работает с copy, а не с исходным ghost. Это означает, что у делегата всегда будет действительный copy, из которого можно получить anInt. Однако это означает, что любые будущие изменения в ghost не повлияют на copy.

public int Mystery()
{
    SomeType ghost = new SomeType() { anInt = 42 };
    var later = DoSomething2(ghost);
    ghost = new SomeType() { anInt = -1 };
    return later(); // returns 42, not -1
}

Это кажется хорошим началом для объяснения проблемы, но «ref избежать проблем» на мой вкус немного волнообразно. Какая именно здесь связь между делегатами и лямбда-выражениями? Является ли само ограничение на лямбда-выражения произвольным или что технически лежит в основе этого?

Karl Knechtel 14.08.2022 17:41

Хм, так что, если я правильно понимаю, предотвращение «в» должно охватывать сценарий, когда метод возвращает саму локальную функцию, и только этот сценарий?

Frank 14.08.2022 18:16

Да, это на самом деле не проясняет для меня вещи, но я чувствую, что могу смотреть на это неправильно. Как я вижу в приведенном выше примере, компилятор должен выдать ошибку «return local»; так как это проблема. Конечно, так оно и есть, но мне это кажется ленивой реализацией компилятора, а не реальным обоснованием.

Frank 14.08.2022 18:25

@KarlKnechtel Лямбда, которая захватывает переменные, преобразуется в класс, где поля содержат захваченные переменные. ref экранирование хорошо документировано: у вас не может быть поля ref в классе, оно разрешено только как локальная переменная. Первая цитата, которую я дал, показывает, что локальные функции становятся лямбда-выражениями, если есть захваченные переменные.

Charlieface 14.08.2022 18:27

Отсрочка исполнения до return local; означает, что компилятор должен выполнить анализ выхода: «Возможно ли, чтобы local вызывался после возврата этой функции?» Анализ побега сложен. Например, будет ли list.Mystery(e => e.value == local()) безопасным? Вы не знаете, потому что не знаете, что делает Mystery. Вам придется создавать все более и более сложные правила, чтобы разрешить это, если Mystery это Select или Where, но не другие методы, и когда вы закончите, неясно, улучшили ли вы ситуацию. Простые правила легко объяснить и понять.

Raymond Chen 14.08.2022 18:30

@RaymondChen Конечно. Мое использование слова «ленивый», возможно, было немного резким, но, по сути, вы говорите то, что я думаю :-) Тем не менее, исходный код в исходном вопросе должен компилирует IMO. Все, что нужно для этого, даже если компилятор специально проверяет этот случай, должно быть реализовано IMO

Frank 14.08.2022 18:32

@KarlKnechtel Lambdas конвертируется в классы - не знал этого. Спасибо!

Frank 14.08.2022 18:33

Обратите также внимание на то, что «ленивая реализация компилятора» думает о проблеме не на том уровне. Это не вопрос реализации. Это языковой вопрос. Чего вы точно не хотите, так это «Некоторые компиляторы принимают этот код, но другие его отвергают». Или «Этот код компилируется только в том случае, если вы установите уровень оптимизации 2 или выше (когда срабатывает escape-анализ), но другие оптимизации уровня 2 делают нашу проблему практически невозможной для отладки». Правила того, что составляет синтаксически легальную программу, должны быть независимыми от реализации.

Raymond Chen 14.08.2022 18:36

@RaymondChen Так ли это? Каков эквивалент спецификации C# lang для этого комментария «Обратите внимание, что когда локальная функция захватывает переменные в охватывающей области, локальная функция реализуется как тип делегата»? Обратите внимание на использование слова «реализовано».

Frank 14.08.2022 18:41

Да, лямбда-выражения должны преобразовываться в классы, если подумать, другого способа захватить переменную нет. Я не могу найти локальные функции в спецификации, я подозреваю, что она еще не обновлена. @RaymondChen Похоже, что в предложении спецификации его тоже нет? docs.microsoft.com/en-us/dotnet/csharp/language-reference/…

Charlieface 14.08.2022 18:48

ок спасибо всем. Я чувствую, что это ответило и прояснило многое.

Frank 14.08.2022 18:49

@Charlieface Частично в шутку, но когда они пишут спецификацию, пожалуйста, не могли бы они написать в форме «локальные функции фиксируются как классы, и ни один компилятор не может обрабатывать in/ref и т. д. и т. д., ЧТО ИЗ случая, о котором Фрэнк упомянул в проблеме SO № 12234 " Очень признателен.

Frank 14.08.2022 18:53

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

Charlieface 14.08.2022 18:55

@Charlieface Имеет смысл. Большое спасибо за ваши разъяснения. Очень познавательно.

Frank 14.08.2022 18:57

Однако Реализация в качестве делегата (непосредственно перед «захватом переменной», на который вы ссылаетесь) указывает «Локальные функции более гибкие, поскольку их можно писать как традиционный метод или как делегат. Локальные функции Только преобразуются в делегаты при использовании в качестве делегата».. Поэтому я не думаю, что правила захвата переменных делегата действительно объясняют рассматриваемое ограничение. Кроме того, даже для делегатов можно понять поведение ref и out...

Ivan Stoev 15.08.2022 10:45

... но не in. in не предназначен для предоставления ref семантики, как два других модификатора, поэтому его захват как «обычная» (не in) переменная не должен быть проблемой (технически), за исключением случаев, когда мы чего-то не видим. Что было бы хорошо объяснить в docs/specs.

Ivan Stoev 15.08.2022 10:48

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

Charlieface 15.08.2022 10:50

Другие вопросы по теме