IL 101–Parte 8

Publicado em 19/08/2010

2


Recapitulando

Essa série tem por objetivo os apresentar fundamentos da programação em Intermediate Language. O que já vimos até aqui foi:

Sobre essa parte

Agora que já sabemos como criar tipos com atributos e propriedades (parte 7), vamos avançar. Nessa parte, irei demonstrar como a CLR trata a inicialização de objetos, com ênfase nos métodos construtores. Para tornar as coisas mais interessantes, vou continuar o exemplo que iniciei na parte anterior. Adicionaremos dois construtores: um sem parâmetros (default) que inicia a propriedade ChildCount para 1; e outro que recebe um unsigned int8 definindo o valor inicial dessa propriedade.

Por enquanto vamos focar em construtores de instância. Não tratarei, agora, de construtores estáticos.

Construtores

Podemos fazer algumas observações comuns ao funcionamento de construtores, sejam em value types como reference types:

  • Construtores de instância são métodos que sempre tem o nome .ctor e retornam void (por curiosidade, construtores estáticos são nomeados como .cctor);
  • São sempre decorados com dois modificadores – specialname e rtspecialname;
  • Em Intermediate Language, construtores funcionam como métodos normais que podem ser chamados a qualquer momento (Lembrando: isso não é uma prática muito bonita);
  • Podem ser definidos tantas sobrecargas para esse método quanto necessário;
  • Para reference types, o construtor sempre deverá chamar o construtor da classe base. Não fazer isso cria o risco de que algum atributo não seja devidamente inicializado. Não há qualquer exigência sobre o momento que essa evocação deve ocorrer, mas costuma ser considerado um bom padrão fazer isso em primeiro lugar;
  • Para value types, não é necessário evocar o construtor da base, mesmo porque não há atributos por inicializar;
  • Não é possível definir um construtor como virtual;
  • Construtores para refence types são sempre evocados por default. Por outro lado, construtores de value types nunca são evocados (apenas se acionados diretamente);
  • Linguagens de alto nível impedem a associação de um tipo de retorno para o construtor. Em Intermediate Language construtores precisam ser declaros explicitamente como retornando void;
  • Algumas linguagens de alto nível, como C#, não permitem a definição de construtores sem parâmetros para value types. Em Intermediate Language não há restrição sintática para isso;
  • Compiladores de linguagens de alto nível inserem código IL nos construtores de reference types para chamar o construtor da classe base; IL não faz isso.

Criando um construtor default

Chega de teoria, agora vamos para o código Smiley piscando

Que fique claro: construir construtores sem argumentos em value types não é uma boa prática. Entretanto, nesse primeiro exemplo, por simplicidade, será exatamente isso que faremos. Observe o código que segue:

.assembly Family2
{
	.ver 1:0:1:0
}

.module Family2.dll

.namespace ElemarJR.Blog
{
	.class public ansi auto sealed FamilySample
		extends [mscorlib]System.ValueType
	{
		.field private unsigned int8 _childCount
		

		.method public specialname rtspecialname instance void .ctor()
		{
			ldarg.0
			ldc.i4.s 1
			stfld unsigned int8 
				ElemarJR.Blog.FamilySample::_childCount
			ret
		}

		
		.method specialname public instance unsigned int8 get_ChildCount()
			cil managed
		{
			ldarg.0
			ldfld unsigned int8 ElemarJR.Blog.FamilySample::_childCount
			ret
		}
		
		.method specialname public instance void set_ChildCount(unsigned int8)
			cil managed
		{
			ldarg.0
			ldarg.1
			stfld unsigned int8 ElemarJR.Blog.FamilySample::_childCount
			ret
		}
		
		.property instance unsigned int8 ChildCount()
		{
			.get instance unsigned int8 ElemarJR.Blog.FamilySample::get_ChildCount()
			.set instance void ElemarJR.Blog.FamilySample::set_ChildCount(unsigned int8)
		}
	}
}

O trecho em negrito mostra o construtor. Simples, não? Repare que é um método normal que inicializa o atributo. (Se desejar, salve esse código em um arquivo chamado Family2.il e compile-o como DLL)

Agora, um exemplo de código cliente, em IL para esse value type:

.assembly extern Family2
{
	.ver 1:0:1:0
}

.assembly TestFamily2
{
	.ver 1:0:1:0
}

.module TestFamily2.exe

.namespace ElemarJR.Blog
{
	.class EntryPoint 
		extends [mscorlib]System.Object
	{
		.method static void Main() cil managed
		{
			.maxstack 2
			.locals init (
				valuetype [Family2]ElemarJR.Blog.FamilySample
			)
			.entrypoint
			

			ldloca.s 0
			call instance void [Family2]ElemarJR.Blog.FamilySample::
							.ctor()

			
			ldstr "ChildCount is "
			call void [mscorlib]System.Console::Write(string)
			
			ldloca.s 0
			call instance unsigned int8 [Family2]
				ElemarJR.Blog.FamilySample::get_ChildCount()
			call void [mscorlib]System.Console::WriteLine(int32)
			
			ret
			
		}
	}
}

