Закрытие семантики для foreach над массивами типов указателей

В С# 5 семантика замыкания оператора foreach (когда переменная итерации "захвачена" или "закрыта" анонимными функциями) была лихо изменена (ссылка на тему в этой теме).

Вопрос: Было ли намерено также изменить это для массивов типов указателей?

Причина, по которой я спрашиваю, заключается в том, что "расширение" оператора foreach должно быть переписано по техническим причинам (мы не можем использовать свойство Current для System.Collections.IEnumerator, поскольку это свойство объявило тип object, который несовместим с типом указателя) по сравнению с foreach над другими коллекциями. Соответствующий раздел в Спецификации языка С# "Матрицы указателей" в версии 5.0 говорит, что:

foreach (V v in x) EMBEDDED-STATEMENT

расширяется до:

{
  T[,,…,] a = x;
  V v;
  for (int i0 = a.GetLowerBound(0); i0 <= a.GetUpperBound(0); i0++)
  for (int i1 = a.GetLowerBound(1); i1 <= a.GetUpperBound(1); i1++)
  …
  for (int in = a.GetLowerBound(N); iN <= a.GetUpperBound(n); iN++) {
    v = (V)a.GetValue(i0,i1,…,iN);
    EMBEDDED-STATEMENT
  }
}

Заметим, что объявление V v; находится за пределами всех циклов for. Таким образом, кажется, что семантика закрытия по-прежнему является "старым" вкусом С# 4, "переменная цикла повторно используется, переменная цикла является" внешней "по отношению к циклу".

Чтобы понять, о чем я говорю, рассмотрите эту полную программу С# 5:

using System;
using System.Collections.Generic;

static class Program
{
  unsafe static void Main()
  {
    char* zeroCharPointer = null;
    char*[] arrayOfPointers =
      { zeroCharPointer, zeroCharPointer + 1, zeroCharPointer + 2, zeroCharPointer + 100, };

    var list = new List<Action>();

    // foreach through pointer array, capture each foreach variable 'pointer' in a lambda
    foreach (var pointer in arrayOfPointers)
      list.Add(() => Console.WriteLine("Pointer address is {0:X2}.", (long)pointer));

    Console.WriteLine("List complete");
    // invoke those delegates
    foreach (var act in list)
      act();
  }

  // Possible output:
  //
  // List complete
  // Pointer address is 00.
  // Pointer address is 02.
  // Pointer address is 04.
  // Pointer address is C8.
  //
  // Or:
  //
  // List complete
  // Pointer address is C8.
  // Pointer address is C8.
  // Pointer address is C8.
  // Pointer address is C8.
}

Итак, каков правильный вывод вышеуказанной программы?

Ответ 1

Я связался с Мэдсом Торгерсеном, преподавателем языка С#, и, похоже, они просто забыли обновить эту часть спецификации. Его точный ответ (я спросил, почему спецификация не была обновлена):

потому что я забыл!:-) Сейчас у меня есть последний проект и представлен в ECMA. Благодарю!

Итак, похоже, что поведение С# -5 идентично для массивов указателей, и именно поэтому вы видите первый результат, который является правильным.

Ответ 2

Я предполагаю, что эта спецификация просто не обновилась в этой части (о массивах указателей), чтобы отразить, что переменная V также попадает во внутреннюю область. Если вы компилируете свой пример с компилятором С# 5 и смотрите на результат - он будет выглядеть в спецификации (с доступом к массиву вместо GetValue, как вы правильно указываете в своем комментарии), за исключением того, что переменная V будет внутри всех для циклов. И выход будет 00-02-04-C8, но, конечно, вы сами знаете все это:)

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

Ответ 3

