No post de hoje, gostaríamos de demonstrar como os compiladores de C# (tanto o Roslyn, que converte o código C# em Intermediate Language, quanto o JIT, que converte o código em Intermediate Language em assembly) tratam uma instrução que usamos com muita frequência: o foreach.
Iremos abordar alguns cenários de uso:
- Usando foreach para iterar caracteres de uma string
- Usando foreach para iterar em elementos de um array (tipo primitivo)
- Usando foreach para iterar em enumerações (IEnumerable)
- Usando foreach para iterar em enumerações (que não são IEnumerable)
- Usando foreach para iterar em enumerações (quando o Enumerator precisa ser IDisposable)
- Usando foreach para iterar em enumerações (IEnumerable e sem boxing)
O conhecimento desses cenários ajuda na escrita de código mais eficiente.
Usando foreach com strings
O código a seguir é uma função que, recebe uma string como parâmetro, e utiliza foreach para iterar nos caracteres dessa string.
public static void PrintAllChars(string s) { foreach (var element in s) { Console.WriteLine(element); } }
O processo de compilação desse código, em Release, gera o seguinte Intermediate Language:
.method public hidebysig static void PrintAllChars ( string s ) cil managed { // Method begins at RVA 0x2054 // Code size 32 (0x20) .maxstack 2 .locals init ( [0] string, [1] int32 ) // Console.WriteLine(s[i]); IL_0000: ldarg.0 // (no C# code) IL_0001: stloc.0 // for (int i = 0; i < s.Length; i++) IL_0002: ldc.i4.0 IL_0003: stloc.1 // (no C# code) IL_0004: br.s IL_0016 // loop start (head: IL_0016) IL_0006: ldloc.0 IL_0007: ldloc.1 IL_0008: callvirt instance char [System.Runtime]System.String::get_Chars(int32) IL_000d: call void [System.Console]System.Console::WriteLine(char) // for (int i = 0; i < s.Length; i++) IL_0012: ldloc.1 IL_0013: ldc.i4.1 IL_0014: add IL_0015: stloc.1 // (no C# code) IL_0016: ldloc.1 IL_0017: ldloc.0 IL_0018: callvirt instance int32 [System.Runtime]System.String::get_Length() IL_001d: blt.s IL_0006 // end loop IL_001f: ret } // end of method Program::PrintAllChars
Podemos verificar que esse código:
- indica a criação de um registro de stack com duas variáveis locais (uma string e outra com um inteiro);
- faz uma cópia da do argumento para uma variável local;
- itera na string, utilizando um laço for simples, usando o indexer da string para obter cada caractere, utilizando a propriedade Length como limite máximo do laço;
Quando esse código é executado, o JIT consegue entregar uma implementação “quase” ótima.
00007FFF3F3114E3 movsxd rcx,edi 00007FFF3F3114E6 movzx ecx,word ptr [rsi+rcx*2+0Ch] 00007FFF3F3114EB call 00007FFF3F311308 00007FFF3F3114F0 inc edi 00007FFF3F3114F2 cmp ebx,edi 00007FFF3F3114F4 jg 00007FFF3F3114E3 00007FFF3F3114F6 add rsp,20h 00007FFF3F3114FA pop rbx 00007FFF3F3114FB pop rsi 00007FFF3F3114FC pop rdi 00007FFF3F3114FD ret
Importante destacar que a chamada para o indexer da string não aparece no assembly (get_Chars sumiu graças a inlining). A única função chamada no assembly é Console.WriteLine (no call)
NOTA TÉCNICA: Este mesmo código, escrito com for no lugar de foreach, geraria um Intermediate Language praticamente idêntico (a diferença seria que não haveria cópia do argumento para uma variável local) . Entretanto, essa pequena diferença já seria suficiente para que o JIT conseguisse aplicar uma otimização extra – inlining em todas as chamadas para o método. Percorrer os caracteres de uma string utilizando for é levemente mais performático do que usando foreach.
O JIT pode aplicar inlining na função que usamos no exemplo desde que a marquemos como candidata para inlining agressivo.
foreach em Arrays (de tipos primitivos)
A mesma lógica aplicada pelo compilador para iterar em uma string é aplicada pelos compiladores para iterar em um array.
O código a seguir, recebe um array de inteiros, imprimindo cada elemento.
public static void PrintAllNumbers(int[] numbers) { foreach (var element in numbers) { Console.WriteLine(element); } }
O assembly que itera nos elementos do array é bastante econômico.
00007FFF3F851523 movsxd rcx,edi 00007FFF3F851526 mov ecx,dword ptr [rsi+rcx*4+10h] 00007FFF3F85152A call 00007FFF3F851340 00007FFF3F85152F inc edi 00007FFF3F851531 cmp ebx,edi 00007FFF3F851533 jg 00007FFF3F851523 00007FFF3F851535 add rsp,20h 00007FFF3F851539 pop rbx 00007FFF3F85153A pop rsi 00007FFF3F85153B pop rdi 00007FFF3F85153C ret
foreach em IEnumerables
foreach é extremamente eficiente para operar com enumerações. Aliás, enumerações são extremamente importantes para a escrita de códigos de alta-performance, sobretudo quando execução lazy é possível.
using System; using System.Collections.Generic; class Program { static void Main() { foreach (var element in GetNumbers()) { Console.WriteLine(element); } } public static IEnumerable<int> GetNumbers() { for (int i = 1; i < 10; i++) { yield return i; } } }
Os compiladores, ao gerar código executável para o foreach do código acima, utilizam os membros das interfaces IEnumerable e IEnumerator para percorrer adequadamente a enumeração.
No código em IL que segue vimos como nossa implementação de GetNumbers faça com que compilador escreva uma classe anônima para suportar o yield return.
.class private auto ansi beforefieldinit Program extends [System.Runtime]System.Object { // Nested Types .class nested private auto ansi sealed beforefieldinit '<GetNumbers>d__1' extends [System.Runtime]System.Object implements class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>, [System.Runtime]System.Collections.IEnumerable, class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32>, [System.Runtime]System.Collections.IEnumerator, [System.Runtime]System.IDisposable { .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Fields .field private int32 '<>1__state' .field private int32 '<>2__current' .field private int32 '<>l__initialThreadId' .field private int32 '<i>5__2' // Methods .method public hidebysig specialname rtspecialname instance void .ctor ( int32 '<>1__state' ) cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) // Method begins at RVA 0x20ad // Code size 25 (0x19) .maxstack 8 // (no C# code) IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() // this.<>1__state = <>1__state; IL_0006: ldarg.0 IL_0007: ldarg.1 IL_0008: stfld int32 Program/'<GetNumbers>d__1'::'<>1__state' // <>l__initialThreadId = Environment.CurrentManagedThreadId; IL_000d: ldarg.0 IL_000e: call int32 [System.Runtime.Extensions]System.Environment::get_CurrentManagedThreadId() IL_0013: stfld int32 Program/'<GetNumbers>d__1'::'<>l__initialThreadId' // (no C# code) IL_0018: ret } // end of method '<GetNumbers>d__1'::.ctor .method private final hidebysig newslot virtual instance void System.IDisposable.Dispose () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance void [System.Runtime]System.IDisposable::Dispose() // Method begins at RVA 0x20c7 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method '<GetNumbers>d__1'::System.IDisposable.Dispose .method private final hidebysig newslot virtual instance bool MoveNext () cil managed { .override method instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext() // Method begins at RVA 0x20cc // Code size 88 (0x58) .maxstack 3 .locals init ( [0] int32, [1] int32 ) // switch (<>1__state) IL_0000: ldarg.0 IL_0001: ldfld int32 Program/'<GetNumbers>d__1'::'<>1__state' IL_0006: stloc.0 // (no C# code) IL_0007: ldloc.0 IL_0008: brfalse.s IL_0010 IL_000a: ldloc.0 IL_000b: ldc.i4.1 IL_000c: beq.s IL_0035 // return false; IL_000e: ldc.i4.0 // (no C# code) IL_000f: ret // <>1__state = -1; IL_0010: ldarg.0 IL_0011: ldc.i4.m1 IL_0012: stfld int32 Program/'<GetNumbers>d__1'::'<>1__state' // <i>5__2 = 1; IL_0017: ldarg.0 IL_0018: ldc.i4.1 IL_0019: stfld int32 Program/'<GetNumbers>d__1'::'<i>5__2' // (no C# code) IL_001e: br.s IL_004c // <>2__current = <i>5__2; IL_0020: ldarg.0 IL_0021: ldarg.0 IL_0022: ldfld int32 Program/'<GetNumbers>d__1'::'<i>5__2' IL_0027: stfld int32 Program/'<GetNumbers>d__1'::'<>2__current' // <>1__state = 1; IL_002c: ldarg.0 IL_002d: ldc.i4.1 IL_002e: stfld int32 Program/'<GetNumbers>d__1'::'<>1__state' // return true; IL_0033: ldc.i4.1 // (no C# code) IL_0034: ret // <>1__state = -1; IL_0035: ldarg.0 IL_0036: ldc.i4.m1 IL_0037: stfld int32 Program/'<GetNumbers>d__1'::'<>1__state' // <i>5__2++; IL_003c: ldarg.0 IL_003d: ldfld int32 Program/'<GetNumbers>d__1'::'<i>5__2' // (no C# code) IL_0042: stloc.1 IL_0043: ldarg.0 IL_0044: ldloc.1 IL_0045: ldc.i4.1 IL_0046: add IL_0047: stfld int32 Program/'<GetNumbers>d__1'::'<i>5__2' // if (<i>5__2 < 10) IL_004c: ldarg.0 IL_004d: ldfld int32 Program/'<GetNumbers>d__1'::'<i>5__2' IL_0052: ldc.i4.s 10 IL_0054: blt.s IL_0020 // return false; IL_0056: ldc.i4.0 // (no C# code) IL_0057: ret } // end of method '<GetNumbers>d__1'::MoveNext .method private final hidebysig specialname newslot virtual instance int32 'System.Collections.Generic.IEnumerator<System.Int32>.get_Current' () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance !0 class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32>::get_Current() // Method begins at RVA 0x2130 // Code size 7 (0x7) .maxstack 8 // return <>2__current; IL_0000: ldarg.0 IL_0001: ldfld int32 Program/'<GetNumbers>d__1'::'<>2__current' // (no C# code) IL_0006: ret } // end of method '<GetNumbers>d__1'::'System.Collections.Generic.IEnumerator<System.Int32>.get_Current' .method private final hidebysig newslot virtual instance void System.Collections.IEnumerator.Reset () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance void [System.Runtime]System.Collections.IEnumerator::Reset() // Method begins at RVA 0x2138 // Code size 6 (0x6) .maxstack 8 // throw new NotSupportedException(); IL_0000: newobj instance void [System.Runtime]System.NotSupportedException::.ctor() // (no C# code) IL_0005: throw } // end of method '<GetNumbers>d__1'::System.Collections.IEnumerator.Reset .method private final hidebysig specialname newslot virtual instance object System.Collections.IEnumerator.get_Current () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance object [System.Runtime]System.Collections.IEnumerator::get_Current() // Method begins at RVA 0x213f // Code size 12 (0xc) .maxstack 8 // return <>2__current; IL_0000: ldarg.0 IL_0001: ldfld int32 Program/'<GetNumbers>d__1'::'<>2__current' IL_0006: box [System.Runtime]System.Int32 // (no C# code) IL_000b: ret } // end of method '<GetNumbers>d__1'::System.Collections.IEnumerator.get_Current .method private final hidebysig newslot virtual instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32> 'System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator' () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0> class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() // Method begins at RVA 0x214c // Code size 43 (0x2b) .maxstack 2 .locals init ( [0] class Program/'<GetNumbers>d__1' ) // if (<>1__state == -2 && <>l__initialThreadId == Environment.CurrentManagedThreadId) IL_0000: ldarg.0 IL_0001: ldfld int32 Program/'<GetNumbers>d__1'::'<>1__state' IL_0006: ldc.i4.s -2 IL_0008: bne.un.s IL_0022 IL_000a: ldarg.0 IL_000b: ldfld int32 Program/'<GetNumbers>d__1'::'<>l__initialThreadId' IL_0010: call int32 [System.Runtime.Extensions]System.Environment::get_CurrentManagedThreadId() IL_0015: bne.un.s IL_0022 // <>1__state = 0; IL_0017: ldarg.0 IL_0018: ldc.i4.0 IL_0019: stfld int32 Program/'<GetNumbers>d__1'::'<>1__state' // return this; IL_001e: ldarg.0 IL_001f: stloc.0 // (no C# code) IL_0020: br.s IL_0029 // return new <GetNumbers>d__1(0); IL_0022: ldc.i4.0 IL_0023: newobj instance void Program/'<GetNumbers>d__1'::.ctor(int32) IL_0028: stloc.0 // (no C# code) IL_0029: ldloc.0 IL_002a: ret } // end of method '<GetNumbers>d__1'::'System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator' .method private final hidebysig newslot virtual instance class [System.Runtime]System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator () cil managed { .custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) .override method instance class [System.Runtime]System.Collections.IEnumerator [System.Runtime]System.Collections.IEnumerable::GetEnumerator() // Method begins at RVA 0x2183 // Code size 7 (0x7) .maxstack 8 // return System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator(); IL_0000: ldarg.0 IL_0001: call instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32> Program/'<GetNumbers>d__1'::'System.Collections.Generic.IEnumerable<System.Int32>.GetEnumerator'() // (no C# code) IL_0006: ret } // end of method '<GetNumbers>d__1'::System.Collections.IEnumerable.GetEnumerator // Properties .property instance int32 'System.Collections.Generic.IEnumerator<System.Int32>.Current'() { .get instance int32 Program/'<GetNumbers>d__1'::'System.Collections.Generic.IEnumerator<System.Int32>.get_Current'() } .property instance object System.Collections.IEnumerator.Current() { .get instance object Program/'<GetNumbers>d__1'::System.Collections.IEnumerator.get_Current() } } // end of class <GetNumbers>d__1 // Methods .method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2050 // Code size 45 (0x2d) .maxstack 1 .entrypoint .locals init ( [0] class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32> ) // foreach (int number in GetNumbers()) IL_0000: call class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> Program::GetNumbers() IL_0005: callvirt instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0> class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() // (no C# code) IL_000a: stloc.0 .try { IL_000b: br.s IL_0018 // loop start (head: IL_0018) // foreach (int number in GetNumbers()) IL_000d: ldloc.0 IL_000e: callvirt instance !0 class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32>::get_Current() // Console.WriteLine(number); IL_0013: call void [System.Console]System.Console::WriteLine(int32) // foreach (int number in GetNumbers()) IL_0018: ldloc.0 IL_0019: callvirt instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext() // (no C# code) IL_001e: brtrue.s IL_000d // end loop IL_0020: leave.s IL_002c } // end .try finally { IL_0022: ldloc.0 IL_0023: brfalse.s IL_002b IL_0025: ldloc.0 IL_0026: callvirt instance void [System.Runtime]System.IDisposable::Dispose() IL_002b: endfinally } // end handler IL_002c: ret } // end of method Program::Main .method public hidebysig static class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> GetNumbers () cil managed { .custom instance void [System.Runtime]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = ( 01 00 18 50 72 6f 67 72 61 6d 2b 3c 47 65 74 4e 75 6d 62 65 72 73 3e 64 5f 5f 31 00 00 ) // Method begins at RVA 0x209c // Code size 8 (0x8) .maxstack 8 // (no C# code) IL_0000: ldc.i4.s -2 IL_0002: newobj instance void Program/'<GetNumbers>d__1'::.ctor(int32) IL_0007: ret } // end of method Program::GetNumbers .method public hidebysig specialname rtspecialname instance void .ctor () cil managed { // Method begins at RVA 0x20a5 // Code size 7 (0x7) .maxstack 8 IL_0000: ldarg.0 IL_0001: call instance void [System.Runtime]System.Object::.ctor() IL_0006: ret } // end of method Program::.ctor } // end of class Program
O problema é que se em nossos códigos, utilizarmos a interface IEnumerable para tipar a variável que iremos iterar no foreach, faremos com que ele siga o mesmo padrão que seria adotado para iterar em enumerações lazy.
Vejamos, por exemplo, o que acontece com nosso código, que imprime os elementos de um array, se, no lugar de especificar o tipo do parâmetro como um array, indicarmos um IEnumerable.
public static void PrintAllNumbers(IEnumerable<int> numbers) { foreach (var element in numbers) { Console.WriteLine(element); } }
Essa simples alteração, faz com que o compilador gere o código de forma completamente diferente daquela para operar com uma array (é mais “genérico”, mas é menos performático).
.method public hidebysig static void PrintAllNumbers ( class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32> numbers ) cil managed { // Method begins at RVA 0x206c // Code size 41 (0x29) .maxstack 1 .locals init ( [0] class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32> ) // foreach (int number in numbers) IL_0000: ldarg.0 IL_0001: callvirt instance class [System.Runtime]System.Collections.Generic.IEnumerator`1<!0> class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>::GetEnumerator() // (no C# code) IL_0006: stloc.0 .try { IL_0007: br.s IL_0014 // loop start (head: IL_0014) // foreach (int number in numbers) IL_0009: ldloc.0 IL_000a: callvirt instance !0 class [System.Runtime]System.Collections.Generic.IEnumerator`1<int32>::get_Current() // Console.WriteLine(number); IL_000f: call void [System.Console]System.Console::WriteLine(int32) // foreach (int number in numbers) IL_0014: ldloc.0 IL_0015: callvirt instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext() // (no C# code) IL_001a: brtrue.s IL_0009 // end loop IL_001c: leave.s IL_0028 } // end .try finally { IL_001e: ldloc.0 IL_001f: brfalse.s IL_0027 IL_0021: ldloc.0 IL_0022: callvirt instance void [System.Runtime]System.IDisposable::Dispose() IL_0027: endfinally } // end handler IL_0028: ret }
Seria o mesmo que percorrer o array assim:
public static void PrintAllNumbers(IEnumerable<int> numbers) { using (var enumerator = numbers.GetEnumerator()) { while (enumerator.MoveNext()) { Console.WriteLine(enumerator.Current); } } }
Como pode ver, dessa vez, não há qualquer menção a simplicidade para varrer o array. Esse código é capaz de operar com valores lazy mas, acaba não performando nada bem e não há nada que o JIT possa fazer, em tempo de execução, para fazer esse código ter performance comparável com o que tínhamos até então.
Outro problema é que esse código, por esperar uma interface, acaba forçando “boxing” do tipo sendo passado por parâmetro.
Mais uma vez, é importante destacar que essa “capacidade” do compilador é que permite que a mágica do yield return funcione.
foreach em Enumerações (que não são IEnumerables)
Algo que pouca gente se atenta é que o compilador permite que criemos enumerações sem implementar as interfaces IEnumerable e IEnumerator.
Vejamos um exemplo:
using System; class Program { static void Main(string[] args) { var f = new Foo(); foreach (var element in f) { Console.WriteLine(element); } } } struct Foo { public FooEnumerator GetEnumerator() { return new FooEnumerator(); } } struct FooEnumerator { private int _current; public bool MoveNext() { _current++; return _current < 10; } public int Current => _current; }
Esse código segue exatamente a mesma linha de implementação que seguiríamos caso estivéssemos implementando as interfaces IEnumerable e IEnumerator e são compatíveis com foreach.
.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2050 // Code size 40 (0x28) .maxstack 1 .entrypoint .locals init ( [0] valuetype Foo, [1] valuetype FooEnumerator ) // (no C# code) IL_0000: ldloca.s 0 // FooEnumerator enumerator = default(Foo).GetEnumerator(); IL_0002: initobj Foo IL_0008: ldloca.s 0 IL_000a: call instance valuetype FooEnumerator Foo::GetEnumerator() IL_000f: stloc.1 // (no C# code) IL_0010: br.s IL_001e // loop start (head: IL_001e) // Console.WriteLine(enumerator.Current); IL_0012: ldloca.s 1 IL_0014: call instance int32 FooEnumerator::get_Current() IL_0019: call void [System.Console]System.Console::WriteLine(int32) // while (enumerator.MoveNext()) IL_001e: ldloca.s 1 IL_0020: call instance bool FooEnumerator::MoveNext() IL_0025: brtrue.s IL_0012 // end loop IL_0027: ret }
Repare que, como utilizemos struct, tanto Foo quanto FooEnumerator não geram pressão no Garbage Collector.
Embora esse código seja “possível”, não é recomendado.
foreach em Enumerações (com Enumerators IDisposable)
O compilador é extremamente inteligente para identificar quando um Enumerator implementa a interface IDisposable e, consequentemente, chamar Dispose quando esta interface estiver presente. Aliás, ele faz isso sem fazer casting para não gerar boxing.
using System; class Program { static void Main(string[] args) { var f = new Foo(); foreach (var element in f) { Console.WriteLine(element); } } } struct Foo { public FooEnumerator GetEnumerator() { return new FooEnumerator(); } } struct FooEnumerator : IDisposable { private int _current; public bool MoveNext() { _current++; return _current < 10; } public int Current => _current; public void Dispose() { Console.WriteLine("Disposed!"); } }
Assim que o foreach concluir a execução do Enumerator irá acionar o seu Dispose automaticamente.
O código em Intermediate Language gerado pelo compilador chama o Dispose.
.method public hidebysig static void PrintAllChars ( string s ) cil managed { // Method begins at RVA 0x205c // Code size 32 (0x20) .maxstack 2 .locals init ( [0] string, [1] int32 ) // Console.WriteLine(s[i]); IL_0000: ldarg.0 // (no C# code) IL_0001: stloc.0 // for (int i = 0; i < s.Length; i++) IL_0002: ldc.i4.0 IL_0003: stloc.1 // (no C# code) IL_0004: br.s IL_0016 // loop start (head: IL_0016) IL_0006: ldloc.0 IL_0007: ldloc.1 IL_0008: callvirt instance char [System.Runtime]System.String::get_Chars(int32) IL_000d: call void [System.Console]System.Console::WriteLine(char) // for (int i = 0; i < s.Length; i++) IL_0012: ldloc.1 IL_0013: ldc.i4.1 IL_0014: add IL_0015: stloc.1 // (no C# code) IL_0016: ldloc.1 IL_0017: ldloc.0 IL_0018: callvirt instance int32 [System.Runtime]System.String::get_Length() IL_001d: blt.s IL_0006 // end loop IL_001f: ret }
foreach em Enumerações (com Enumerators, sem boxing)
Implementar enumeradores sem implementar as interfaces IEnumerable e IEnumerator parece errado. Na verdade, achamos que é! Por outro lado, gostaríamos de usar structs para não gerar carga no GC – como proceder?
É perfeitamente possível implementar código mais abstrato sem sobrecarregar o GC.
class Program { static void Main(string[] args) { var f = new Foo(); foreach (var element in f) { Console.WriteLine(element); } } } struct Foo : IEnumerable<int> { public IEnumerator<int> GetEnumerator() { return new FooEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return new FooEnumerator(); } } struct FooEnumerator : IEnumerator<int> { private int _current; public bool MoveNext() { _current++; return _current < 10; } public void Reset() { _current = 0; } object IEnumerator.Current => Current; public int Current => _current; public void Dispose() { Console.WriteLine("Disposed!"); } }
O compilador vai continuar respeitando o código, sem cast nem boxing, a menos que façamos uma atribuição da struct para uma variável tipada como IEnumerable.
static void Main(string[] args) { IEnumerable<int> f = new Foo(); foreach (var element in f) { Console.WriteLine(element); } }
O código acima, por exemplo, gera boxing .
Por enquanto … É isso
Os códigos que examinamos aqui mostram como os compiladores .NET tratam a instrução foreach. Vimos que o compilador evita gerar boxing e respeita características de cada tipo que estamos tentando iterar.
A forma mais cara de iterar com foreach é com enumerações. Porém, é a abstração mais poderosa.
Devemos evitar usar o “modelo” com enumerações para tipos que oferecem iterações mais simples (como arrays e strings).
Dúvidas? Algum cenário não discutido? Deixe suas observações nos comentários.