2015-09-17 13 views
12

In C# 5 war die Abschluss-Semantik der foreach-Anweisung (wenn die Iterationsvariable von anonymen Funktionen "erfasst" oder "geschlossen" wurde) famously changed (link to thread on that topic).Closure-Semantik für foreach over Arrays von Zeigertypen

Frage: War es die Absicht, dies für Arrays von Zeigertypen auch zu ändern?

Der Grund, warum ich frage ist, dass die „Erweiterung“ eine foreach Anweisung neu geschrieben werden muss, aus technischen Gründen (wir nicht die Current Eigenschaft der System.Collections.IEnumerator verwenden können, da diese Eigenschaft Typ deklariert hat object, die mit einem Zeiger nicht kompatibel ist Typ) im Vergleich zu foreach gegenüber anderen Sammlungen. Der entsprechende Abschnitt in der C# Language Specification, "Pointer-Arrays", in Version 5.0, sagt, dass:

foreach (V v in x) EMBEDDED-STATEMENT 

zu erweitert wird:

{ 
    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 
    } 
} 

Wir nehmen zur Kenntnis, dass die Erklärung V v; ist außerhalb aller for Schleifen. So scheint es, dass die Closing-Semantik immer noch der "alte" C# 4-Flavor ist, "Loop-Variable wird wiederverwendet, Loop-Variable ist" äußerlich "in Bezug auf die Schleife".

Um deutlich zu machen, was ich rede, halte das komplette C# 5-Programm:

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. 
} 

Also, was die korrekte Ausgabe des obigen Programms ist?

+0

Anmerkung: Die obige Expansion hat ein weiteres offensichtliches Problem, dass es schreibt 'a.GetValue (i0, i1, ..., iN)' 'wo GetValue' scheint die Methode' System definiert werden .Array'. Aber diese Methode hat den Rückgabewert 'object' und kann daher nicht für Zeigertypen verwendet werden. Daher ist die C# -Spezifikation nicht in der Lage, _ "jeden Versuch zu vermeiden, auf die Array-Elemente über' System.Array' "_ zuzugreifen, um die C# -Spezifikation selbst zu zitieren. Vielleicht hätte das "a [i0, i1, ..., iN]" sein sollen, wobei die Klammer '[...]' durch den Unterabschnitt _ "Array element access" _ definiert wird. Versuchen Sie im obigen Codebeispiel "arrayOfPointers.GetValue (0)" selbst zu sagen. –

Antwort

11

Ich habe Mads Torgersen, die C# Sprach-PM, kontaktiert und es scheint, dass sie einfach vergessen haben, diesen Teil der Spezifikation zu aktualisieren. His exact answer war (ich fragte warum die Spezifikation nicht aktualisiert wurde):

weil ich vergessen habe! :-) Ich habe jetzt im neuesten Entwurf und an ECMA eingereicht. Vielen Dank!

So scheint es, dass das Verhalten von C# -5 auch für Zeiger-Arrays identisch ist, und es ist, warum Sie die erste Ausgabe sind zu sehen, die die richtige ist.

+0

Vielleicht wird er das Problem auch mit 'GetValue' beheben (siehe meinen Kommentar direkt unter meiner Frage oben)? –

4

Ich nehme an, dass die Spezifikation in diesem Teil (über Zeigerarrays) gerade nicht aktualisiert wurde, um widerzuspiegeln, dass V-Variable auch zum inneren Bereich geht. Wenn Sie Ihr Beispiel mit dem C# 5-Compiler kompilieren und die Ausgabe betrachten, wird es wie in der Spezifikation aussehen (mit Array-Zugriff statt GetValue, wie Sie in Ihrem Kommentar richtig zeigen), außer dass die Variable V innerhalb aller for-Schleifen liegt. Und Ausgabe wird 00-02-04-C8 sein, aber natürlich weißt du alles selbst :)

Lange Rede kurzer Sinn - natürlich kann ich nicht sagen, ob das Absicht war oder nicht, aber meine Vermutung ist, dass es beabsichtigt war Verschieben der Variablen in den inneren Bereich für alle foreach-Schleifen, einschließlich Zeiger-Arrays, und die Spezifikation wurde einfach nicht aktualisiert, um dies zu berücksichtigen.

4

Der folgende Code wird kompiliert (C# 5.0) an den angegebenen IL-Code(Kommentare in Code):

.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 
} 

Wie Sie sehen können, eine Klasse in Kompilierung- erzeugt wird, genannt <>c__DisplayClass0_0 die Ihre Action enthält und eine Wert von char*. Die Klasse sieht wie folgt aus:

[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)); 
    } 
} 

Im MSIL Code wir sehen können, dass die foreach auf die folgende for-Schleife kompiliert wird:

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 
} 

Was es bedeutet, dass die Wert des Zeigers wird tatsächlich kopiert, wenn die Schleife iteriert wird und die Delegierten erstellt werden, so der Wert des Zeigers zu der Zeit ist derjenige, der gedruckt wird (a.k.a: Jede Aktion stammt von ihrer eigenen Instanz <>c__DisplayClass0_0 und erhält ihren temporären geklonten Zeiger).

Wie wir gerade gesehen haben, die "reused variable" aus der Zeit vor dem foreach ist das Array selbst, das heißt, die die referenzierten Zeiger nicht wiederverwendet werden, was bedeutet, dass, wenn die Spezifikationen sind wie Sie sagen, als sie, da die Spezifikationen falsch sind Sie attched vorschlagen, dass die Ausgabe 00 00 00 00 sein sollte. Und das Ergebnis:

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

Dieser Beitrag enthält sehr viele Implementierungsdetails (ich nehme an, aus einer Implementierung von C# Version 5 oder 6?), Aber es befasst sich nicht wirklich mit der Frage, die mich interessiert: Setzt die C# -Sprachspezifikation die Ausgabe als C8 C8 C8 voraus C8', oder verlangt die C# -Spezifikation, dass der Ausgang '00 02 04 C8' ist oder keines? –

+0

Ändern Sie das Ende der Antwort mit Bezug auf die angeforderte Spezifikation. –