O trecho de código em negrito chama o construtor do value type explicitamente. (Se desejar, salve esse código em um arquivo chamado TestFamily2.il e compile-o como executável)

Adicionando um construtor que aceita parâmetros

Agora que sabemos como escrever construtores sem parâmetros, vamos escrever um que aceita um parâmetro determinando o valor inicial de um atributo. Considere o exemplo anterior, adicionando o seguinte método:

		.method public specialname rtspecialname instance void .ctor
				(unsigned int8)
		{
			ldarg.0
			ldarg.1
			stfld unsigned int8 
				ElemarJR.Blog.FamilySample::_childCount
			ret
		}

Adicionando este código,  temos todo o necessário para termos dois construtores. Entretanto, observe que os dois inicializam, independentemente um atributo. Uma boa prática seria fazer um construtor chamar o outro. Assim, vamos modificar o primeiro construtor:

		
		.method public specialname rtspecialname instance void .ctor()
		{
			ldarg.0
			ldc.i4.s 1

			stfld unsigned int8 
				ElemarJR.Blog.FamilySample::_childCount


			
			call instance void ElemarJR.Blog.FamilySample::
				.ctor(unsigned int8)

			
			ret
		}

O trecho em negrito destaca a chamada do outro construtor.  O trecho riscado deverá ser removido do código. (Se está testando, recompile tudo).

Trabalhando com Reference Objects

Agora que já conseguimos instanciar e inicializar value types  é hora de aprendermos a lidar com Reference types.  Em Intermediate Language, fazemos isso derivando a classe de System.Object. Quanto ao construtor, passamos a ter a responsabilidade de chamar o construtor do tipo base. Nossa implementação agora fica mais ou menos assim:

.assembly Family2
{
	.ver 1:0:1:0
}

.module Family2.dll

.namespace ElemarJR.Blog
{

	.class public ansi auto sealed FamilySample
		extends [mscorlib]System.Object

	{
		.field private unsigned int8 _childCount
		
		.method public specialname rtspecialname instance void .ctor()
		{
			ldarg.0
			ldc.i4.s 1
			
			call instance void ElemarJR.Blog.FamilySample::
				.ctor(unsigned int8)
			ret
		}

		.method public specialname rtspecialname instance void .ctor
				(unsigned int8)
		{

			ldarg.0
			call instance void [mscorlib]System.Object::.ctor()

			ldarg.0
			ldarg.1
			stfld unsigned int8 
				ElemarJR.Blog.FamilySample::_childCount
			ret
		}
		
		.method specialname public instance unsigned int8 get_ChildCount()
			cil managed
		{
			ldarg.0
			ldfld unsigned int8 ElemarJR.Blog.FamilySample::_childCount
			ret
		}
		
		.method specialname public instance void set_ChildCount(unsigned int8)
			cil managed
		{
			ldarg.0
			ldarg.1
			stfld unsigned int8 ElemarJR.Blog.FamilySample::_childCount
			ret
		}
		
		.property instance unsigned int8 ChildCount()
		{
			.get instance unsigned int8 ElemarJR.Blog.FamilySample::get_ChildCount()
			.set instance void ElemarJR.Blog.FamilySample::set_ChildCount(unsigned int8)
		}
	}
}

Os trechos em negrito destacam as mudanças: a classe herda de System.Object e os construtores chamam, direta ou indiretamente, o construtor da classe base.

Para nossos exemplos continuarem funcinando, teremos que alterar também o código cliente:

.assembly extern Family2
{
	.ver 1:0:1:0
}

.assembly TestFamily2
{
	.ver 1:0:1:0
}

.module TestFamily2.exe

.namespace ElemarJR.Blog
{
	.class EntryPoint 
		extends [mscorlib]System.Object
	{
		.method static void Main() cil managed
		{
			.maxstack 2

			.locals init (
				class [Family2]ElemarJR.Blog.FamilySample

			)
			.entrypoint
			

			newobj void [Family2]ElemarJR.Blog.FamilySample::.ctor()
			stloc.0

			
			ldstr "ChildCount is "
			call void [mscorlib]System.Console::Write(string)
			

			ldloc.s 0

			call instance unsigned int8 [Family2]
				ElemarJR.Blog.FamilySample::get_ChildCount()
			call void [mscorlib]System.Console::WriteLine(int32)
			
			ret
			
		}
	}
}

Os trechos em negrito indicam os pontos que foram alterados. Primeiro, observe que o modificador class foi utilizado no lugar de valuetype. Depois, usamos a instrução newobj para criar uma instância do tipo especificado. Por fim, voltamos usar as instruções normais para armazenar e recuperar valores dos atributos locais.

Conclusões

Hoje mostrei os procedimentos para a criação de construtores. Como visto, o compilador do C# simplifica bastante o trabalho com construtores, seja impedindo a definição de construtores sem parâmetros para value type, seja pela inclusão automática de código de chamada para o construtor da classe base.

Bem, por hoje é isso!

Publicado em: Sem categoria