Является ли "Доступ к модифицированному закрытию" разрешенным синтаксисом понимания?

ReSharper 6.0 дает предупреждение "Доступ к модифицированному закрытию" для идентификатора dr в первом фрагменте кода.

private IEnumerable<string> GetTheDataTableStrings(DataTable dt) {
    foreach (DataRow dr in dt.Rows) {
        yield return GetStringFuncOutput(() => dr.ToString());
    }
}

Я думаю, что у меня есть общее представление о том, что это предупреждение пытается защитить меня: dr несколько раз меняются до вывода запроса GetTheDataTableStrings, и поэтому вызывающий может не получить ожидаемый результат/поведение.

Но R # не дает мне предупреждения для второго фрагмента кода.

private IEnumerable<string> GetTheDataTableStrings(DataTable dt) {
    return from DataRow dr in dt.Rows select GetStringFuncOutput(dr.ToString);
}

Безопасно ли мне отказаться от этого предупреждения/беспокойства при использовании синтаксиса понимания?

Другой код:

string GetStringFuncOutput(Func<string> stringFunc) {
    return stringFunc();
}

Ответ 1

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

Во-вторых, к вашему сведению, мы, скорее всего, исправим это в следующей версии С#; это главная проблема для разработчиков.

(ОБНОВЛЕНИЕ: Этот ответ был написан в 2011 году. На самом деле мы приняли исправление, описанное ниже в С# 5.)

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

Цикл for не будет изменен.

См. Http://ericlippert.com/2009/11/12/closing-over-the-loop-variable-considered-harmful-part-one/ для деталей.

В-третьих, нет проблемы с версией для понимания запросов, потому что нет закрытой переменной, которая изменяется. Форма понимания запроса такая же, как если бы вы сказали:

return dt.Rows.Select(dr=>GetStringFuncOutput(dr.ToString));

Лямбда не закрыта ни по какой внешней переменной, поэтому нет случайной переменной, которую можно изменить.

Ответ 2

Проблема, о которой предупреждает Resharper, была устранена как на С# 5.0, так и на VB.Net 11.0. Ниже приведены выдержки из языковых спецификаций. Обратите внимание, что спецификации можно найти по следующим путям по умолчанию на машине с установленной Visual Studio 2012.

  • C:\Program Files (x86)\Microsoft Visual Studio 11.0\VB\Specifications\1033\Visual Basic Language Specification.docx
  • C:\Program Files (x86)\Microsoft Visual Studio 11.0\VС#\Specifications\1033\CSharp Language Specification.docx

Спецификация языка С# Версия 5.0

8.8.4 Оператор foreach

Размещение v внутри цикла while важно для того, как оно захватывается какой-либо анонимной функцией, встречающейся во встроенной инструкции.

Например:

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

Если v было объявлено вне цикла while, оно будет разделяться между всеми итерациями, а его значение после цикла for будет конечным значением 13, что и будет вызывать вызов f. Вместо этого, поскольку каждая итерация имеет свою собственную переменную v, первая, захваченная f в первой итерации, будет продолжать удерживать значение 7, которое будет напечатано. (Примечание: более ранние версии С# объявили v за пределами цикла while.)

Спецификация языка Microsoft Visual Basic версии 11.0

10.9.3 Для каждого... Следующие утверждения (Аннотация)

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

Dim lambdas As New List(Of Action)
For Each x In {1,2,3}
   lambdas.Add(Sub() Console.WriteLine(x)
Next
lambdas(0).Invoke()
lambdas(1).Invoke()
lambdas(2).Invoke()

До Visual Basic 10.0 это вызвало предупреждение во время компиляции и три раза напечатало "3". Это было связано с тем, что все итерации цикла были разделены только одной переменной x, и все три лямбда взяли один и тот же "x", и к тому времени, когда были выполнены лямбды, он удерживал число 3. Начиная с Visual Basic 11.0, он печатает "1, 2, 3". Это потому, что каждая лямбда фиксирует другую переменную "x".