Закрытие по переменной цикла в Scala - PullRequest
11 голосов
/ 23 марта 2012

Как обсуждалось в блоге Эрика Липперта Закрытие переменной цикла, считающейся вредной , закрытие переменной цикла в C # может иметь неожиданные последствия. Я пытался понять, применима ли та же самая «гоча» к Scala .

Прежде всего, поскольку это вопрос 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, «видят» одну и ту же переменную 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 ]

9 голосов
/ 23 марта 2012

У Scala такой же проблемы нет, потому что v - это не переменная, а val.Поэтому, когда вы пишете

() => 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!)

5 голосов
/ 23 марта 2012

Близким эквивалентом примера C # будет цикл while и var.Это будет вести себя как в C #.

С другой стороны, for(v <- values) funcs += (() => v) переводится в values.foreach(v => funcs += () => v)

только для того, чтобы дать имена, которые могут быть

def iteration(v: Int) = {funcs += () => v)
values.foreach(iteration)

Закрытие () => v появляется втело итерации и то, что она захватывает, это не некоторая переменная, общая для всех итераций, а аргумент вызова итерации, который не является общим и, кроме того, является постоянным значением, а не переменной.Это предотвращает неинтуитивное поведение.

В реализации foreach может быть переменная, но это не то, что видит замыкание.

Если в C # вы перемещаете тело цикла отдельным методом, вы получаете тот же эффект.

2 голосов
/ 25 марта 2012

Обратите внимание, что понимание Скалы работает совсем по-другому.Это:

for(v <- values) funcs += (() => v)

переводится во время компиляции в это:

values.foreach(v => funcs += (() => v))

Так что v является новой переменной для каждого значения.

1 голос
/ 23 марта 2012

Если вы разберете пример 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 #:

  1. Создание объекта области замыкания
  2. Перебор значений ...
    1. Нажмите ссылку нафункция доступа к замыканию в funcs
    2. Обновите v объекта области замыкания на текущее значение.
  3. Итератор по funcs.Каждый вызов вернет текущее значение v.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...