Desenvolvimento - C#

Novidades na Linguagem C#

Este artigo mostra todas as novidades do C# em seqüência de uma maneira didática e com exemplos.

por Mauro Sant'Anna



Introdução

O Microsoft Framework 2.0 trouxe diversas novidades nas linguagens de programação. A principal delas foi “Generics”, uma maneira escrever código onde os tipos são parametrizados. Isto vai além de funções normais, onde os valores é que são parametrizados. Os Generics permitem diversos novos recursos, como os tipos por valor (inteiros, por exemplo) que podem receber valor “null” e também coleções fortemente tipadas e bastante eficientes.

O suporte a Generics implicou em mudanças fortes nas camadas de mais baixo nível do .NET Framework, como novas instruções da linguagem IL e também a capacidade de gerar código em tempo de execução em alguns casos.

A nova versão do .NET Framework presente no Visual Studio 2008 traz evidentemente novas bibliotecas de classes com novas funcionalidades, como interface com usuário (WPF - Windows Presentation Foundation), comunicação (WCF – Windows Communication Foundation) e muitas outras novidades, algumas sem nomes grandiosos. No entanto, desta vez não houve grandes modificações nas camadas de baixo nível. Ou seja, o código IL é o mesmo da versão 2.0. Na verdade, as versões 3.0 e 3.5 do .NET Framework são essencialmente extensões do Framework 2.0 e não o substituem completamente. Seria então de esperar que não houvesse muitas novidades na linguagem, certo? Errado!

Na verdade, existem grandes modificações nas principais linguagens, C# e VB. E o interessante é que essas modificações existem apenas em nível do compilador das linguagens em si, quando os assemblies (DLL e EXE) são gerados, as instruções usadas são as mesmas que as antigas. Isto é importante, pois significa que as modificações são na verdade “truques” de compilação e muitas vezes restritas à própria classe ou mesmo apenas à própria função.

Inferência de tipos de variáveis

Considere o exemplo abaixo onde estamos declarando e inicializando uma variável na mesma linha de código:

int y = 20;

Somos obrigados a declarar o tipo, muito embora o compilador “saiba” que a variável deve ser inteira, pelo lado direito da expressão, que é um número inteiro. Efetivamente, salvo por algumas conversões automáticas, por exemplo de inteiro para ponto flutuante, se declararmos o tipo errado teremos um erro de compilação. Por exemplo:

string w = 10;

DateTime F = 10;

Ora, será que o compilador não poderia “saber” o tipo da variável automaticamente pelo tipo da expressão que está sendo atribuída a ela? Isso é exatamente o que a inferência de tipos de variáveis faz. Agora podemos escrever a expressão seguinte:

var x = 10;

 Como estamos atribuindo um inteiro, o compilador sabe que a expressão “x” é do tipo inteiro. A expressão acima gera exatamente o mesmo código que a expressão a seguir:

int x = 10;

Uma vez compilado não dá para saber a diferença. Note que isto é muito diferente de declarar uma variável do tipo “object”. Com “object”, o tipo é decidido em tempo de execução e temos alguma perda de eficiência. Já com “var”, o tipo é conhecido em tempo de compilação e não existe nenhuma perda de eficiência em relação a declarar um inteiro ou outro tipo especificamente.

Um detalhe: este recurso só está disponível para variáveis locais, ou seja, as declaradas dentro de um método. Você não pode usá-lo em variáveis de classe.

Object Initializers

Suponha que você tem uma classe com uma série de propriedades públicas que você queira inicializar, por exemplo:

class Pessoa

{

    public Pessoa()

    {

        FNome = string.Empty;

        FSobrenome = string.Empty;

    }

    string FNome;

    public string Nome

    {

        get { return FNome; }

        set { FNome = value; }

    }

    string FSobrenome;

    public string Sobrenome

    {

        get { return FSobrenome; }

        set { FSobrenome = value; }

    }

}

Este seriao código de inicialização:

