2015-09-17 34 views
12

In C# 5, la semantica di chiusura dell'istruzione foreach (quando la variabile di iterazione è "acquisita" o "chiusa sopra" da funzioni anonime) era famously changed (link to thread on that topic).Semantica di chiusura per foreach su array di tipi di puntatore

Domanda: Era l'intenzione di cambiare questo anche per gli array di tipi di puntatore?

Il motivo per cui mi chiedo è che il "espansione" di una dichiarazione foreach deve essere riscritta, per motivi tecnici (non possiamo usare la proprietà Current del System.Collections.IEnumerator poiché questa proprietà ha dichiarato tipo object che non è compatibile con un puntatore tipo) rispetto a foreach rispetto ad altre raccolte. La sezione relativa nel linguaggio C# Specification, di "matrici Pointer", in versione 5.0, dice che:

foreach (V v in x) EMBEDDED-STATEMENT 

si espande a:

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

Notiamo che la dichiarazione V v; è al di fuori di tutti i loop for. Quindi sembrerebbe che la semantica di chiusura sia ancora il "vecchio" sapore di C# 4, "la variabile di loop viene riutilizzata, la variabile di ciclo è" esterna "rispetto al ciclo".

per far capire di cosa sto parlando, si consideri questo completo programma C# 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. 
} 

Allora, qual è il corretto output del programma di cui sopra?

+0

Nota: È possibile che l'espansione ha un altro problema evidente in quanto scrive 'a.GetValue (I0, I1, ..., in)' 'dove GetValue' sembra essere il metodo definito da' Sistema .Array'. Ma quel metodo ha valore di ritorno 'oggetto', quindi non può essere usato per i tipi di puntatore. Quindi la specifica C# non riesce ad evitare _ "qualsiasi tentativo di accedere agli elementi dell'array tramite' System.Array' "_, per citare la specifica C# stessa. Forse avrebbe dovuto essere "a [i0, i1, ..., iN]" dove la parentesi '[...]' è definita dalla sottosezione _ "Array element access" _. Prova a dire 'arrayOfPointers.GetValue (0)' da solo, nell'esempio di codice precedente. –

risposta

11

Ho contattato Mads Torgersen, il C# Language PM, e sembra che abbiano semplicemente dimenticato di aggiornare questa parte delle specifiche. His exact answer era (ho chiesto perché la specifica non è stata aggiornata):

perché ho dimenticato! :-) Ora ho nell'ultima bozza e presentato all'ECMA. Grazie!

Quindi sembra che il comportamento di C# -5 sia identico anche per gli array di puntatore, ed è per questo che stai vedendo il primo output, che è quello corretto.

+0

Forse correggerà il problema anche con 'GetValue' (vedi il mio commento appena sotto la mia domanda sopra)? –

4

Suppongo che la specifica non sia stata aggiornata in questa parte (sugli array di puntatori) per riflettere che anche la variabile V va allo scope interno. Se compilate il vostro esempio con il compilatore C# 5 e guardate l'output, apparirà come nella specifica (con l'accesso dell'array invece di GetValue come puntate correttamente nel vostro commento), tranne che la variabile V sarà all'interno di tutti i loop. E l'uscita sarà 00-02-04-C8, ma ovviamente lo sapete voi stessi :)

Per farla breve - certo che non posso dire se fosse intenzionale o meno, ma la mia ipotesi è che fosse inteso per spostare la variabile sull'ambito interno per tutti i cicli foreach, inclusi gli array di puntatori, e la specifica non è stata aggiornata per riflettere questo.

4

Il seguente codice è compilato (C# 5.0) al dato codice IL(Commenti nel codice):

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

Come si può vedere, una classe viene generata in fase di compilazione, chiamati <>c__DisplayClass0_0 che contiene il Action e valore di char*. La classe si presenta così:

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

Nel codice MSIL possiamo vedere che la foreach è redatto al seguente ciclo for:

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 
} 

che cosa significa in modo che il il valore del puntatore viene effettivamente copiato quando il ciclo viene iterato e vengono creati i delegati, quindi il valore del puntatore in quel momento è quello che verrà stampato (a.k.a: ogni azione proviene dalla propria istanza di <>c__DisplayClass0_0 e riceverà il puntatore temporaneo clonato).

Come abbiamo appena visto, il "reused variable" da prima della foreach è la matrice stessa, il che significa che i puntatori di riferimento non vengono riutilizzati che significa che se le specifiche sono, come si sta dicendo, quello che sono sbagliate in quanto le specifiche che avete attched suggerire che l'output dovrebbe essere 00 00 00 00. E il risultato:

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

Questo post contiene molti dettagli di implementazione (suppongo da alcune implementazioni di C# versione 5 o 6?), Ma in realtà non affronta la domanda a cui ero interessato: La specifica del linguaggio C# richiede che l'output sia 'C8 C8 C8 C8', o la specifica C# richiede che l'uscita sia "00 02 04 C8', o nessuno dei due? –

+0

Modificare la fine della risposta con riferimento alla specifica richiesta. –