Access Violation

segunda-feira, outubro 31, 2005

Generics III - Constrains
Como prometi hoje vou falar um pouco sobre como utilizar o esquema de generics de uma forma mais eficaz.

Um dos problemas de templates em C++ é que, apesar do esforço do compilador, existem situações que o mesmo não detecta determinado tipo de erros, os quais, serão detectados apenas em tempo de execução.

Bom, para não ficar muito abstrato vamos apresentar um exemplo em C# mesmo (assim deixamos claro que podemos escrever código "não ideal" em qualquer linguagem):
0: using System;
1:
2: class TestConstrains<T>
3: {
4: public TestConstrains(T data)
5: {
6: this.data = data;
7: }
8:
9: public void TakeSomeBadAction(T other)
10: {
11: Test(data, other);
12: }
13:
14: private void Test(T o, T other)
15: {
16: IComparable comp = (IComparable) o;
17: Console.WriteLine(comp.CompareTo(other));
18: }
19:
20: private T data;
21: }
22:
23: class B
24: {
25: public B(int data)
26: {
27: this.data = data;
28: }
29:
30: private int data;
31: }
32:
33: public class TestConstrainsDriver
34: {
35: private static void Main()
36: {
37: TestConstrains<int> tc = new TestConstrains<int>(10);
38: tc.TakeSomeBadAction(10);
39:
40: TestConstrains<B> tcb = new TestConstrains<B>(new B(10));
41: tcb.TakeSomeBadAction(new B(10));
42: }
43: }

Neste exemplo a classe TestConstrains recebe em seu construtor um objeto do tipo T. Depois, no método TakeSomeBadAction() o mesmo executa o método Test().

Da forma como esta, o compilador ficará feliz em compilar este código, contudo em tempo de execução este programa gerará uma exceção do tipo InvalidCastException (você é capaz de dizer quais os pontos envolvidos neste problema sem compilar/executar o programa?).

O problema todo ocorre porque os compiladores não realizam análide de fluxo de código tão extensivamente para determinar o tipo de uma variável em todos os pontos de um programa (isto envolveria muito overhead).

Explico melhor: Neste programa ridiculamente pequeno o compilador poderia manter informações a respeito do tipo da variável utilizada para chamar o método TakeSomeBadAction() e assim verificar se este tipo suporta ou não a interface IComparable e emitir um erro em caso negativo.

Simples assim, correto? Talvez em um programa como este sim, mas não para a maioria dos programas "sérios"; confie em mim, você não iria querer esperar 3 horas para seu código (não tão extenso assim) compilar (talvez com a melhoria na performance dos computadores e a evolução da tecnologia, um dia teremos compiladores que analisam o fluxo do código mais agressivamente).

Para aliviar este problema, o framework permite que restrinjamos os possíveis tipos para um type argument. Para tanto utilizamos a sintaxe:

class C where (restrição aqui)
Basicamente o framework suporta 5 tipo de restrições (constrains): i) o tipo dever ser uma classe, ii) o tipo deve ser uma estrutura, iii) o tipo deve implementar (direta ou indiretamente) uma interface, iv) o tipo deve derivar (direta ou indiretamente) de uma classe, v) o tipo deve possuir um construtor (público) sem parâmetros. Não vou repetir aqui a explicação de como cada restrição funciona (vou usar o princípio DRY; para maiores informações consulte a documentação da Microsoft).

Para exemplificar melhor vamos utilizar o item iii (tipo deve implementar uma interface) para restringir T a tipos que implementem a interface IComparable:

 0: using System;
1:
2: class TestConstrains<T> where T : IComparable
3: {
4: public TestConstrains(T data)
5: {
6: this.data = data;
7: }
8:
9: public void TakeSomeBadAction(T other)
10: {
11: Test(data, other);
12: }
13:
14: private void Test(object o, object other)
15: {
16: IComparable comp = (IComparable) o;
17: Console.WriteLine(comp.CompareTo(other));
18: }
19:
20: private T data;
21: }
22:
23: class B
24: {
25: public B(int data)
26: {
27: this.data = data;
28: }
29:
30: private int data;
31: }
32:
33: class C : IComparable
34: {
35: public C(int data)
36: {
37: this.data = data;
38: }
39:
40: public int CompareTo(object o)
41: {
42: C other = o as C;
43: if (other != null)
44: {
45: return data - other.data;
46: }
47:
48: return -1;
49: }
50:
51: private int data;
52: }
53:
54: public class TestConstrainsDriver
55: {
56: private static void Main()
57: {
58: TestConstrains<int> tc = new TestConstrains<int>(10);
59: tc.TakeSomeBadAction(10); // Ok, Funciona corretamente...
60:
61: //TestConstrains<B> tcb = new TestConstrains<B>(new B(10));
62: //tcb.TakeSomeBadAction(new B(10)); //oopsss Classe 'B' não suporta a interface IComparable
63:
64: TestConstrains<C> tcc = new TestConstrains<C>(new C(10));
65: tcc.TakeSomeBadAction(new C(10));
66: }
67: }
Agora tente remover os comentários das linhas 61 e 62 e compilar! Perfeito, agora o compilador emite um erro nos avisando que o tipo B não implementa a interface IComparable!

Além de tornar nosso código mais seguro a utilização de restrições também ajudam a documentar o código.

Bom, espero ter acrescentado informações úteis a seu conhecimento :)

Adriano

[+/-] mostrar/esconder este post