Este é um assunto já bastante discutido pela internet fora. Todos sabemos o que são Generics, e para que são usados, certo? Não. Existe uma variedade de razões pelas quais o leitor pode não saber o que são Generics, e algumas delas são:
- O leitor é novo na linguagem,
- O leitor não é novo na linguagem, mas nunca se deu ao trabalho de tentar aprender.
Se o leitor nunca aprendeu generics, isso não significa que nunca terá de aprender. Se reconhece este snippet, então saiba que já estamos a lidar com generics.
Portanto, qual é a vantagem de utilizar generics?
Type safety
Generics permite-nos apanhar erros na compilação em vez de em runtime, obrigando o programador a respeitar as regras de type safety que fazem do C# uma linguagem estática. Isto leva-nos a uma produtividade acelerada porque não temos que correr o programa para saber que cometemos um erro. Por exemplo, se no snippet acima tentássemos adicionar um novo item do tipo string, o compilador daria erro.
Tipicamente, no C# 1.0, podiamos usar uma colecção ArrayList para conter uma lista de itens, independentemente do tipo desses itens. Penso que nunca precisei de mais do que um tipo na mesma colecção, mas uma ArrayList dá-nos essa liberdade. Ao dizermos o tipo que queremos na nossa List, o compilador vai criá-la internamente e vai reconhecer que naquela instância apenas podemos ter int's. Por exemplo:
O mesmo aconteceria para o Add, Remove e todos os outros métodos que utilizam generics da List. Mas já iremos ver mais sobre isso.
Performance
Performance é uma das vantagens mais importantes ao utilizar generics na situação acima descrita. Quando adicionamos um item a uma ArrayList, estamos a fazer uma operação de boxing (o que significa que estamos a criar um objecto especial e a meter o nosso valor lá dentro), e quando vamos buscar o valor temos que fazer uma conversão para o nosso tipo específico e fazer uma operação de unboxing. Estas operações podem tornar-se pesadas quando as fazemos múltiplas vezes, tal como atravessar a colecção inteira.
Ao dizermos à colecção qual o tipo com que está a lidar, evitamos esse inconveniente de ter que converter o nosso valor para o utilizar, e isso irá dar-nos um ganho de performance. Claro que a diferença apenas se torna visível quando estamos a lidar com colecções grandes, mas está lá.
Reutilização do código
Penso que esta é a maior vantagem de utilizarmos generics como ferramenta. Quando lidamos com colecções de itens no C# 1.0, se quisermos ter os benefícios acima explicados teríamos de criar a nossa própria colecção. Imaginem todo o trabalho em criar manualmente uma colecção para a classe Pessoa, Barco, Carro, etc. Ao invés disso, podemos utilizar generics para esta tarefa. List(of T) faz exactamente isso, portanto vamos ver a sua declaração.
A única coisa estranha aqui é o T que vem após o nome da classe. Isto é o que nos permite passar o type com que estamos a lidar e chama-se um type parameter. Dentro da nossa classe genérica, utilizamos T (ou qualquer nome que lhe queiramos dar) para nos referirmos ao tipo que o código cliente nos forneceu por parâmetro. Quando criamos uma List(of T), o compilador vai criar uma classe da mesma forma que nós faríamos manualmente. Isto significa que podemos reutilizar o nosso código muito mais facilmente.
Vamos explorar a capacidade dos generics criando uma colecção nossa:
Como podemos ver, este não é um conceito difícil de aprender. Criamos uma nova classe MinhaLista que vai receber um type parameter chamado T. Podemos ver que temos uma propriedade chamada UltimoItem do tipo T a representar o último item adicionado à nossa colecção. Temos também um método que recebe um T e o devolve após adicionar à nossa colecção interna e disparar o evento ItemAdicionado. Este evento também utiliza o type argument T, e um delegate genérico incluído no .NET que nos permite especificar de forma genérica o tipo do seu segundo parâmetro.
Um dado importante na nossa implementação é que nós estamos a utilizar generic constraints. Estas são representadas pelo where T : Pessoa que podemos ver na declaração da classe. Quando utilizamos constraints, o código cliente vai apenas conseguir passar um tipo que implemente as interfaces ou seja herdeiro das classes especificadas nas constraints. Se Pessoa for uma classe então conseguiremos apenas passar o tipo Pessoa ou um tipo mais derivado. Dentro da nossa classe genérica passamos então a ter acesso aos membros na classe Pessoa, porque temos a certeza que estamos a lidar com esta classe. São elegíveis para generic constraint os seguintes:
Podemos ver que utilizamos a nossa colecção combinada com o tipo Pessoa. Após especificarmos o tipo, o compilador tem toda a informação para criar a classe e analisar as chamadas. O handler do evento (Pessoa_Adicionada) é chamado com o segundo parâmetro do tipo Pessoa, como foi especificado na declaração do evento. O método AdicionarEDevolverOMesmo irá receber e devolver uma Pessoa.
Também podemos ter um método genérico numa classe não genérica da seguinte forma:
Reparem que estamos a especificar um type parameter quando declaramos o método. Ao chamar o método não é necessário incluir o type parameteri, porque o compilador consegue inferi-lo através do argumento do método. Temos também a opção de especificar constraints para estes parâmetros, tal como na declaração de uma classe.
Quando usar
Um novo namespace (System.Collections.Generic) foi adicionado no C# 2.0 especialmente para as colecções genéricas. List(of T) é uma das mais usadas, mas também temos o Dictionary(of TKey, of TValue) que podemos usar praticamente da mesma forma que uma Hashtable (mas com os benefícios acima descritos acima) e também toda a estrutura base que suporta estas e outras colecções genéricas.
Todo o conceito de generics é mais direccionado para colecções no geral, mas isso não é obrigatório. Podemos utilizá-los obviamente onde quisermos, incluindo classes, interfaces, métodos ou mesmo delegates. Utilizar generics vai trazer mais flexibilidade ao nosso código, mas pode também aumentar o grau de dificuldade em compreendê-lo se abusarmos. Ainda assim, existem situações onde já os apliquei com bastante sucesso. O exemplo a que me refiro era um User Control que, quando clicado, criava um Form de popup. Utilizar generics permitiu-me reutilizar o código apenas especificando algumas constraints. Desta forma, o User Control nunca teria que saber o tipo do Form, mas podia instanciá-lo na mesma. Apliquei aqui um pouco o conceito do strategy design pattern e funcionou muito bem.
A palavra reservada default
A palavra default tem um objectivo muito bem definido no código genérico. Recebe um parâmetro que é o nome de um tipo e devolve o valor por defeito desse mesmo tipo. Portanto, se passarmos uma classe, irá retornar null, se passarmos uma struct irá retornar uma struct com todos os campos inicializados para o seu valor por defeito, e por aí em diante... Vejamos o snippet:
O meu conselho é: aprendam bem esta feature do C# 2.0, porque eventualmente vai dar frutos!
Obrigado.
Type safety
Generics permite-nos apanhar erros na compilação em vez de em runtime, obrigando o programador a respeitar as regras de type safety que fazem do C# uma linguagem estática. Isto leva-nos a uma produtividade acelerada porque não temos que correr o programa para saber que cometemos um erro. Por exemplo, se no snippet acima tentássemos adicionar um novo item do tipo string, o compilador daria erro.
Tipicamente, no C# 1.0, podiamos usar uma colecção ArrayList para conter uma lista de itens, independentemente do tipo desses itens. Penso que nunca precisei de mais do que um tipo na mesma colecção, mas uma ArrayList dá-nos essa liberdade. Ao dizermos o tipo que queremos na nossa List, o compilador vai criá-la internamente e vai reconhecer que naquela instância apenas podemos ter int's. Por exemplo:
O mesmo aconteceria para o Add, Remove e todos os outros métodos que utilizam generics da List. Mas já iremos ver mais sobre isso.
Performance
Performance é uma das vantagens mais importantes ao utilizar generics na situação acima descrita. Quando adicionamos um item a uma ArrayList, estamos a fazer uma operação de boxing (o que significa que estamos a criar um objecto especial e a meter o nosso valor lá dentro), e quando vamos buscar o valor temos que fazer uma conversão para o nosso tipo específico e fazer uma operação de unboxing. Estas operações podem tornar-se pesadas quando as fazemos múltiplas vezes, tal como atravessar a colecção inteira.
Ao dizermos à colecção qual o tipo com que está a lidar, evitamos esse inconveniente de ter que converter o nosso valor para o utilizar, e isso irá dar-nos um ganho de performance. Claro que a diferença apenas se torna visível quando estamos a lidar com colecções grandes, mas está lá.
Reutilização do código
Penso que esta é a maior vantagem de utilizarmos generics como ferramenta. Quando lidamos com colecções de itens no C# 1.0, se quisermos ter os benefícios acima explicados teríamos de criar a nossa própria colecção. Imaginem todo o trabalho em criar manualmente uma colecção para a classe Pessoa, Barco, Carro, etc. Ao invés disso, podemos utilizar generics para esta tarefa. List(of T) faz exactamente isso, portanto vamos ver a sua declaração.
A única coisa estranha aqui é o T que vem após o nome da classe. Isto é o que nos permite passar o type com que estamos a lidar e chama-se um type parameter. Dentro da nossa classe genérica, utilizamos T (ou qualquer nome que lhe queiramos dar) para nos referirmos ao tipo que o código cliente nos forneceu por parâmetro. Quando criamos uma List(of T), o compilador vai criar uma classe da mesma forma que nós faríamos manualmente. Isto significa que podemos reutilizar o nosso código muito mais facilmente.
Vamos explorar a capacidade dos generics criando uma colecção nossa:
Como podemos ver, este não é um conceito difícil de aprender. Criamos uma nova classe MinhaLista que vai receber um type parameter chamado T. Podemos ver que temos uma propriedade chamada UltimoItem do tipo T a representar o último item adicionado à nossa colecção. Temos também um método que recebe um T e o devolve após adicionar à nossa colecção interna e disparar o evento ItemAdicionado. Este evento também utiliza o type argument T, e um delegate genérico incluído no .NET que nos permite especificar de forma genérica o tipo do seu segundo parâmetro.
Um dado importante na nossa implementação é que nós estamos a utilizar generic constraints. Estas são representadas pelo where T : Pessoa que podemos ver na declaração da classe. Quando utilizamos constraints, o código cliente vai apenas conseguir passar um tipo que implemente as interfaces ou seja herdeiro das classes especificadas nas constraints. Se Pessoa for uma classe então conseguiremos apenas passar o tipo Pessoa ou um tipo mais derivado. Dentro da nossa classe genérica passamos então a ter acesso aos membros na classe Pessoa, porque temos a certeza que estamos a lidar com esta classe. São elegíveis para generic constraint os seguintes:
- Qualquer classe
- uma interface
- As palavras reservadas class e struct
- outro type parameter
- new() Isto permite-nos especificar que o tipo tem que ter um construtor public e sem parâmetros.
Podemos ver que utilizamos a nossa colecção combinada com o tipo Pessoa. Após especificarmos o tipo, o compilador tem toda a informação para criar a classe e analisar as chamadas. O handler do evento (Pessoa_Adicionada) é chamado com o segundo parâmetro do tipo Pessoa, como foi especificado na declaração do evento. O método AdicionarEDevolverOMesmo irá receber e devolver uma Pessoa.
Também podemos ter um método genérico numa classe não genérica da seguinte forma:
Reparem que estamos a especificar um type parameter quando declaramos o método. Ao chamar o método não é necessário incluir o type parameteri, porque o compilador consegue inferi-lo através do argumento do método. Temos também a opção de especificar constraints para estes parâmetros, tal como na declaração de uma classe.
Quando usar
Um novo namespace (System.Collections.Generic) foi adicionado no C# 2.0 especialmente para as colecções genéricas. List(of T) é uma das mais usadas, mas também temos o Dictionary(of TKey, of TValue) que podemos usar praticamente da mesma forma que uma Hashtable (mas com os benefícios acima descritos acima) e também toda a estrutura base que suporta estas e outras colecções genéricas.
Todo o conceito de generics é mais direccionado para colecções no geral, mas isso não é obrigatório. Podemos utilizá-los obviamente onde quisermos, incluindo classes, interfaces, métodos ou mesmo delegates. Utilizar generics vai trazer mais flexibilidade ao nosso código, mas pode também aumentar o grau de dificuldade em compreendê-lo se abusarmos. Ainda assim, existem situações onde já os apliquei com bastante sucesso. O exemplo a que me refiro era um User Control que, quando clicado, criava um Form de popup. Utilizar generics permitiu-me reutilizar o código apenas especificando algumas constraints. Desta forma, o User Control nunca teria que saber o tipo do Form, mas podia instanciá-lo na mesma. Apliquei aqui um pouco o conceito do strategy design pattern e funcionou muito bem.
A palavra reservada default
A palavra default tem um objectivo muito bem definido no código genérico. Recebe um parâmetro que é o nome de um tipo e devolve o valor por defeito desse mesmo tipo. Portanto, se passarmos uma classe, irá retornar null, se passarmos uma struct irá retornar uma struct com todos os campos inicializados para o seu valor por defeito, e por aí em diante... Vejamos o snippet:
O meu conselho é: aprendam bem esta feature do C# 2.0, porque eventualmente vai dar frutos!
Obrigado.
Comments
Post a Comment