var P0 = new Pessoa();

P0.Nome = "Mauro";

P0.Sobrenome = "Sant"Anna";

É claro que podemos declarar um construtor que aceite um nome e um sobrenome como argumentos:

public Pessoa(string Nome, string Sobrenome)

{

    this.FNome = Nome;

    this.FSobrenome = Sobrenome;

}

E chamar assim:

var P1 = new Pessoa("Mauro", "Sant"Anna");

Mas ainda assim dá um certo trabalho. O C# 3.0 permite a sintaxe abaixo sem ter que criar o construtor específico:

var P1 = new Pessoa { Nome = "Mauro", Sobrenome = "Sant"Anna" };

O código acima gera exatamente o mesmo que:

var P0 = new Pessoa();

P0.Nome = "Mauro";

P0.Sobrenome = "Sant"Anna";

Collection Initializers

Os arrays do C# permitem uma inicialização bastante fácil, como por exemplo:

char [] Vogais3 = { "A", "E", "I", "O", "U" };

Já se tivermos uma coleção, temos que iniciar os elementos um por um, como no exemplo abaixo:

List<char> Vogais1 = new List<char>();

Vogais1.Add("A");

Vogais1.Add("E");

Vogais1.Add("I");

Vogais1.Add("O");

Vogais1.Add("U");

Seria interessante se pudessemos inicializar as coleções da mesma maneira que os arrays. Bem, isso agora é possível com a sintaxe seguinte:

var Vogais2 = new List<char> { "A", "E", "I", "O", "U" };

O código acima gera exatamente o mesmo código que o anterior, mas é mais conveniente de escrever.

Autogenerated Properties

É comum criar classes nas quais armazenamos dados em variáveis e colocamos propriedades de acesso, como na classe Pessoa do exemplo anterior. Isto significa que escrevemos um monte de código “braçal”, além de ter que inventar um nome para a variável diferente da propriedade - eu costumo preceder com a letra “F”, como no exemplo acima.

O novo recurso de “propriedades autogeradas” permite que declaremos apenas a propriedade de uma maneira bastante simplificada e a variável de instância é declarada automaticamente, como no exemplo abaixo:

public class Pessoa {

    public string Nome {get; private set; }

    public string Sobrenome { get; private set; }

    public Pessoa(string Nome, string Sobrenome)

    {

        this.Nome = Nome;

        this.Sobrenome = Sobrenome;

    }

}

Note que não é mais possível acessar diretamente a variável; isto deve ser feito obrigatoriamente através da propriedade. No exemplo acima, a atribuição é “private” exatamente para evitar que os dados possam ser atualizados diretamente.

Veja a classe gerada com a ferramenta ILDASM:

Observe as variáveis criadas automaticamente.

Anonymous Types

Até a versão 2.0 do Framework, todos os tipos precisavam ser explicitamente declarados antes de utilizados. Agora, o compilador pode declara automaticamente alguns tipos para nós.

Considere a classe a seguir

class Pessoa {

    public Pessoa(string Nome, string Sobrenome)

    {

        this.FNome = Nome;

        this.FSobrenome = Sobrenome;

    }

    string FNome;

    public string Nome

    {

        get { return FNome; }

    }

    string FSobrenome;

    public string Sobrenome

    {

        get { return FSobrenome; }

    }

    public override int GetHashCode()

    {

        return Nome.GetHashCode() ^ Sobrenome.GetHashCode();

    }

    public override bool Equals(object obj)

    {

        Pessoa P = (Pessoa)obj;

        return ((P.Nome == Nome) && (P.Sobrenome == Sobrenome));

    }

    public override string ToString()

    {

        return string.Format("Nome: {0} Sobrenome: {1}", Nome, Sobrenome);

    }

}

Esta classe armazena dados de uma pessoa, mas especificamente nome e sobrenome. Ela contém campos para armazenar os dados, um construtor para inicializar e propriedades de leitura. Além disso, sobrecarrega três métodos: ToString(), Equals() e GetHashCode(). Estes três últimos não são particularmente importantes, eles estão aí mais para deixar a classe completa.

Podemos utilizar a classe da seguinte forma:

var P1 = new Pessoa("Mauro", "Sant"Anna");

Console.WriteLine(string.Format("{0} {1} {2}", P1.GetType().ToString(), P1.Nome, P1.Sobrenome));

Pode ser que desejemos deixar os tipos de Nome e Sobrenome como sendo genéricos, assim podemos definir seus tipos exatos em tempo de execução. Neste caso, a classe ficaria assim:

class GenericPessoa<T1, T2>

{

    public GenericPessoa(T1 Nome, T2 Sobrenome)

    {

        this.FNome = Nome;

        this.FSobrenome = Sobrenome;

    }

    T1 FNome;

    public T1 Nome

    {

        get { return FNome; }

    }

    T2 FSobrenome;

    public T2 Sobrenome

    {

        get { return FSobrenome; }

    }

    public override int GetHashCode()

    {

        return Nome.GetHashCode() ^ Sobrenome.GetHashCode();

    }

    public override bool Equals(object obj)

    {

        GenericPessoa<T1, T2> P = (GenericPessoa<T1, T2>)obj;

        return ((P.Nome.Equals(Nome)) && (P.Sobrenome.Equals(Sobrenome)));

    }

    public override string ToString()

    {

        return string.Format("Nome: {0} Sobrenome: {1}", Nome, Sobrenome);

    }

}

Poderíamos usar desta forma:

var P2 = new GenericPessoa<string, string>("Mauro", "Sant"Anna");

Console.WriteLine(string.Format("{0} {1} {2}", P2.GetType().ToString(), P2.Nome, P2.Sobrenome));

Não existe nada de errado com o código acima. A questão é que este tipo de classes para conter valores é bastante comum e escrevê-las demanda certo trabalho braçal.

O compilador C# 3.0 pode agora declarar classes como a acima automaticamente apenas ao se declarar e inicializar uma variável da seguinte forma:

 

var P3 = new { Nome = "Mauro", Sobrenome = "Sant"Anna" };

O compilador irá criar uma classe real, visível com ferramentas como debugger e decompiladores. Esta classe é virtualmente idêntica à classe GenericPessoa criada anteriormente, mas não precisamos ter tido nenhum trabalho para isso!

Efetivamente, o decompilador ILDASM, fornecido com o Visual Studio permite examinar as classes e ver como elas são semelhantes.

Estas são as classes explícitas:

Esta é a classe anônima. Note a semelhança com a classe acima definida com Generics, a não ser por alguns atributos diferentes:

Extension Methods

Um método de instância nada mais é que um método “static” que aceita um parâmetro escondido, o “this”. Podemos criar em uma classe um método “static” que aceita uma instância da própria classe, mas isso não faz muito sentido; é melhor criar um método de instância logo de uma vez.

Caso desejemos em uma classe “B” manipular uma instância de uma classe “A”, podemos passar uma instância de A como parâmetro de algum método de B. Se este método de B não precisar acessar nada de B, ele pode muito bem ser estático.

Considere o exemplo abaixo:

class Numero

{

public Numero(int N)

{

FX = N;

}

int FX;

public int X

{

get { return FX; }

set { FX = value; }

}

public override string ToString()

{

return FX.ToString();

}

}

static class Extensora

{

public static void Dobro(Numero N)

{

N.X = 2 * N.X;

}

}

O método “Dobro” da classe Extensora manipula a classe Numero. Podemos usar o seguinte código:

Numero N = new Numero(10);

Extensora.Dobro(N);

O método “Dobro” pertence à classe “Extensora”, mas ele na verdade manipula instâncias da classe “Numero”. Seria interessante poder usar a sintaxe abaixo:

Numero N = new Numero(10);

N.Dobro();

É exatamente isso que um “extension method” permite. Ele é um método declarado em outra classe, mas que funciona como um método da própria classe. Ele é declarado em uma classe estática e a única diferença é o uso da palavra reservada “this” como mostrado abaixo:

static class Extensora

{

public static void Dobro(this Numero N)

{

N.X = 2 * N.X;

}

}

O Visual Studio reconhece os “extension methods” e apesar de listá-los na classe estendida, eles aparecem com uma seta azul indicando este fato:

Lambda Expressions

Esta novidade tem um nome meio esquisito, que parece vindo de um filme de ficção científica. Aliado a uma sintaxe meio peculiar, ele torna-se um recurso meio difícil de entender. Mas não é algo tão complicado, pois se parece mais como um “delegate metido à besta”.

Muitas vezes no desenvolvimento de software precisamos de algum mecanismo de “calback”, que permita que um código chame a outro através de algum tipo de ponteiro de função.

No .NET Framework, este mecanismo existe desde a versão 1.0 e chama-se “delegates”. Veja por exemplo o seguinte código:

delegate double Operacao(double x, double y);

static double Soma(double x, double y)

{

return x + y;

}

static double Produto(double x, double y)

{

return x * y;

}

private static void UsaDelegate()

{

Operacao F;

F = Soma;

Console.WriteLine(F(10, 30));

F = Produto;

Console.WriteLine(F(10, 30));

}

O delegate é um tipo que permite por exemplo declarar variáveis como “F” acima. À esta variável podemos atribuir o endereço de uma ou outra função e decidir isso em tempo de execução. O código funciona, mas não é muito conciso.

No Framework 2.0 temos os “anonymous methods”, que permite associar um bloco de código a um delegate sem precisar declarar funções explicitamente. O código acima poderia ser escrito da seguinte forma:

delegate double Operacao(double x, double y);

private static void UsaAnonymousMethod()

{

Operacao F;

F = delegate(double x, double y)

{

return x + y;

};

Console.WriteLine(F(10, 30));

F = delegate(double x, double y)

{

return x * y;

};

Console.WriteLine(F(10, 30));

}

O código acima quando compilado acaba gerando dois métodos, de forma muito semelhante ao exemplo anterior.

Note que não precisamos mais os corpos das funções “Soma” e “Produto”. Isto é particularmente útil em métodos com pouco código que não serão chamados de outros lugares. Já no C# 3.0, podemos usar uma sintaxe ainda mais simples:

private static void UsaLambda()

{

Func<double, double, double> F;

F = (x, y) => x + y;

Console.WriteLine(F(10, 30));

F = (x, y) => x * y;

Console.WriteLine(F(10, 30));

}

O código acima acaba geando mais ou menos o mesmo código que o anterior, mas o fonte é mais conciso.

Note o seguinte:

· Func<double, double, double> é semelhante a declarar um delegate que aceita dois double e retorna um “double” (o tipo mais à direita é o tipo de retorno);

· => significa “vai para”

· “(x, y) =>” corresponde à declaração de um método anônimo que aceita dois valores;

· “x + y” corresponde ao corpo da função;

· Caso a expressão do lado esquerdo de => contenha apenas um valor, os parênteses podem ser omitidos.

Um lambda é uma maneira muito conveniente de especificar um “callback”. Por exemplo, suponha que queremos varrer todos os elementos de uma lista de strings e separar os elementos que atendem a certo critério. Podemos definir um método que aceita um lambda com o critério de seleção:

static List<string> Seleciona(List<string> Lista, Func<string, bool> F) {

List<string> Result = new List<string>();

foreach (string S in Lista)

{

if (F(S)) {

Result.Add(S);

}

}

return Result;

}

Func<string, bool> corresponde a um lambda que aceita uma string e retorna um “bool” indicado se o elemento deve ser selecionado. Podemos chamar a função acima da seguinte forma:

List<string> Lista = new List<string> {"Maria", "Camila", "Mariana", "Carla"};

List<string> Resultado = Seleciona(Lista, b => b[0] == "C");

Note que o critério (no caso a primeira letra ser ‘C’ é especificado na chamada à função. É como se o código fosse um parâmetro da função. No fim das contas, o efeito do lambda é estamos sob certos aspectos “tratando funções como dados”, um mecanismo presente em linguagens “funcionais” como LISP. Não estou aqui afirmando que o C# tem a mesma expressividade que linguagens puramente funcionais, mas o C# “tomou emprestado” um mecanismo delas.

Em resumo, uma expressão lambda é uma forma simplificada de um método anônimo e pode ser usada em praticamente qualquer lugar em que um delegate seria usado. Além disso, um lambda pode referir-se a uma “Expression Tree” (veja a seguir).

Expression Tree

Uma “expression tree” ou “árvore de expressão” é semelhante às expressões que podemos definir em tempo de compilação, mas no caso das “expresion trees”, elas são definidas em tempo de execução.

Estas expressões podem ser depois compiladas e chamadas a partir de nosso código. O exemplo a seguir cria uma expressão que soma dois números, compila e invoca a expressão:

ParameterExpression x = Expression.Parameter(typeof(double), "x");

ParameterExpression y = Expression.Parameter(typeof(double), "y");

Expression body = Expression.Add(x, y);

Expression<Func<double, double, double>> Exp = Expression.Lambda<Func<double, double, double>>(body, x, y);

Console.WriteLine(Exp);

Func<double, double, double> F = Exp.Compile();

Console.WriteLine(F(10, 40));

Ou seja, os “lambdas” são até mais expressivos que os delegates, pois temos uma maneira fácil e bem documentada de criar expressões em tempo de execução, compilá-las e executálas.

De uma certa forma isso era possível anteriormente, mas deveríamos criar um assembly e chamá-lo com remoting, um trabalho bem maior.

LINQ

O objetivo principal do LINQ é aproximar dois mundos presentes no desenvolvimento de software que são relativamente separados: o mundo das linguagens de programação e o dos bancos de dados, especialmente os bancos de dados relacionais.

Os principais problemas endereçados pelo LINQ são os seguintes:

· As consultas de banco de dados relacionais são baseadas em strings, que são quase como “corpos estranhos” em relação ao resto dos elementos das linguagens de programação. A manipulação de strings é sujeita não só a erros, usualmente descobertos apenas em tempo de execução, como também a um grave problema de segurança chamado “injeção de SQL”.

· Os conjuntos de resultados retornados pelas consultas voltam em um formato que deve ser traduzido de alguma forma para s linguagem de alto nível.

O LINQ consolida as novidades discutidas anteriormente de forma a facilitar o acesso a banco de dados, permitindo expressões semelhantes às linguagens de quarta geração, mas sem abandonar as linguagens OOP tradicionais. Com o LINQ podemos efetuar uma consulta em uma base de dados com um comando semelhante ao seguinte:

IEnumerable<string> query =

from s in Nomes

where s.StartsWith("A")

orderby s

select s.ToUpper();

Um exemplo simples

Suponha que tenhamos a seguinte classe:

class Linq1

{

static string[] Nomes = { "Antonio", "Amanda", "Bruno", "Frederico", "Alberto", "Jorge", "Mauricio", "Ademar" };

static void Mostra(IEnumerable<string> query)

{

foreach (string item in query)

Console.WriteLine(item);

}

private static void Exp1()

{

Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Exp1");

IEnumerable<string> query = Nomes.Where(s => s.StartsWith("A"));

Mostra(query);

}

static void Main()

{

Exp1();

}

}

Note que no método Exp1 estamos usando um Extension Method chamado “Where” para o qual passamos a expressão lambda “s => s.StartsWith(“A”)” para especificar o critério de seleção.

Podemos adicionar ao resultado uma chamada a outro Extension Method, “OrderBy” passando a expressão lambda “s => s” de forma a ordenar o resultado:

private static void Exp2()

{

Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Exp2");

IEnumerable<string> query = Nomes.Where(s => s.StartsWith("A"));

query = query.OrderBy(s => s);

Mostra(query);

}

Adicionaremos outra chamada a um Extension Method que dirá o que selecionamos, no caso os dados convertidos para maiúsculo:

private static void Exp3()

{

Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Exp3");

IEnumerable<string> query = Nomes.Where(s => s.StartsWith("A"));

query = query.OrderBy(s => s);

query = query.Select(s => s.ToUpper());

Mostra(query);

}

Evidentemente podemos colocar tudo em uma única linha de código:

private static void Exp4()

{

Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Exp4");

IEnumerable<string> query = Nomes.Where(s => s.StartsWith("A")).OrderBy(s => s).Select(s => s.ToUpper());

Mostra(query);

}

O C# 3.0 tem uma novidade chamada “query expressions” que permite escrever a linha de código acima de uma forma mais elegante, usando novos operadores que mapeiam para os extension methods acima. Com estes novos operadores, o código fica assim:

private static void Exp5()

{

Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> Exp5");

IEnumerable<string> query =

from s in Nomes

where s.StartsWith("A")

orderby s

select s.ToUpper();

Mostra(query);

}

LINQ to SQL

Evidentemente a maior parte dos dados estará em bancos de dados SQL. Portanto, é interessante termos uma maneira de mapear a expressão LINQ para uma chamada SQL submetida a um servidor.

Para isso precisamos inicialmente definir uma classe que armazenará os dados retornados pela query. Esta classe deve ter um campo para cada coluna da query e atributos adequados, como por exemplo:

[Table(Name = "Products")]

public class Products

{

[Column]

public int ProductID { get; set; }

[Column]

public string ProductName { get; set; }

[Column]

public int? CategoryID { get; set; }

}

Depois precisamos criar um “DataContext”, que basicamente contém uma string de conexão SQL para um SQL Server. Depois disso chamamos o método GetTable que retorna uma referência a uma Tabela que será usada posteriormente na query LINQ, como mostrado a seguir:

static void Main()

{

DataContext context =

new DataContext(@"Initial Catalog=Northwind;Integrated Security=sspi");

Table<Products> Prods = context.GetTable<Products>();

var q = from c in Prods

where c.CategoryID == 1

select c;

foreach (Products P in q)

{

Console.WriteLine(P.ProductName);

}

}

A criação do comando SQL é feita dinamicamente e a query é executada apenas no momento em que os dados são lidos na chamada a foreach.

A classe DataContext é específica para o Microsoft SQL Server, mas já foram anunciadas classes similares para outros bancos de dados como Oracle, Postgres e MySSQL.

Conclusão

O uso do LINQ permite a criação de código de maneira mais fácil que o correspondente código ADO.NET.e representa sem dúvida uma grande inovação em recusros em uma linguagem de programação de uso amplo.

Mauro Sant’Anna (mauro@mas.com.br) ministra terinamentos na MAS Informática (www.mas.com.br).

Mauro Sant'Anna

Mauro Sant'Anna - Mauro tem mais de 20 anos de experiência no desenvolvimento de software, com produtos publicados no Brasil, Portugal e Estados Unidos, além de extensa experiência em treinamento e consultoria no desenvolvimento de software, tanto criando material como ministrando cursos.
Mauro é um "Microsoft Most Valuable Professional" (MVP - www.microsoft.com/mvp), “Microsoft Regional Director” (RD - www.microsoft.com/rd), membro do INETA Speaker’s Bureau (www.ineta.org) e possui as certificações MCP, MCSA (Windows 2000/2003), MCAD (C# e VB), MCDBA, MCSE (Windows 2000/2003).
Sua empresa, a M. A. S Informática (www.mas.com.br), treinou centenas de turmas em desenvolvimento de software nos últimos anos.