Закрытие по переменной цикла в Scala
Как обсуждалось в блоге Эрика Липперта, закрытие переменной цикла, считающейся вредной, закрытие переменной цикла в C# может иметь неожиданные последствия. Я пытался понять, применима ли та же "гоча" к Скале.
Прежде всего, поскольку это вопрос Scala, я попытаюсь объяснить пример Эрика Липперта на C#, добавив несколько комментариев к его коду.
// Create a list of integers
var values = new List<int>() { 100, 110, 120 };
// Create a mutable, empty list of functions that take no input and return an int
var funcs = new List<Func<int>>();
// For each integer in the list of integers we're trying
// to add a function to the list of functions
// that takes no input and returns that integer
// (actually that's not what we're doing and there's the gotcha).
foreach(var v in values)
funcs.Add( ()=>v );
// Apply the functions in the list and print the returned integers.
foreach(var f in funcs)
Console.WriteLine(f());
Большинство людей ожидают, что эта программа напечатает 100, 110, 120. На самом деле она печатает 120, 120, 120. Проблема в том, что () => v
функция, которую мы добавляем к funcs
Список закрывается по переменной v, а не по значению v. Когда v меняет значение, в первом цикле все три замыкания, которые мы добавляем к funcs
list "видит" ту же переменную v, которая (к тому времени, как мы применяем их во втором цикле) имеет значение 120 для всех из них.
Я пытался перевести пример кода в Scala:
import collection.mutable.Buffer
val values = List(100, 110, 120)
val funcs = Buffer[() => Int]()
for(v <- values) funcs += (() => v)
funcs foreach ( f => println(f()) )
// prints 100 110 120
// so Scala can close on the loop variable with no issue, or can it?
Действительно ли Scala не страдает от той же проблемы, или я только что плохо перевел код Эрика Липперта и не смог его воспроизвести?
Такое поведение сбило с толку многих доблестных разработчиков на C#, поэтому я хотел убедиться, что в Scala нет странных подобных ошибок. Но также, как только вы поймете, почему C# ведет себя так, как он это делает, вывод примера кода Эрика Липперта имеет смысл (в основном, это то, как работают замыкания): так что же делает Scala по-другому?
4 ответа
Scala не имеет той же проблемы, потому что v
это не вар, это вал. Поэтому, когда вы пишете
() => v
Компилятор понимает, что он должен создать функцию, которая возвращает это статическое значение.
Если вместо этого вы используете var
Вы можете иметь ту же проблему. Но гораздо понятнее, что это запрашиваемое поведение, поскольку вы явно создаете переменную, а затем возвращаете ее функции:
val values = Array(100, 110, 120)
val funcs = collection.mutable.Buffer[() => Int]()
var value = 0
var i = 0
while (i < values.length) {
value = values(i)
funcs += (() => value)
i += 1
}
funcs foreach (f => println(f()))
(Обратите внимание, что если вы попробуете funcs += (() => values(i))
вы получите исключение вне границ, потому что вы закрыли переменную i
который, когда вы звоните, сейчас 3
!)
Близкий эквивалент примера C# будет с while
петля и var
, Это будет вести себя как в C#.
С другой стороны, for(v <- values) funcs += (() => v)
переводится на values.foreach(v => funcs += () => v)
просто дать имена, это может быть
def iteration(v: Int) = {funcs += () => v)
values.foreach(iteration)
Закрытие () => v
появляется в теле итерации, и она захватывает не какой-то var, общий для всех итераций, а аргумент вызова итерации, который не является общим и, кроме того, является постоянным значением, а не переменной. Это предотвращает неинтуитивное поведение.
Там может быть переменная в реализации foreach
, но это не то, что видит закрытие.
Если в C# вы переместите тело цикла в отдельный метод, вы получите тот же эффект.
Обратите внимание, что понимание Scala работает совсем по-другому. Это:
for(v <- values) funcs += (() => v)
переводится во время компиляции в это:
values.foreach(v => funcs += (() => v))
Так v
новая переменная для каждого значения.
Если вы разберете пример C#, вы увидите, что компилятор генерирует класс для хранения закрытых переменных. Reflector отображает этот класс как:
[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
// Fields
public int v;
// Methods
public int <Main>b__1()
{
return this.v;
}
}
Reflector отображает такой красивый C#, вы не можете реально увидеть, как этот класс используется. Чтобы увидеть, что вам нужно посмотреть на сырой IL.
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 4
.locals init (
[0] class [mscorlib]System.Collections.Generic.List`1<int32> values,
[1] class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>> funcs,
[2] class ConsoleApplication1.Program/<>c__DisplayClass2 CS$<>8__locals3,
[3] class [mscorlib]System.Func`1<int32> f,
[4] class [mscorlib]System.Collections.Generic.List`1<int32> <>g__initLocal0,
[5] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32> CS$5$0000,
[6] bool CS$4$0001,
[7] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>> CS$5$0002)
L_0000: nop
L_0001: newobj instance void [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
L_0006: stloc.s <>g__initLocal0
L_0008: ldloc.s <>g__initLocal0
L_000a: ldc.i4.s 100
L_000c: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_0011: nop
L_0012: ldloc.s <>g__initLocal0
L_0014: ldc.i4.s 110
L_0016: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_001b: nop
L_001c: ldloc.s <>g__initLocal0
L_001e: ldc.i4.s 120
L_0020: callvirt instance void [mscorlib]System.Collections.Generic.List`1<int32>::Add(!0)
L_0025: nop
L_0026: ldloc.s <>g__initLocal0
L_0028: stloc.0
L_0029: newobj instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::.ctor()
L_002e: stloc.1
L_002f: nop
L_0030: ldloc.0
L_0031: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<!0> [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()
L_0036: stloc.s CS$5$0000
L_0038: newobj instance void ConsoleApplication1.Program/<>c__DisplayClass2::.ctor()
L_003d: stloc.2
L_003e: br.s L_0060
L_0040: ldloc.2
L_0041: ldloca.s CS$5$0000
L_0043: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>::get_Current()
L_0048: stfld int32 ConsoleApplication1.Program/<>c__DisplayClass2::v
L_004d: ldloc.1
L_004e: ldloc.2
L_004f: ldftn instance int32 ConsoleApplication1.Program/<>c__DisplayClass2::<Main>b__1()
L_0055: newobj instance void [mscorlib]System.Func`1<int32>::.ctor(object, native int)
L_005a: callvirt instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::Add(!0)
L_005f: nop
L_0060: ldloca.s CS$5$0000
L_0062: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>::MoveNext()
L_0067: stloc.s CS$4$0001
L_0069: ldloc.s CS$4$0001
L_006b: brtrue.s L_0040
L_006d: leave.s L_007e
L_006f: ldloca.s CS$5$0000
L_0071: constrained. [mscorlib]System.Collections.Generic.List`1/Enumerator`0<int32>
L_0077: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_007c: nop
L_007d: endfinally
L_007e: nop
L_007f: nop
L_0080: ldloc.1
L_0081: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<!0> [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Func`1<int32>>::GetEnumerator()
L_0086: stloc.s CS$5$0002
L_0088: br.s L_009e
L_008a: ldloca.s CS$5$0002
L_008c: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>::get_Current()
L_0091: stloc.3
L_0092: ldloc.3
L_0093: callvirt instance !0 [mscorlib]System.Func`1<int32>::Invoke()
L_0098: call void [mscorlib]System.Console::WriteLine(int32)
L_009d: nop
L_009e: ldloca.s CS$5$0002
L_00a0: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>::MoveNext()
L_00a5: stloc.s CS$4$0001
L_00a7: ldloc.s CS$4$0001
L_00a9: brtrue.s L_008a
L_00ab: leave.s L_00bc
L_00ad: ldloca.s CS$5$0002
L_00af: constrained. [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Func`1<int32>>
L_00b5: callvirt instance void [mscorlib]System.IDisposable::Dispose()
L_00ba: nop
L_00bb: endfinally
L_00bc: nop
L_00bd: ret
.try L_0038 to L_006f finally handler L_006f to L_007e
.try L_0088 to L_00ad finally handler L_00ad to L_00bc
}
В первом foreach вы можете видеть, что создан только один экземпляр этого класса. Значения итератора присваиваются общедоступному экземпляру v
поле. funcs
список заполнен делегатами этого объекта b__1
метод.
По сути, то, что происходит в C#,
- Создать объект области закрытия
- Перебирая значения...
- Вставьте ссылку на функцию доступа к крышке в
funcs
- Обновите объект области закрытия
v
с текущим значением.
- Вставьте ссылку на функцию доступа к крышке в
- Итератор закончен
funcs
, Каждый вызов вернет текущее значениеv
,