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.