Следующий код компилирует (С# 5.0) в данный код IL (комментарии в коде):

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 6
    .locals init (
        [0] char* chPtr,
        [1] char*[] chPtrArray,
        [2] class [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action> list,
        [3] char*[] chPtrArray2,
        [4] int32 num,
        [5] class ConsoleTests.Program/<>c__DisplayClass0_0 class_,
        [6] valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Action> enumerator,
        [7] class [mscorlib]System.Action action)
    L_0000: nop 
    L_0001: ldc.i4.0 //{{{{{
    L_0002: conv.u  //chPtr = null;
    L_0003: stloc.0 //}}}}}
    L_0004: ldc.i4.4 //{{{{{
    L_0005: newarr char* //Creates a new char*[4]}}}}}
    L_000a: dup //{{{{{
    L_000b: ldc.i4.0 // Sets the first element in the new
    L_000c: ldloc.0 // char*[] to chPtr.
    L_000d: stelem.i //}}}}}
    L_000e: dup //{{{{{
    L_000f: ldc.i4.1 //
    L_0010: ldloc.0 // Sets the second element of the
    L_0011: ldc.i4.2 // char*[] to chPtr + 1 
    L_0012: add // (loads 2 instead of 1 because char is UTF-16)
    L_0013: stelem.i //}}}}}
    L_0014: dup //{{{{{
    L_0015: ldc.i4.2 // 
    L_0016: ldloc.0 //
    L_0017: ldc.i4.2 // Sets the third element of the
    L_0018: conv.i // char*[] to chPtr + 2
    L_0019: ldc.i4.2 // (loads 4 instead of 2 because char is UTF-16)
    L_001a: mul //
    L_001b: add //
    L_001c: stelem.i //}}}}}
    L_001d: dup //{{{{{
    L_001e: ldc.i4.3 //
    L_001f: ldloc.0 //
    L_0020: ldc.i4.s 100 // Sets the third element of the
    L_0022: conv.i // char*[] to chPtr + 100
    L_0023: ldc.i4.2 // (loads 200 instead of 100 because char is UTF-16)
    L_0024: mul //
    L_0025: add //
    L_0026: stelem.i // }}}}}
    L_0027: stloc.1 // chPtrArray = the new array that we have just filled.
    L_0028: newobj instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::.ctor() //{{{{{
    L_002d: stloc.2 // list = new List<Action>()
    L_002e: nop //}}}}}
    L_002f: ldloc.1 //{{{{{
    L_0030: stloc.3 //chPtrArray2 = chPtrArray}}}}}
    L_0031: ldc.i4.0 //for (int num = 0; num < 3; num++)
    L_0032: stloc.s num //
    L_0034: br.s L_0062 //<<<<< (for start)
    L_0036: newobj instance void ConsoleTests.Program/<>c__DisplayClass0_0::.ctor() //{{{{{
    L_003b: stloc.s class_ //class_ = new temporary compile-time class
    L_003d: ldloc.s class_ //}}}}}
    L_003f: ldloc.3 //{{{{{
    L_0040: ldloc.s num //
    L_0042: ldelem.i //
    L_0043: stfld char* ConsoleTests.Program/<>c__DisplayClass0_0::pointer //class_.pointer = chPtrArray2[num]}}}}}
    L_0048: ldloc.2 //{{{{{
    L_0049: ldloc.s class_ //
    L_004b: ldftn instance void ConsoleTests.Program/<>c__DisplayClass0_0::<Main>b__0() // list.Add(class_.<Main>b__0);
    L_0051: newobj instance void [mscorlib]System.Action::.ctor(object, native int) // (Adds the temporary compile-time class action, which has the correct pointer since
    L_0056: callvirt instance void [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::Add(!0) //it is a specific class instace for this iteration, to the list)}}}}}
    L_005b: nop 
    L_005c: ldloc.s num //practically the end of the for
    L_005e: ldc.i4.1 // (actually increasing num and comparing)
    L_005f: add //
    L_0060: stloc.s num //
    L_0062: ldloc.s num //
    L_0064: ldloc.3 //
    L_0065: ldlen //
    L_0066: conv.i4 //
    L_0067: blt.s L_0036 //>>>>> (for complete)
    L_0069: ldstr "List complete" //Printing and stuff.....
    L_006e: call void [mscorlib]System.Console::WriteLine(string)
    L_0073: nop 
    L_0074: nop 
    L_0075: ldloc.2 
    L_0076: callvirt instance valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator`0<!0> [mscorlib]System.Collections.Generic.List`1<class [mscorlib]System.Action>::GetEnumerator()
    L_007b: stloc.s enumerator
    L_007d: br.s L_0090
    L_007f: ldloca.s enumerator
    L_0081: call instance !0 [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Action>::get_Current()
    L_0086: stloc.s action
    L_0088: ldloc.s action
    L_008a: callvirt instance void [mscorlib]System.Action::Invoke()
    L_008f: nop 
    L_0090: ldloca.s enumerator
    L_0092: call instance bool [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Action>::MoveNext()
    L_0097: brtrue.s L_007f
    L_0099: leave.s L_00aa
    L_009b: ldloca.s enumerator
    L_009d: constrained. [mscorlib]System.Collections.Generic.List`1/Enumerator`0<class [mscorlib]System.Action>
    L_00a3: callvirt instance void [mscorlib]System.IDisposable::Dispose()
    L_00a8: nop 
    L_00a9: endfinally 
    L_00aa: ret 
    .try L_007d to L_009b finally handler L_009b to L_00aa
}

Как вы можете видеть, генерируется класс во время компиляции, называемый <>c__DisplayClass0_0, который содержит ваши Action и значение char*. Класс выглядит следующим образом:

[CompilerGenerated]
private sealed class <>c__DisplayClass0_0
{
    // Fields
    public unsafe char* pointer;

    // Methods
    internal unsafe void <Main>b__0()
    {
        Console.WriteLine("Pointer address is {0:X2}.", (long) ((ulong) this.pointer));
    }
}

В MSIL-коде мы видим, что foreach скомпилирован для цикла:

shallowCloneOfArray = arrayOfPointers;
for (int num = 0; num < arrayOfPointers.Length; num++)
{
    <>c__DisplayClass0_0 temp = new <>c__DisplayClass0_0();
    temp.pointer = shallowCloneOfArray[num];
    list.Add(temp.<Main>b__0); //Adds the action to the list of actions
}

Что это означает, что значение фактического копирования указателя, когда цикл повторяется и создаются делегаты, поэтому значение указателя в момент тот, который будет напечатан (aka: каждое действие происходит от его собственного экземпляра <>c__DisplayClass0_0 и получит его временный клонированный указатель).

Как мы только что видели, "reused variable" перед foreach является самим массивом, что означает, что ссылочные указатели не используются повторно, а это означает, что если спецификации указаны так, как вы говорите, они ошибочны, поскольку спецификации, которые вы указали, предполагают, что выход должен быть 00 00 00 00. И результат:

List complete
Pointer address is 00.
Pointer address is 02.
Pointer address is 04.
Pointer address is C8.