Закрытие семантики для 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.