Como o compilador entende o “foreach” em C#

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

Compartilhe este insight:

Comentários

Participe deixando seu comentário sobre este artigo a seguir:

Subscribe
Notify of
guest
0 Comentários
Inline Feedbacks
View all comments

AUTOR

Elemar Júnior
Fundador e CEO da EximiaCo atua como tech trusted advisor ajudando empresas e profissionais a gerar mais resultados através da tecnologia.

NOVOS HORIZONTES PARA O SEU NEGÓCIO

Nosso time está preparado para superar junto com você grandes desafios tecnológicos.

Entre em contato e vamos juntos utilizar a tecnologia do jeito certo para gerar mais resultados.

Insights EximiaCo

Confira os conteúdos de negócios e tecnologia desenvolvidos pelos nossos consultores:

Arquivo

Pós-pandemia, trabalho remoto e a retenção dos profissionais de TI

CTO Consulting e Especialista em Execução em TI
0
Queremos saber a sua opinião, deixe seu comentáriox
Oferta de pré-venda!

Mentoria em
Arquitetura de Software

Práticas, padrões & técnicas para Arquitetura de Software, de maneira efetiva, com base em cenários reais para profissionais envolvidos no projeto e implantação de software.

Muito obrigado!

Deu tudo certo com seu envio!
Logo entraremos em contato

Como o compilador entende o “foreach” em C#

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Como o compilador entende o “foreach” em C#

Para se candidatar nesta turma aberta, preencha o formulário a seguir:

Condição especial de pré-venda: R$ 14.000,00 - contratando a mentoria até até 31/01/2023 e R$ 15.000,00 - contratando a mentoria a partir de 01/02/2023, em até 12x com taxas.

Tenho interesse nessa capacitação

Para solicitar mais informações sobre essa capacitação para a sua empresa, preencha o formulário a seguir:

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

O seu insight foi excluído com sucesso!

O seu insight foi excluído e não está mais disponível.

O seu insight foi salvo com sucesso!

Ele está na fila de espera, aguardando ser revisado para ter sua publicação programada.

Tenho interesse em conversar

Se você está querendo gerar resultados através da tecnologia, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse nessa solução

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

Tenho interesse neste serviço

Se você está procurando este tipo de solução para o seu negócio, preencha este formulário que um de nossos consultores entrará em contato com você:

× Precisa de ajuda?