Например,
public int DoSomething(in SomeType something){
int local(){
return something.anInt;
}
return local();
}
Почему компилятор выдает ошибку, что переменную something нельзя использовать в локальной функции?
@ Pac0 Что-то очень сложно объяснить, что выходит за рамки обычного паркета разработчиков C#. На самом деле я реализую компилятор C#, но он использует предварительно проверенный синтаксис C# с Roslyn в качестве входных данных, и эта конкретная особенность не соответствует моим потребностям. Но это заставляет меня задаться вопросом, чего мне не хватает, как будто здесь есть какой-то пограничный случай, тогда мне, вероятно, нужно будет понять это и для того, что я делаю.
@Pac0 Если бы мне пришлось угадывать, я бы сказал, что это как-то связано с оптимизацией ... что локальной функции не разрешено «видеть» что-то, что осталось в исходной/декларирующей области ...
Достаточно справедливо для вашей потребности понять обоснование. Я не вижу сейчас конкретного теоретического момента, который сделал бы это невозможным (хотя, может быть, и есть). Может быть, на этот вопрос будет получен поучительный ответ! Но имейте в виду, что в языке функции по умолчанию не реализованы. Это некоторая работа по анализу, расстановке приоритетов, дизайну и т. д., чтобы добавить причудливые привилегии к языку. Так что вполне может быть, что C# не позволяет этого просто потому, что... это еще не реализовано в языке (пока?). См. этот ответ/отступление Эрика Липперта здесь stackoverflow.com/a/8673015/479251
наводит меня на мысль, что было бы неплохо задать вопрос на их github (от roslyn или c#language?). Это может стать запросом функции.
@Pac0 Я, наверное, спрошу в их Discord. Я мог бы получить ответ типа «потому что Эрик был пьян в ту ночь», который на самом деле был бы чрезвычайно удовлетворительным, если бы был правдой.
«Локальные» функции С# — это просто [вложенные функции], верно? Есть ли какое-то полезное различие между этим тегом и [local-functions] или идея иметь отдельный тег для одной и той же концепции в C# по сравнению с другими языками, например, [c#-local-functions] по сравнению с другими языками? [Вложенные-функции GNU-C] или [вложенные-функции pascal]? В любом случае, я подозреваю, что эти теги следует сделать синонимами, если только я не упустил различия. (@Чарлифейс)
@PeterCordes Я предлагаю вам поместить это на [meta.so], лично я считаю, что они должны оставаться отдельными, поскольку, как вы можете видеть, локальные функции C# имеют свои особенности.
@Charlieface: У каждого языка есть свои особенности. Это тег, который уже должен использоваться с языком, чтобы иметь смысл, например [C#][nested-functions] против [pascal][nested-functions]. Но да, это, вероятно, должно быть обсуждено на meta.SO, чтобы люди могли взвесить более подробно.
@Charlieface: Опубликовано Должен ли [local-functions] быть синонимом [nested-function] или локальные функции C# требуют отдельного тега? в мете.
В документации по локальным функциям указано следующее
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
, orout
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
избежать проблем» на мой вкус немного волнообразно. Какая именно здесь связь между делегатами и лямбда-выражениями? Является ли само ограничение на лямбда-выражения произвольным или что технически лежит в основе этого?
Хм, так что, если я правильно понимаю, предотвращение «в» должно охватывать сценарий, когда метод возвращает саму локальную функцию, и только этот сценарий?
Да, это на самом деле не проясняет для меня вещи, но я чувствую, что могу смотреть на это неправильно. Как я вижу в приведенном выше примере, компилятор должен выдать ошибку «return local»; так как это проблема. Конечно, так оно и есть, но мне это кажется ленивой реализацией компилятора, а не реальным обоснованием.
@KarlKnechtel Лямбда, которая захватывает переменные, преобразуется в класс, где поля содержат захваченные переменные. ref
экранирование хорошо документировано: у вас не может быть поля ref
в классе, оно разрешено только как локальная переменная. Первая цитата, которую я дал, показывает, что локальные функции становятся лямбда-выражениями, если есть захваченные переменные.
Отсрочка исполнения до return local;
означает, что компилятор должен выполнить анализ выхода: «Возможно ли, чтобы local
вызывался после возврата этой функции?» Анализ побега сложен. Например, будет ли list.Mystery(e => e.value == local())
безопасным? Вы не знаете, потому что не знаете, что делает Mystery
. Вам придется создавать все более и более сложные правила, чтобы разрешить это, если Mystery
это Select
или Where
, но не другие методы, и когда вы закончите, неясно, улучшили ли вы ситуацию. Простые правила легко объяснить и понять.
@RaymondChen Конечно. Мое использование слова «ленивый», возможно, было немного резким, но, по сути, вы говорите то, что я думаю :-) Тем не менее, исходный код в исходном вопросе должен компилирует IMO. Все, что нужно для этого, даже если компилятор специально проверяет этот случай, должно быть реализовано IMO
@KarlKnechtel Lambdas конвертируется в классы - не знал этого. Спасибо!
Обратите также внимание на то, что «ленивая реализация компилятора» думает о проблеме не на том уровне. Это не вопрос реализации. Это языковой вопрос. Чего вы точно не хотите, так это «Некоторые компиляторы принимают этот код, но другие его отвергают». Или «Этот код компилируется только в том случае, если вы установите уровень оптимизации 2 или выше (когда срабатывает escape-анализ), но другие оптимизации уровня 2 делают нашу проблему практически невозможной для отладки». Правила того, что составляет синтаксически легальную программу, должны быть независимыми от реализации.
@RaymondChen Так ли это? Каков эквивалент спецификации C# lang для этого комментария «Обратите внимание, что когда локальная функция захватывает переменные в охватывающей области, локальная функция реализуется как тип делегата»? Обратите внимание на использование слова «реализовано».
Да, лямбда-выражения должны преобразовываться в классы, если подумать, другого способа захватить переменную нет. Я не могу найти локальные функции в спецификации, я подозреваю, что она еще не обновлена. @RaymondChen Похоже, что в предложении спецификации его тоже нет? docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
ок спасибо всем. Я чувствую, что это ответило и прояснило многое.
@Charlieface Частично в шутку, но когда они пишут спецификацию, пожалуйста, не могли бы они написать в форме «локальные функции фиксируются как классы, и ни один компилятор не может обрабатывать in/ref и т. д. и т. д., ЧТО ИЗ случая, о котором Фрэнк упомянул в проблеме SO № 12234 " Очень признателен.
Я думаю, что единственное, что они могли бы сделать, это потенциально разрешить захват параметров ref
и in
, когда делегат не генерируется, но это довольно сложно определить, и я думаю, маловероятно, что они это сделают.
@Charlieface Имеет смысл. Большое спасибо за ваши разъяснения. Очень познавательно.
Однако Реализация в качестве делегата (непосредственно перед «захватом переменной», на который вы ссылаетесь) указывает «Локальные функции более гибкие, поскольку их можно писать как традиционный метод или как делегат. Локальные функции Только преобразуются в делегаты при использовании в качестве делегата».. Поэтому я не думаю, что правила захвата переменных делегата действительно объясняют рассматриваемое ограничение. Кроме того, даже для делегатов можно понять поведение ref
и out
...
... но не in
. in
не предназначен для предоставления ref
семантики, как два других модификатора, поэтому его захват как «обычная» (не in
) переменная не должен быть проблемой (технически), за исключением случаев, когда мы чего-то не видим. Что было бы хорошо объяснить в docs/specs.
Это неправда: предполагается, что in
предоставляет ссылочную семантику вызываемой функции, чтобы она могла прочитать исходное местоположение, если в нем были изменения в другом потоке. Это невозможно сделать, если его поднять в поле
«потому что язык разработан таким образом» / «язык не реализовал эту функцию» будет основным ответом. Чего вы на самом деле пытаетесь достичь, что заблокировано этим ограничением?