Skip to main content

C# 2.0 - Generics

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:
  • 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.
Esta é a forma como iríamos utilizar a classe MinhaLista:

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

Popular posts from this blog

The repository's repository

Ever since I started delving into architecture,  and specifically service oriented architecture, there has been one matter where opinions get divided. Let me state the problem first, and then take a look at both sides of the barricade. Given that your service layer needs to access persistent storage, how do you model that layer? It is almost common knowledge what to do here: use the Repository design pattern. So we look at the pattern and decide that it seems simple enough! Let's implement the shit out of it! Now, let's say that you will use an ORM - here comes trouble. Specifically we're using EF, but we could be talking about NHibernate or really any other. The real divisive theme is this question: should you be using the repository pattern at all when you use an ORM? I'll flat out say it: I don't think you should... except with good reason. So, sharpen your swords, pray to your gods and come with me to fight this war... or maybe stay in the couch? ...

Follow up: improving the Result type from feedback

This post is a follow up on the previous post. It presents an approach on how to return values from a method. I got some great feedback both good and bad from other people, and with that I will present now the updated code taking that feedback into account. Here is the original: And the modified version: Following is some of the most important feedback which led to this. Make it an immutable struct This was a useful one. I can't say that I have ever found a problem with having the Result type as a class, but that is just a matter of scale. The point of this is that now we avoid allocating memory in high usage scenarios. This was a problem of scale, easily solvable. Return a tuple instead of using a dedicated Result type The initial implementation comes from a long time ago, when C# did not have (good) support for tuples and deconstruction wasn't heard of. You would have to deal with the Tuple type, which was a bit of a hassle. I feel it would complicate the ...

C# 2.0 - Partial Types

For those of you interested, i found a very interesting list of features that were introduced in C# in  here . This is a very complete list that contains all the features, and i'm explaining them one by one in this post series. We've talked about  Generics  and  Iterators . Now it's time for some partial types . A partial type  is a type which definition is spread across one or more files. It doesn't have to be in multiple separated files, but can be. This is a very simple concept that can give us many benefits, let's see: If a type is partial, multiple developers can work on every part of it. This allows a more organized way of working and can lead to production improvement.  Winforms , for example, generates a partial class for the form so that the client can separately edit other parts it. This way, a part contains information about the design and the other contains the logic of the form. In fact, this is a very spread pattern across .Net. Ent...