Я изучал некоторые странные проблемы жизни объекта и столкнулся с этим очень загадочным поведением компилятора С#:
Рассмотрим следующий тестовый класс:
class Test
{
delegate Stream CreateStream();
CreateStream TestMethod( IEnumerable<string> data )
{
string file = "dummy.txt";
var hashSet = new HashSet<string>();
var count = data.Count( s => hashSet.Add( s ) );
CreateStream createStream = () => File.OpenRead( file );
return createStream;
}
}
Компилятор генерирует следующее:
internal class Test
{
public Test()
{
base..ctor();
}
private Test.CreateStream TestMethod(IEnumerable<string> data)
{
Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0();
cDisplayClass10.file = "dummy.txt";
cDisplayClass10.hashSet = new HashSet<string>();
Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0)));
return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1));
}
private delegate Stream CreateStream();
[CompilerGenerated]
private sealed class <>c__DisplayClass1_0
{
public HashSet<string> hashSet;
public string file;
public <>c__DisplayClass1_0()
{
base..ctor();
}
internal bool <TestMethod>b__0(string s)
{
return this.hashSet.Add(s);
}
internal Stream <TestMethod>b__1()
{
return (Stream) File.OpenRead(this.file);
}
}
}
Оригинальный класс содержит два lambdas: s => hashSet.Add( s )
и () => File.OpenRead( file )
. Первая закрывается по локальной переменной hashSet
, вторая замыкает локальную переменную file
. Однако компилятор генерирует один класс реализации закрытия <>c__DisplayClass1_0
, который содержит как hashSet
, так и file
. Как результат, возвращаемый делегат CreateStream
содержит и сохраняет ссылку на объект hashSet
, который должен был быть доступен для GC после возврата TestMethod
.
В реальном сценарии, где я столкнулся с этой проблемой, очень существенный (т.е. > 100 МБ) объект неправильно заключен.
Мои конкретные вопросы:
- Это ошибка? Если нет, то почему это поведение считается желательным?
Update:
Спецификация С# 5 7.15.5.1 гласит:
Когда внешняя переменная ссылается на анонимную функцию, считается, что внешняя переменная была захвачена анонимным функция. Обычно время жизни локальной переменной ограничено выполнение блока или оператора, с которым оно связано (§5.1.7). Однако время жизни захваченной внешней переменной расширено, по крайней мере, до тех пор, пока дерево делегатов или выражений, созданное из анонимная функция становится пригодной для сбора мусора.
Это, казалось бы, открыто для некоторой степени интерпретации и не запрещает явным образом лямбду от захвата переменных, которые она не ссылается. Тем не менее, этот вопрос охватывает связанный сценарий, который @eric-lippert считается ошибкой. IMHO, я вижу, что комбинированная реализация закрытия, предоставленная компилятором, является хорошей оптимизацией, но что оптимизация не должна использоваться для lambdas, которую может разумно обнаружить компилятор, может иметь время жизни за пределами текущего кадра стека.
- Как я могу противопоставить это, не отказываясь от использования лямбда? Примечательно, как я могу защищать себя от этого, так что будущие изменения кода не заставят некоторые другие неизменные лямбда в одном и том же методе запускать что-то, что ему не нужно?
Update:
Пример кода, который я предоставил, по необходимости надуман. Ясно, что реорганизация лямбда-разработки по отдельному методу работает вокруг проблемы. Мой вопрос не предназначен для проектирования лучших практик (также охватываемых @peter-duniho). Скорее, учитывая содержимое TestMethod
в его нынешнем виде, я хотел бы знать, есть ли способ принудить компилятор исключить лямбда CreateStream
из объединенной реализации закрытия.
Для записи я нацелен на .NET 4.6 с VS 2015.