DataLoadOptions

A utilização da classe DataLoadOptions pelo LINQ To SQL pode ter um grande benefício em termos de performance. Quando estamos efetuando queries que possuem dados relacionados com outras tabelas, esses dados serão carregados sob demanda, ou seja, eles somente serão carregados quando precisarmos efetivamente deles.

Imagine que temos uma tabela Cliente e uma tabela Telefone, onde a segunda possui uma coluna chamada ClienteId que liga o telefone ao cliente. Para percorrer os clientes e exibir a quantidade de telefones que cada um deles tem cadastrado, podemos executar o seguinte código:

foreach (Cliente c in ctx.Clientes)
{
    Console.WriteLine(c.Nome);
    Console.WriteLine(c.Telefones.Count);
}

Ao visualizar os logs no SQL Profiler, veremos que duas queries chegam até ele, sendo uma para capturar o cliente e a outra para extrair os telefones que ele possui:

SELECT [t0].[ClienteId], [t0].[Nome]
FROM [dbo].[Cliente] AS [t0]

exec sp_executesql N‘SELECT [t0].[ClienteId], [t0].[Tipo], [t0].[Numero]
FROM [dbo].[Telefone] AS [t0]
WHERE [t0].[ClienteId] = @p0′
,N’@p0 int’,@p0=1

Com a utilização da classe DataLoadOptions, podemos especificar quais dados queremos carregar juntamente com o registro principal que, no nosso caso, são os telefones do cliente. Basicamente, precisamos instanciar esta classe e, via método LoadWith, especificamos a entidade alvo (Cliente) e, como parametro, definimos qual campo ou propriedade queremos carregar. Isso é mostrado a partir do código abaixo:

DataLoadOptions opts = new DataLoadOptions();
opts.LoadWith<Cliente>(c => c.Telefones);
ctx.LoadOptions = opts;

foreach (Cliente c in ctx.Clientes)
{
    Console.WriteLine(c.Nome);
    Console.WriteLine(c.Telefones.Count);
}

Depois da instancia da classe DataLoadOptions criada e configurada, basta definirmos a mesma na propriedade LoadOptions do contexto do LINQ To SQL. Ao executar este código, apenas uma única query será encaminhada para o SQL Server, mas preparada para extrair todos os dados:

SELECT [t0].[ClienteId], [t0].[Nome], [t1].[ClienteId] AS [ClienteId2], [t1].[Tipo], [t1].[Numero], (
    SELECT COUNT(*)
    FROM [dbo].[Telefone] AS [t2]
    WHERE [t2].[ClienteId] = [t0].[ClienteId]
    ) AS [value]
FROM [dbo].[Cliente] AS [t0]
LEFT OUTER JOIN [dbo].[Telefone] AS [t1] ON [t1].[ClienteId] = [t0].[ClienteId]
ORDER BY [t0].[ClienteId], [t1].[Tipo], [t1].[Numero]

LINQ To SQL e Processamento Assíncrono do ASP.NET

escrevi e palestrei sobre a vantagem que temos ao fazer uso das páginas assíncronas, recurso que é fornecido a partir do ASP.NET 2.0.

Infelizmente o LINQ To SQL não possui intrinsicamente métodos para executar as queries de forma assíncrona e, sendo assim, não podemos incorporá-lo na execução assíncrona da página ASP.NET. Vale lembrar que voce pode criar um delegate apontando para um método, e dentro deste invocar as queries a partir do contexto do LINQ To SQL.

Utilizando esta técnica não trará os benefícios propostos pelas páginas assíncronas, pois quando voce invocar o delegate, ele extrairá uma thread do ThreadPool para executar a tarefa. A idéia das páginas assíncronas é fazer com que o processo custoso, como acesso a banco de dados, web services, etc., seja disparado através de uma thread de IO, liberando as threads do ThreadPool apenas para executar as páginas ASP.NET.

Para fazer o LINQ To SQL (ou qualquer outra tarefa) executar em uma thread de IO, podemos recorrer ao Power Threading, criada pela Wintellect. Essa library possui várias classes que nos auxiliam em tarefas assíncronas e, entre elas, temos a classe chamada CallbackThreadPool, que encapsula e gerencia a execução de tarefas a partir de threads de IO. Um único detalhe que precisamos nos atentar é a criação de um IAsyncResult customizado, que será utilizado pelo ASP.NET para determinar quando a query executada pelo LINQ To SQL for finalizada.

A parte mais complexa é a criação do IAsyncResult. Além da sua principal finalidade, ele também trará o resultado do processo assíncrono e possíveis exceções que essa tarefa possa disparar. Para facilitar e também conseguir reutilizar essa classe por várias entidades, eu a criei de forma genérica, trabalhando fortemente tipada. Para poupar espaço, abaixo consta apenas a sua definição.

public class DataAsyncResult<TResult> : IAsyncResult
{
    //implementação
}

A classe CallbackThreadPool fornece um método QueueUserWorkItem, e recebe como parametro uma instancia do delegate WaitCallback e da classe DataAsyncResult. O delegate deverá apontar para o método que irá executar a query via LINQ To SQL. O código abaixo ilustra como proceder para alistar um novo processo assíncrono através da Power Threading da Wintellect:

DataAsyncResult<IEnumerable<Cliente>> ar = new DataAsyncResult<IEnumerable<Cliente>>(callback, state);
_threadPool.QueueUserWorkItem(new WaitCallback(RecuperarDados), ar);

A partir deste ponto tudo é como já acontece normalmente com uma página assíncrona, ou seja, definindo o atributo Async da diretiva @Page como True e registrar a execução do processo assíncrono através do método AddOnPreRenderCompleteAsync da classe Page. Código de exemplo:

AsyncLINQToSQL.zip (39.97 kb)

Reutilizando a conexão no LINQ To SQL

Uma das boas práticas que sempre foram pregadas ao escrever código de acesso à dados é abrir a conexão o mais tarde e fechar o mais cedo possível. Isso deve-se ao fato de que acessar algum recurso deste tipo é uma tarefa custosa e, caso não seja dada a devida atenção, podemos ter alguns problemas relacionados a performance.

Há alguns cenários onde essa técnica sofre uma pequena variação. Um exemplo é quando há várias queries a serem executadas e, se a cada uma delas abrirmos e fecharmos a conexão, isso irá piorar consideravelmente. Neste caso, é nítida a necessidade da reutilização de uma mesma conexão para efetuar os comandos. Ainda abriremos o mais tarde e fecharemos o mais cedo possível, mas aumentando o intervalo de tempo entre essas duas ações.

Quando trabalhamos com o LINQ To SQL, temos uma classe derivada de DataContext que encapsula o acesso os dados. A cada execução, essa classe abrirá a conexão, executará o comando desejado e, em seguida, fechará a conexão com o respectivo banco de dados. Utilizando o DataContext envolvido ou não em um bloco using, ele sempre executará estes passos para cada uma das queries executadas a partir dele, ou seja, é o comportamento padrão.

Assim como já era fornecido pela classe SqlDataAdapter, há uma versão do construtor da classe DataContext que possibilita fornecer uma conexão já aberta para que ele utilize-a por todas as queries. Basicamente o que ele faz é identificar se a instancia da conexão que é passada como parametro para a DataContext já está aberta; caso esteja, depois de executar a query, ela não será fechará, nos obrigando a fechá-la explicitamente. Se analisarmos os internals da classe DataContext, veremos que existe uma classe não documentada chamada SqlConnectionManager que é responsável por essa manipulação.

Em alguns testes, podemos notar a diferença entre os dois modos de acesso, ou seja, aquele convencional, onde cada comando abre e fecha a conexão, e o segundo modo que reutiliza a mesma conexão para todos os comandos. No primeiro caso, temos os seguintes resultados:

[ Modo Convencional ]
using (DataClasses1DataContext ctx =
    new DataClasses1DataContext(“CONNECTION_STRING”))
{
    //executar várias queries
}

    01: 00:00:01.3890421
    02: 00:00:00.9600488
    03: 00:00:00.9668471
    04: 00:00:01.0287346
    05: 00:00:00.9587953
    06: 00:00:01.1188069
    07: 00:00:01.0369689
    08: 00:00:01.0047073
    09: 00:00:00.9850514
    10: 00:00:00.9772066

Utilizando a segunda opção, devemos criar a conexão com o banco de dados e abrí-la explicitamente antes de passá-la para o construtor da classe DataContext. É importante lembrar que neste caso, a conexão não será fechada implicitamente. Dependendo da frequencia de utilização desta técnica, a criação deste código poderá ser repetitiva. Para encapsular todo esse trabalho, criei uma classe chamada ConnectedDataContext<TDataContext>. Abaixo consta a sua definição:

public class ConnectedDataContext<TDataContext> : IDisposable where TDataContext : DataContext
{
    //implementação
}

[ Reutilizando uma mesma conexão ]
using (ConnectedDataContext<DataClasses1DataContext> ctx =
                new ConnectedDataContext<DataClasses1DataContext>(“CONNECTION_STRING”))
{
    //executar várias queries
}

    01: 00:00:01.0577727
    02: 00:00:00.7618577
    03: 00:00:00.6605342
    04: 00:00:00.6552123
    05: 00:00:00.6637952
    06: 00:00:00.6604886
    07: 00:00:00.6363001
    08: 00:00:00.6754408
    09: 00:00:00.7525822
    10: 00:00:00.6538641

SQL-SMO

Diversas vezes me perguntaram como é possível acessar os dados de metadados, de gerenciamento e manipulação de objetos de um banco de dados SQL Server. Para isso, podemos recorrer ao tradicional SqlConnection e SqlCommand, executando queries, stored procedures e views que trazem informações a respeito destes recursos.

Como alternativa, a Microsoft disponibilizou um conjunto de tipos que permitem o acesso e manipulação, de forma tipada, à um servidor SQL Server, podendo extrair informações ou até mesmo criar novos objetos dentro do mesmo. O namespace em questão é o Microsoft.SqlServer.Management.Smo e está contido no assembly Microsoft.SqlServer.Smo.dll que, em conjunto com alguns outros, permitem acessar um servidor e extrair tais informações, como é mostrado através do exemplo abaixo:

foreach (Database db in new Server(“localhost”).Databases)
{
    Console.WriteLine(db.Name);

    foreach (Table t in db.Tables)
    {
        Console.WriteLine(“t” + t.Name);

        foreach (Column c in t.Columns)
        {
            Console.WriteLine(“tt” + c.Name);
        }
    }
}

Segurança em SQLCLR

O .NET Framework 2.0 em conjunto com o SQL Server 2005 permite criarmos Stored Procedures, FunctionsTriggers em código gerenciado (C# ou VB.NET). Quando o código que colocamos dentro destes objetos são códigos puramente de acesso à dados, não encontramos problemas no momento de catalogar o Assembly dentro do SQL Server.

Alguns cuidados devem ser tomados quando, dentro destes códigos, há acesso à recursos externos, como IO, Threading, SMTP, etc. Ao catalogar o Assembly, podemos definir uma propriedade chamada PERMISSION_SET, que aceita uma das tres opções a seguir:

  • SAFE: É o padrão. Neste modo o Assembly somente poderá rodar no contexto local, mas não através do SqlClient. Previne também o acesso através de recursos externos e de código não gerenciado.
  • EXTERNAL_ACCESS: É o mesmo que SAFE, somente habilitando o acesso aos recursos externos.
  • UNSAFE: Acesso irrestrito, desde que o Assembly seja assinado e catalogado por um usuário que seja membro do grupo sql_admins.

Quando temos acesso a recurso externo, então teoricamente devemos definir a PERMISSION_SET como EXTERNAL_ACCESS. O problema é que, por padrão, isso não é permitido e se tentarmos, a seguinte mensagem de erro será retornada:

CREATE ASSEMBLY for assembly ‘%’ failed because assembly ‘%’ is not authorized for PERMISSION_SET = EXTERNAL_ACCESS.  The assembly is authorizedwhen either of the following is true: the database owner (DBO) has EXTERNAL ACCESS ASSEMBLY permission and the database has the TRUSTWORTHY database property on; or the assembly is signed with a certificate or an asymmetric key that has a corresponding login with EXTERNAL ACCESS ASSEMBLY permission.

Como a própria mensagem de erro diz, bastaríamos definir a propriedade TRUSTWORTHY do banco de dados como ON, permitindo assim o acesso aos recursos utilizados. O problema é que isso não é recomendado pela Microsoft, já que possibilitaria que qualquer Assembly seja catalogado, até mesmo Assemblies maliciosos.

Para contornar o problema e catalogar o Assembly concedendo a ele, privilégios para acesso à recursos externos, a solução é a criação de de uma chave assimétrica (baseada em chave pública/privada). Para isso, utilizamos a opção CREATE ASYMMETRIC KEY juntamente com a opção FROM EXECUTABLE FILE; isso fará com que a chave pública seja importada do Assembly (devidamente assinado com Strong Name) onde está o código gerenciado com os objetos SQL e irá criá-la dentro do banco de dados master. Depois disso, devemos criar um login e vinculá-lo a esta chave assimétrica que acabamos de gerar. Finalmente damos permissão para acesso à recursos externos à este login, como é mostrado no código abaixo:

CREATE ASYMMETRIC KEY MinhaChave FROM EXECUTABLE FILE = ‘C:SQLExtensibility.dll’
CREATE LOGIN MeuLogin FROM ASYMMETRIC KEY MinhaChave
GRANT EXTERNAL ACCESS ASSEMBLY TO MeuLogin

Depois disso, basta catalogar o Assembly via IDE ou até mesmo via código, concedendo à ele EXTERNAL_ACCESS, como é mostrado no código abaixo:

CREATE ASSEMBLY RecursosExtras
FROM ‘C:SQLExtensibility.dll’
WITH PERMISSION_SET = EXTERNAL_ACCESS

Apesar da primeira forma (TRUSTWORTHY) ser muito mais simples, o melhor é seguir a recomendação da Microsoft, não concedendo TRUSTWORTHY para a base de dados que apenas utilizam recursos externos a partir de um Assembly CLR.

Customizando o SQLCLR

Uma das maiores novidades (e aparentemente pouco utilizada/comentada) que o SQL Server 2005 trouxe em conjunto com o .NET Framework 2.0, foi a capacidade de escrevermos Stored Procedures, Triggers e Functions em código gerenciado, ou seja, VB.NET ou C#, como dito aqui.

A idéia por trás da criação destes objetos em .NET é justamente fornecer ao SQL Server, acesso a recursos que são extremamente difícieis em uma linguagem de banco, garantindo também que outras aplicações (não gerenciadas) possam fazer o acesso as mesmas Stored Procedures sem pagar pelo preço do COM Interop. Sendo assim, dentro de uma Stored Procedure, antes de retornar os dados para o chamador, voce pode customizar o result-set, definindo os campos que achar conveniente e, além disso, pode aplicar a manipulação que desejar.

Imagine o seguinte cenário: voce tem uma coluna na sua tabela e, dentro dela, é necessário guardar um relatório. Dependendo do tamanho do relatório e da quantidade de registros, voce poderá “inchar” a sua base de dados rapidamente; sendo assim, voce poderia recorrer a compactação fornecida pelo .NET 2.0 e, antes de guardar ou antes de exibir, aplicar essa (des)compactação. A idéia do código abaixo é justamente, antes de retornar o result-set para o cliente, customizar a sua saída, ou seja, não é necessário retornar exatamente os dados provenientes da query que voce faz internamente.

[SqlProcedure]
public static void ResgatarConsultas()
{
    using (SqlConnection conn = new SqlConnection(“context connection=true”))
    {
        string query = “SELECT Data, Relatorio FROM Tabela”;

        using (SqlCommand cmd = new SqlCommand(query, conn))
        {
            conn.Open();
            using (SqlDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
            {
                SqlDataRecord record =
                    new SqlDataRecord(
                        new SqlMetaData(“Data”, SqlDbType.DateTime),
                        new SqlMetaData(“Relatorio”, SqlDbType.Text));

                SqlContext.Pipe.SendResultsStart(record);

                while (dr.Read())
                {
                    if (!dr.IsDBNull(0) && !dr.IsDBNull(1))
                    {
                        record.SetDateTime(0, dr.GetDateTime(0));
                        record.SetString(1,
                            Encoding.Default.GetString(IO.Decompress((byte[])dr.GetValue(1))));

                        SqlContext.Pipe.SendResultsRow(record);
                    }
                }

                SqlContext.Pipe.SendResultsEnd();
            }
        }
    }
}

Se repararmos no código acima, não há nada de novidade, pois utilizamos as mesmas classes do ADO.NET em uma aplicação tradicional. A única diferença é que não vamos expor o result-set da forma em que ele é retornado pela query interna; antes disso, será necessário passar pelo algoritmo de descompactação.

A classe SqlDataRecord representa uma linha de metadados; é através dela que vamos construir a estrutura do result-set. O construtor recebe um array de objetos do tipo SqlMetadata onde, em cada uma delas, precisamos especificar o nome e o tipo de dado do campo. Feito isso, recorremos ao método SendResultsStart passando a instancia da classe SqlDataRecord para caracterizar o ínicio do retorno para o cliente.

Uma vez que isso está pronto, utilizamos o tradicional SqlDataReader para percorrer os registros internos (retornados pelo query) e definirmos os valores fornecidos por ele para o SqlDataRecord. Note que vinculamos cada coluna do SqlDataReader a uma determinada coluna do SqlDataRecord, fazendo isso através de métodos do tipo SetXXX, onde XXX deve ser substituído pelo tipo de dado a ser carregado. O primeiro parametro de métodos SetXXX é um número inteiro que representa a coluna (do result-set) em que uma determinada coluna do SqlDataReader será carregada. Fazemos esse processo por cada iteração do SqlDataReader e, dentro deste loop, passamos o SqlDataRecord para o método SendResultsRow que, por sua vez, envia a linha para o cliente.

Finalmente, para encerrar o processamento, invocamos o método SendResultsEnd para informar ao chamador que o result-set está finalizado.

Performance, Segurança e Boas Práticas

Recentemente um amigo me procurou para dar algumas dicas se segurança e performance para melhorar de uma aplicação Web. Depois da ajuda, eu decidi disponibilizar essa listagem aqui no blog. Compilei algumas dicas e disponibilizei abaixo essa listagem, com algumas guidelines que julgo necessárias para quando for desenvolver e disponibilizar a aplicação em um servidor Web. É importante dizer que isso pode ou não ser importante, dependendo de qual o cenário da aplicação/empresa.

  • Performance
    • Desabilite a Session nas páginas que você não precisa dela. Isso pode ser realizado através do atributo EnableSessionState da diretiva @Page. Se não for utilizar na aplicação como um tudo, então desabilite a nível de aplicação, através do elemento enableSessionState no elemento do arquivo Web.Config.
    • Sempre defina para false a propriedade EnableViewState para quando não precisar do ViewState na página Web. Através do atributo maxPageStateFieldlength definido dentro do elemento , você tem a possibilidade de “quebrar” o campo __VIEWSTATE. Mas isso não traz melhora em termos de performance. Essa configuração é útil para quando alguns proxies e firewalls barram o acesso a determinadas páginas, quando ela, por sua vez, contém campos ocultos com um grande conteúdo.
    • Defina o atributo debug do elemento compilation para false. Quando definido como true em ambiente de produção, é um dos grandes vilões de performance. Se desejar que todas as aplicações do servidor Web obrigatoriamente estejam com este mesmo comportamento, voce pode definir para true o valor do atributo retail do elemento deployment do arquivo Machine.config do servidor Web.
    • Considere a utilização de OutputCache. Para facilitar a manutenção, utilize o CacheProfile.
    • Remova todos os módulos que não estão sendo utilizados pela aplicação.
    • Reduza a quantidade de round-trips entre o cliente e o servidor. Para isso, utilize sempre que possível o método Server.Transfer.
    • A compactação da resposta a ser enviada para o cliente é sempre uma boa opção. Além disso, remova caracteres desnecessários, como espaços em branco e TABs.
    • Utilize sempre DataReader para ter uma forma eficiente de carga de dados.
    • Paginar os resultados é quase sempre uma necessidade para exibição de dados.
    • Defina para true o atributo enableKernelOutputCache do elemento httpRuntime (acredito que isso já seja o padrão). Isso tem um benefício a nível do IIS, mas atente-se que, em alguns cenários, essa configuração pode trazer resultados inesperados.
    • Remova todos os HTTP Headers desnecessários. Quando você instala o .NET Framework, é criado um valor por padrão dentro do IIS, que é enviado como resposta para todos os clientes que consomem a aplicação. Geralmente é algo como: “X-Powered by ASP.NET”. Você pode remover diretamente do IIS ou via atributo enableVersionHeader do elemento httpRuntime.
    • Considere a utilização de páginas assíncronas quando possível.
  • Segurança
    • Toda aplicação Web roda sempre em FullTrust. Considere utilizar Medium (partial trust) já que ela atende a maioria dos casos, inclusive na versão 2.0 do .NET Framework, esse nível já concede acesso ao SqlClientPermission, responsável por gerenciar o acesso à servidores SQL Server. Para alterar esse nível, você pode utilizar o atributo level do elemento trust no arquivo Web.Config da aplicação ou do servidor Web, se desejar que todas as aplicações que estão rodando ali sejam definidas como medium trust.
    • Não permita que outros desenvolvedores sobrescrevam a sua policy. Para isso, defina o atributo allowOverride como false.
    • Sempre crie uma página padrão de erros para que os usuários seja redirecionados para ela quando um problema ocorrer. Para isso, configure a seção do arquivo Web.Config. Neste mesmo local, evite que as exceções sejam enviadas para os usuários, definindo o atributo mode para RemoteOnly. Isso evitará exibir detalhes da exceção que ocorreu, qual pode comprometer a sua aplicação.
    • Todos os parâmetros fornecidos pelo usuário (Forms, QueryStrings, etc.) devem ser validados por tamanho, tipo e formato.
    • Páginas importantes, que podem sofrer alguma espécie de ataque, podemos configurar a sua propriedade ViewStateUserKey para um valor qualquer (Session.SessionID), evitando assim, ataques de “one-click”.
    • Nunca armazene informações sigilosas em QueryStrings ou ViewState. Se necessitar armazenar algo importante no ViewState, então criptografe-o. Para isso defina o valor “Auto” para o atributo viewStateEncryptionMode do elemento e invoque o método RegisterRequiresViewStateEncryption na página em que deseja armazenar o conteúdo sigiloso no ViewState.
    • Use SSL somente em páginas que realmente precisam deste nível de segurança.
    • Criptografe a seção de connectionStrings.
    • Criptografe a seção de appSettings quando ele possui informações confidenciais.
    • Quando for conectar com uma base de dados qualquer, utilize um usuário que só tenha permissão necessária para manipular os objetos que estão sendo utilizados pela aplicação, nada mais.
    • Sempre armazena password “hasheados” na sua base de dados. Quando utiliza a arquitetura do Membership, ele já fornece isso intrinsicamente.
    • Defina para true o atributo httpOnlyCookies do elemento httpCookies no arquivo Web.Config. Esse atributo permite ou não que código client-side acessem o cookie.
  • Boas Práticas
    • Sempre extraia strings de arquivos de Resources (*.resx). Mais cedo ou mais tarde, alguém sempre pede para você globalizar a aplicação. 🙂
    • Defina o nome da aplicação no atributo applicationName quando utilizar o Membership/RoleProvider/Profile.
    • Se estiver utilizando o tratamento global de exceções, habilite o Health Monitoring para logar as exceções e efetuar uma posterior análise.
    • Desabilite o Trace em produção. Quando precisar habilitá-lo para poder monitorar alguma requisição, então define o atributo localOnly como true, para que o output possa ser somente visualizado no próprio computador onde a aplicação está sendo executada.

Bem, de momento o que me veio a cabeça é isso. É muito provável que existe outros detalhes que devo ter esquecido ao colocar aqui.

Convertendo uma DataRow em um Objeto

Em um projeto em qual estou trabalhando, precisei consumir um WebService e, para variar, ele retorna um Dataset e DataTables. Como em minha aplicação eu trabalho com objetos mais específicos, então tentei criar um método que convertesse cada uma das DataRows em um desses objetos e, para facilitar o trabalho, criei uma classe que é composta com alguns métodos estáticos e, entre eles, existe um que chamei de Converter:

public class CollectionHelper
{
    public static IEnumerable<TOutput> Converter<TInput, TOutput>(IEnumerable coll, Converter<TInput, TOutput> converter)
    {
        foreach (object o in coll)
        {
            yield return converter((TInput)o);
        }
    }
}

E para utilizá-lo:

IEnumerable<Usuario> result = CollectionHelper.Converter<DataRow, Usuario>(dt.Rows,
        new Converter<DataRow, Usuario>(delegate(DataRow dr)
{
    return new Usuario(dr[“Nome”].ToString());
}));

Explorando o LINQ

Bem, não é uma tradição minha colocar aqui no blog os artigos que escrevo, mas como eu gostaria de um feedback maior e, sendo assim, vai aqui o link para o mesmo: http://www.israelaece.com/post/Explorando-o-LINQ-Background.aspx.

Como voces podem ver, eu estou publicando sob-demanda e, na medida em que eu for escrevendo eu estarei atualizando o artigo com as respectivas seções. Um detalhe importante para este artigo, é que antes de decidir escreve-lo, eu fiz uma enquete com algum dos meus alunos e grande parte deles disseram que o vídeo é muito interessante e, na opinião deles, até mais do que o próprio artigo. Apesar de não concordar muito com isso e lembrando que gosto de escrever, eu decidi criar os dois, ou seja, o artigo e o complemento dele que é um vídeo.

É importante dizer que críticas, dúvidas ou sugestões são sempre bem-vindas mas não vale reclamar da quantidade de “” e “ãhhhhhn” que tem no vídeo! 😛

Explorando o LINQ – Background

Antes de efetivamente aprendermos sobre o LINQ e suas respectivas funcionalidades, não podemos deixar de lado alguns recursos que foram incorporados nas versões 2.0 e 3.5 do .NET Framework que são utilizadas imensamente quando estamos manipulando determinados objetos utilizando o LINQ. Essas funcionalidades fazem parte das versões 2.0 e 3.0 do Visual C# e das versões 8.0 e 9.0 do Visual Basic .NET, lembrando que cada uma dessas versões estão contidas em versões diferentes do .NET Framework.

É extremamente importante que essas funcionalidades fiquem claras pois elas serão utilizadas nas seções posteriores e assumirei isso como pré-requisito, não comentando mais sobre o funcionamento das mesmas. É importante dizer também que algumas destas funcionalidades não será possível abordar totalmente, pois necessitariam de um artigo exclusivo e, nestes casos, farei uma simples introdução e apontarei possíveis fontes de estudos para que possa aprimorar seus conhecimentos nesta determinada funcionalidade. Nesta seção abordaremos Generics, Nullable Types, Actions, Predicates, Comparison, Converter, Inference Types, Object Initializers, Anonymous Types, Extension Methods, Implicitly Typed Arrays e Lambda Expressions.

Generics

Generics é um novo conceito introduzido na versão 2.0 do .NET Framework, como parte integrante do CLS (Common Language Specification). Generics permitem termos classes, métodos, propriedades, delegates e Interfaces que trabalhem com um tipo não especificado. Esse tipo “não especificado” quer dizer que estes membros trabalharão com os tipos que você especificar em sua construção.

Como já sabemos, o ArrayList permite adicionarmos qualquer tipo dentro dele. Mas e se quiséssemos apenas adicionar valores inteiros, ou somente strings? Isso não seria possível pois o método Add aceita um System.Object. Com Generics, é possível criar coleções de um determinado tipo, o que permitirá que o usuário (outro desenvolvedor) somente adicione objetos do mesmo tipo e, se por acaso ele quiser adicionar algo incompatível, o erro já é detectado em design-time. O .NET Framework 2.0 fornece uma porção de coleções que utilizam Generics e que estão contidas dentro do namespace System.Collections.Generic. Classes genéricas oferecem várias vantagens, entre elas:

  • Reusabilidade: Um simples tipo genérico pode ser utilizado em diversos cenários. Um exemplo é uma classe que fornece um método para somar dois números. Só que estes números podem ser do tipo Integer, Double ou Decimal. Utilizando o conceito de Generics, não precisaríamos de overloads do método Somar(…). Bastaria criar um único método com parâmetros genéricos e a especificação do tipo a ser somado fica a cargo do consumidor.

  • Type-safety: Generics fornecem uma melhor segurança, mais especificamente em coleções. Quando criamos uma coleção genérica e especificamos em seu tipo uma string, somente podemos adicionar strings, ao contrário do ArrayList.

  • Performance: Fornecem uma melhor performance, já que não há mais o boxing e unboxing. Além disso, o número de conversões cai drasticamente, já que tudo passa a trabalhar com um tipo especificado, o que evita transformarmos em outro tipo para termos acesso às suas propriedades, métodos e eventos.

Uma classe que aceita um tipo em sua declaração pode trabalhar internamente com este tipo. Isso quer dizer que os métodos podem aceitar em seus parâmetros objetos do tipo especificado na criação da classe, retornar esses tipos em propriedades, etc..

Para exemplificarmos, vejamos uma classe que aceita um valor genérico, o que quer dizer que ela pode trabalhar (fortemente tipada) com qualquer tipo: através do exemplo abaixo “T” que identifica o tipo genérico que já pode ser acessado internamente pela classe.

public class ClasseGenerica<T>
{
    private T _valor;

    public T Valor
    {
        get
        {
            return this._valor;
        }
        set
        {
            this._valor = value;
        }
    }
}

//Utilização:
ClasseGenerica<string> classe1 = new ClasseGenerica<string>();
classe1.Valor = ".NET";

ClasseGenerica<int> classe2 = new ClasseGenerica<int>();
classe2.Valor = 123;

O que diferencia uma classe normal de uma classe genérica é o tipo que devemos especificar durante a criação da mesma. O valor “T” pode ser substituído por qualquer palavra que você achar mais conveniente para a situação e, quando quiser referenciar o tipo genérico em qualquer parte da classe, poderá acessá-lo como um tipo qualquer, como um Integer e felizmente, o Intellisense dá suporte completo a Generics. Digamos que sua classe somente trabalhará com Streams, então poderia definir “T” como “TStream” (se assim desejar):

public class ClasseGenerica<TStream>
{
    //....
}

Como podemos notar no exemplo, a classe1 trabalha somente com valores do tipo string. Já a classe2 somente trabalha com valores do tipo inteiro. Mesmo que quiser adicionar um tipo incompatível, o erro já é informado em design-time. Mas os Generics não param por aqui. Existem muitas outras possibilidades para tornarmos os Generics ainda mais poderosos, como por exemplo critérios (constraints), criação de métodos genéricos, delegates, Interfaces, entre outros e, para uma explicação completa sobre isso, consultem este artigo do MSDN.

Nullable Types

Qualquer tipo-valor em .NET possui sempre um valor padrão. Isso quer dizer que ele nunca poderá ter um valor nulo e mesmo que tentar, definindo nulo (Nothing em VB.NET e null em C#) para o tipo-valor, o compilador atirará uma exceção. Um exemplo é a estrutura DateTime. Ela tem um valor padrão que é 01/01/0001 00:00:00. Apesar de estranha, é uma data válida.

Muitas vezes um tipo pode ter uma data não informada, como por exemplo: imagine um objeto Funcionario que tem uma propriedade chamada DataDemissao. Só que este funcionário não foi demitido. Então como conseguimos distingüir se essa data é válida ou não? Pois bem, o .NET Framework 2.0 fornece o que chamamos de Nullable Types.

System.Nullable<T> é uma estrutura de dados genérica que aceita como tipo qualquer outro tipo, desde que esse tipo seja uma outra estrutura (tipo-valor), como por exemplo: Int32, Double, DateTime, etc.. Através deste tipo especial podemos definir valores nulos para ele, como por exemplo:

Nullable<DateTime> dataDemissao = null;

A estrutura genérica Nullable<T> fornece também uma propriedade chamada HasValue do tipo booleana que retorna um valor indicando se existe ou não um valor definido. E note que a estrutura Nullable<T> trata de uma estrutura genérica, onde o tipo a ser definido obrigatoriamente deve também ser uma estrutura, devido a constraint que obriga isso. Esses tipos são ideais para utilizá-los junto com registros retornados de uma base de dados qualquer. Apesar de não ter uma grande integração, ajuda imensamente para conseguirmos mapear as colunas do result-set para as propriedades dos objetos da aplicação que permitem valores nulos.

Actions

Action trata-se de um delegate que foi introduzido dentro da versão 2.0 do .NET Framework que representa um método que executa uma ação em um objeto específico. Trata-se de um delegate genérico que não possui nenhum retorno. Esse delegate e os delegates que veremos a seguir são comumente utilizandos em conjunto com arrays, evitando a necessidade da criação de um método exclusivo para iterar pela respectiva coleção efetuando alguma tarefa para cada um dos seus itens que estão contidos dentro dela. Se analisarmos a classe Array veremos vários métodos que recebem como parâmetros delegates do tipo Actions, Predicates, Comparison e Converter.

Para exemplificar, utilizamos o método ForEach da classe List que aceita como parâmetro um delegate do tipo Action que deve referenciar o método que será executado para cada um dos itens da coleção. É bom lembrar que o delegate Action trata-se de um delegate genérico e o tipo a especificar neste cenário deve, obrigatoriamente, ser o mesmo tipo que foi especificado na criação do objeto List. Como exemplo criarei também uma classe chamada Usuario que será utilizada pelos exemplos durante esta seção:

public class Usuario
{
    private string _nome;

    public Usuario(string nome)
    {
        this._nome = nome;
    }

    public string Nome
    {
        get
        {
            return this._nome;
        }
        set
        {
            this._nome = value;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        nomes.ForEach(new Action<Usuario>(Write));
    }

    private static void Write(Usuario user)
    {
        Console.WriteLine(user.Nome);
    }
}

O Visual C# ainda permite a utilização do que chamamos de métodos anônimos (in-line). Isso evitará a criação de um método exclusivo para a realização desta tarefa. Se o método será utilizado somente naquele local, então podemos omitir a criação do método através da seguinte sintaxe:

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        nomes.ForEach(new Action<Usuario>(delegate(Usuario user)
        {
            Console.WriteLine(user.Nome);        
        }));
    }
}

Predicates

Os Predicates trabalham de forma semelhante ao Action, com a exceção de que ao invés de ser um void ele retorna um valor booleano. O Predicate representa um método que define alguns critérios, determinando se um objeto atende ou não a estes critérios estabelecidos pelo Predicate. Através do exemplo abaixo, utilizamos o método Find da classe List que, dado um Predicate, ele analisa se o usuário está ou não contido dentro da listagem e, se estiver, a instância deste usuário será atribuído a variável busca.

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        Usuario busca = users.Find(new Predicate<Usuario>(Search));
        if (busca != null)
            Console.WriteLine(busca.Nome);
    }

    private static bool Search(Usuario user)
    {
        return user.Nome == "Anna";
    }
}

O Visual C# ainda permite a utilização do que chamamos de métodos anônimos (in-line). Isso evitará a criação de um método exclusivo para a realização desta tarefa. Se o método será utilizado somente naquele local, então podemos omitir a criação do método através da seguinte sintaxe:

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        Usuario busca = users.Find(new Predicate<Usuario>(delegate(Usuario user)
        {
            return user.Nome == "Anna";        
        }));

        if (busca != null)
            Console.WriteLine(busca.Nome);
    }
}

Comparison

Este delegate representa um método que é utilizado para comparar dois tipos. Este método é geralmente utilizado em conjunto com o método Sort da classe fornecido pelos array que permite ordenar uma coleção de forma ascendente ou descendente. É importante lembrar que o método Sort não retorna nenhum valor, é apenas um void (Sub em VB.NET) que manipula os itens internos ao array. Através do exemplo abaixo, utilizamos um Comparer padrão para ordenar os usuários que estão contidos dentro da coleção:

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        users.Sort(new Comparison<Usuario>(Sort));
        users.ForEach(new Action<Usuario>(Write));
    }

    private static int Sort(Usuario u1, Usuario u2)
    {
        return Comparer<string>.Default.Compare(u1.Nome, u2.Nome);
    }

    private static void Write(Usuario user)
    {
        Console.WriteLine(user.Nome);
    }
}

O Visual C# ainda permite a utilização do que chamamos de métodos anônimos (in-line). Isso evitará a criação de um método exclusivo para a realização desta tarefa. Se o método será utilizado somente naquele local, então podemos omitir a criação do método através da seguinte sintaxe:

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        users.Sort(new Comparison<Usuario>(delegate(Usuario u1, Usuario u2)
        {
            return Comparer<string>.Default.Compare(u1.Nome, u2.Nome);
        }));

        users.ForEach(new Action<Usuario>(delegate(Usuario u)
        {
            Console.WriteLine(u.Nome);
        }));
    }
}

Converter

A classe List possui um método chamado ConvertAll que recebe como parâmetro um delegate do tipo Converter. Este é um delegate genérico que devemos especificar qual será o tipo de dado de entrada e de saída em que o método ConvertAll deverá converter cada um dos elementos contidos dentro da coleção. Esse método retornará um novo objeto List onde cada um dos elementos desta coleção será do tipo especificado na criação do delegate Converter. O exemplo abaixo ilustra esse processo, onde especificamos que a coleção de objetos Usuarios será convertida para uma coleção de strings, contendo apenas o nome de cada um deles.

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        List<string> nomes = 
            users.ConvertAll<string>(new Converter<Usuario, string>(Conversao));

        nomes.ForEach(new Action<string>(Write));
    }

    private static string Conversao(Usuario user)
    {
        return user.Nome;
    }

    private static void Write(string nome)
    {
        Console.WriteLine(nome);
    }
}

O Visual C# ainda permite a utilização do que chamamos de métodos anônimos (in-line). Isso evitará a criação de um método exclusivo para a realização desta tarefa. Se o método será utilizado somente naquele local, então podemos omitir a criação do método através da seguinte sintaxe:

class Program
{
    static void Main(string[] args)
    {
        List<Usuario> users = new List<Usuario>();
        users.Add(new Usuario("Israel"));
        users.Add(new Usuario("Claudia"));
        users.Add(new Usuario("Anna"));
        users.Add(new Usuario("Juliano"));
        users.Add(new Usuario("Leandro"));
        users.Add(new Usuario("Decio"));

        List<string> nomes = users.ConvertAll<string>(
            new Converter<Usuario, string>(delegate(Usuario user)
        {
            return user.Nome;
        }));

        nomes.ForEach(new Action<string>(delegate(string nome)
        {
            Console.WriteLine(nome);
        }));
    }
}

Observação: Quando utilizamos os métodos anônimos do Visual C#, eles apenas são uma forma eficiente e elegante de criarmos o código que será executado através do delegate. Ao compilar a aplicação, os métodos são explicitamente criados dentro do Assembly e são automaticamente vinculados a algum dos delegates que vimos acima.

Inference Types

As inferências de tipo ou tipos implícitos são uma exclusividade da versão 3.5 do .NET Framework. Esta funcionalidade permite ocultarmos a declaração do tipo da variável durante a escrita do código. Mas não pense que isso irá tornar a variável do tipo System.Object (como acontece nas versões anteriores do VB.NET); ao invés disso, o tipo que a variável terá será definido a partir do valor com o qual você a inicializa. Sendo assim, podemos passar a declarar as variáveis da seguinte forma:

var id = 123;
var nomes = new string[] {"Israel", "Claudia", "Juliano"};
var valor = 12.0;

Ao declarar as variáveis desta forma, em design-time você já terá suporte aos membros do tipo que você inicializou a mesma. Para comprovar que o tipo da variável irá depender exclusivamente do valor a ela definida, podemos visualizar o código compilado, que será algo mais ou menos como o que é mostrado logo abaixo:

int id = 123;
string[] nomes = new string[] {"Israel", "Claudia", "Juliano"};
double valor = 12.0;

Entretanto, esse tipo de declaração possui algumas restrições, quais são detalhes na listagem abaixo:

  • Toda variável deve ser inicializada;

  • O inicializador deve ser uma expressão. O inicializador não pode ser um inicializador de objeto (veremos isso detalhadamente mais abaixo) ou de coleção, mas pode ser uma nova expressão que inclui um inicializador de objeto ou coleção;

  • Não é possível inicializar a variável com um tipo nulo;

  • Não é possível inicializar uma variável com ela mesma;

  • Não é possível incluir múltiplas variáveis na mesma declaração.

No caso do Visual C#, além de utilizar a inferência de tipos na declaração das variáveis, podemos fazer o mesmo na inicialização de um laço for ou até mesmo dentro do um bloco using. No primeiro caso, dentro do laço For, a variável é automaticamente inicializada com o mesmo tipo dos elementos da coleção; já no caso do bloco using, a variável terá o mesmo tipo da instância que é a ela atribuída. Só é importante lembrar que, neste caso, o objeto deve obrigatoriamente implementar a Interface IDisposable. O código abaixo ilustra esses dois casos:

//Utilização no For
var nomes = new string[] { "Israel", "Claudia", "Juliano" };
foreach (var nome in nomes)
    Console.WriteLine(nome);

//Utilização no bloco using
using (var t = new Teste())
{
    //...
}

public class Teste : IDisposable
{
    //Implementação...
}

Object Initializers

Os inicializadores de objetos consiste em uma seqüência de membros inicializada, que devem estar entre { } (chaves), onde cada uma das propriedades são separadas por vírgulas. Para que cada propriedade seja inicializada você deve especificar o nome da mesma e, depois do sinal de igual, definir o seu respectivo valor. É importante dizer que você pode inicializar os membros independente de qual tipo ele seja. O exemplo abaixo ilustra como devemos proceder para utilizar esse recurso:

public class Usuario
{
    public int Id;
    public string Nome;
}

//Utilização

Usuario user = new Usuario() { Id = 1, Nome = "Israel" };

Como podemos notar, o Visual Basic .NET exige a utilização de uma keyword chamada With para indicar ao compilador que você está tentando utilizar o inicializador de objeto. Finalmente, é importante dizer que os inicializadores de objetos não tem nenhuma relação com o construtor da classe, ou seja, eles tem funcionalidades diferentes pois, como o próprio nome diz, os inicializadores são úteis para definirmos valores para as propriedades públicas no momento da criação do objeto.

Anonymous Types

Basicamente, os tipos anônimos, assim como os métodos anônimos, são a habilidade que as linguagens Visual Basic .NET e Visual C# fornecem que nos permite criar um tipo, sem explicitamente criar uma classe formal para o mesmo. Esse recurso é extremamente importante na utilização do LINQ, que veremos nas próximos seções deste artigo. O código abaixo mostra a sintaxe de como podemos criar um objeto com duas propriedades e, logo após a sua criação, já podemos acessar as propriedades recém definidas para o tipo anônimo:

var u = new { Nome = "Israel", Idade = 25 };
Console.WriteLine(u.Nome);

Claro que, ao compilar o projeto, automaticamente uma classe com as respectivas propriedades é criada. Os tipos anônimos permitem encurtar o trabalho, sem a necessidade de perdemos tempo para criar uma “classe temporária”. Vale lembrar que veremos a grande utilidade desses recursos quando abordarmos o LINQ.

Extension Methods

Os extension methods nada mais é que métodos estáticos que permitem “adicionarmos” estes métodos a tipos pré-existentes e que podem ser invocados a partir das instâncias desses tipos. Para exemplificar, poderíamos adicionar um método rotineiro ao tipo string e assim utilizarmos por toda a aplicação. Especialmente neste caso, existem algumas diferenças consideráveis para a definição de extension methods nas linguagens VB.NET e Visual C#.

No caso do VB.NET, esses métodos devem obrigatoriamente ser definidos dentro de um módulo decorado com um atributo chamado ExtensionAttribute, que está contido dentro do namespace System.Runtime.CompilerServices (Assembly System.Core.dll). No caso do C#, a exigência é que estes métodos devem ser declarados dentro de uma classe estática, sem nenhum atributo definido. O que vai definir explicitamente o tipo onde este método será “adicionado” é o primeiro parâmetro deste método que está criando. Os parâmetros que vem a seguir são utilizados para informar um parâmetro qualquer que é necessário para que este extension method possa realizar o seu trabalho. O código abaixo ilustra como criá-los:

public static class Helper
{
    public static void Escrever(this string s, string label)
    {
        Console.WriteLine(label + ": " + s);
    }
}

//Utilização
string nome = "Israel";
nome.Escrever("Nome");

Ligeiramente o Visual C# tem uma pequena diferença na sintaxe, pois o primeiro parâmetro (que especifica o tipo em que o método será “adicionado”) deve estar pré-fixado com a keyword this. Finalmente, como podemos notar no exemplo acima, ao definir o extension method você já o tem disponível no Intellisense, com um ícone diferente em relação ao ícone padrão de um método qualquer do tipo.

Implicitly Typed Arrays

A criação implícita de arrays fortemente tipados também é uma exclusividade dessas novas versões das linguagens. Neste caso, o tipo de cada elemento é automaticamente deduzido de acordo com o tipo dos elementos onde, para isso, é necessário que no momento da inicialização do array, todos os elementos sejam, obrigatoriamente, do mesmo tipo ou que possam ser implicitamente convertidos. O exemplo abaixo ilustra como devemos proceder para criar este tipo de array:

var ids = new[] { 1.2, 2, 3, 4, 5 };
var nomes = new[] { "Israel", "Claudia", "Juliano", "Decio", "Leandro" };

Felizmente podemos combinar essa funcionalidade com algumas das funcionalidades acima, que são os tipos anônimos e os inicializadores de objetos e tornar o código um pouco mais performático e, em um primeiro momento, talvez um pouco estranho, mas com a utilização em nosso dia a dia veremos a produtividade e eficácia que isso nos proporciona. O exemplo abaixo cria um array onde em cada um dos elementos são adicionados um tipo anônimo:

var pessoas = new[]{
      new {
            Nome = "Israel", 
            Idade = 25
      },
      new {
            Nome = "Claudia", 
            Idade = 23
      }
};

Importante: Como até o presente momento o Visual Basic .NET não possui o recurso Implicitly Typed Arrays, o array pessoas possui no interior de cada elemento um tipo anônimo, mas o tipo efetivo de cada elemento é um System.Object que, vai ao contrário do que essa funcionalidade tenta fornecer, comportamento que não ocorre no Visual C#.

Lambda Expressions

Na versão 2.0 do Visual C# a Microsoft introduziu um conceito chamado métodos anônimos. Os métodos anônimos permitem escrevermos uma “expressão” de forma “in-line” em locais onde uma instância de um delegate é esperada. Apesar de citarmos alguns exemplos acima, vamos analisar um exemplo de método anônimo. Supondo que temos um delegate que recebe dois números inteiros e retorna um valor, também inteiro, temos as seguintes formas de proceder para executar o delegate nas versões .NET 1.x e 2.0 do .NET:

delegate int Operacao(int a, int b);

//Utilizando a versão 1.x
static void Main(string[] args)
{
    Operacao op = new Operacao(Soma);
    Console.WriteLine(op(2, 4));
}

public static int Soma(int a, int b)
{
    return (a + b) * 3;
}

//Utilizando a versão 2.0
static void Main(string[] args)
{
    Operacao op = new Operacao(delegate(int a, int b)
    {
        return (a + b) * 3;
    });

    Console.WriteLine(op(2, 4));
}

A primeira versão não tem muito o que comentar. É aquilo e pronto! Como podemos notar, com a versão 2.0 do Visual C#, o método para qual aponta o delegate não existe mais. Muitas vezes, criávamos um método exclusivamente para utilizar em conjunto com o delegate, poluindo a classe. Os métodos anônimos permitem-nos omitir o método que será executado quando o delegate for invocado, mas respeitando os parâmetros (em quantidade e tipo) que foram estipulados pelo delegate e que serão passados para o método anônimo. Depois de compilado, o compilador se encarrega de criar o método que, em tempo de desenvolvimento, omitimos e, além disso, se preocupa em definir todas as referências para invocá-lo corretamente.

Já as Lambda Expressions tornam os métodos anônimos muito mais concisos mas, se não ter um pouco de cuidado, pode parecer confuso em um primeiro momento. Essa é uma das principais funcionalidades que o LINQ utiliza em suas query expressions, fornecendo uma forma compacta e fortemente tipada de escrever funções, passar seus respectivos argumentos e, efetivamente, a sua avaliação (implementação).

Para exemplificar, vamos analisar como fica o código acima utilizando as Lambda Expressions:

delegate int Operacao(int a, int b);

static void Main(string[] args)
{
    Operacao op = (a, b) => (a + b) * 3;
    Console.WriteLine(op(2, 4));
}
C#  

Como podemos ver, ao invés de atribuirmos a instância propriamente dita do delegate Operacao para a variável op especificamos uma Lambda Expression. Da esquerda para a direita, (a, b) é a lista de parâmetros que foram especificados pelo delegate e, como já devem perceber, os tipos são inferidos, ou seja, na ordem em que eles aparecem na assinatura do delegate é que os seus respectivos tipos são atribuídos; em seguida temos o símbolo => que aparece sempre e, finalmente, uma expressão ou um bloco que será executado quando a expressão for invocada. É importante dizer que o nome dos parâmetros são completamente irrelevantes assim como qualquer método ou evento dentro da plataforma .NET. Como exemplo eu nomeei de a e b, mas poderia ser numero1 e numero2 ou algo de sua preferência.

O fato de podermos utilizar as Lambda Expressions impõe algumas regras que devem ser seguidas. Algumas dessas regras são exibidas através da listagem abaixo:

  • Parênteses: você pode omitir parênteses da lista de argumento somente quando existe apenas um único argumento (salvo quando quiser explicitamente definir o tipo). Quando o delegate possuir mais que um argumento ou quando o delegate for do tipo void, os parênteses devem ser especificados e, no caso do void, você deve definir um par de parênteses vazio.

  • Múltiplas linhas: é possível criar uma Lambda Expression que possui mais de uma linha. Para isso é necessário que você a envolva entre chanves { }.

Através dos códigos abaixo conseguimos ter uma idéia do poder das Lambda Expressions e, ao longo de sua utilização, a sintaxe fica amigável e rapidamente você já estará confortável em ler e entender o código.

delegate int Operacao(int a, int b);
delegate void WriteText(string text);
delegate void Teste();

static void Main(string[] args)
{
    Operacao op1 = (int a, int b) => a + b;
    Operacao op2 = (a, b) => a + b;

    WriteText wt1 = s => Console.WriteLine(s);
    WriteText wt2 = s =>
                        {
                            if (!string.IsNullOrEmpty(s))
                                Console.WriteLine(s);
                            else
                                Console.WriteLine("Valor não definido.");
                        };

    Teste t1 = () => Console.WriteLine("Teste");

    Console.WriteLine(op1(2, 3));
    Console.WriteLine(op2(3, 9));

    wt1("Israel - 1");
    wt2("Israel - 2");
    wt2(string.Empty);
    t1();
}

Baseando-se pelas explicações acima, acredito que você já consiga definir qual será o resultado. De qualquer forma, coloco logo abaixo o resultado gerado pelo código acima:

5
12
Israel – 1
Israel – 2
Valor nao definido.
Teste

A utilização das Lambda Expressions vai muito além dos exemplos que vimos aqui mas, analisaremos cada um destes cenários sob demanda, ainda neste artigo. Como você pode notar, os exemplos deste tópico são voltados exclusivamente para o Visual C#. Isso acontece porque a versão atual do Visual Basic .NET Express (Beta 1 – Orcas) ainda não suporta as Lambda Expressions. Apesar de já anunciado que até a versão Beta 2 ou Release o Visual Basic .NET suportará esta funcionalidade, optei por não abordá-la aqui justamente por não conseguir efetivamente testar.

Vídeos

Estes vídeos (Versão em C#/Versão em VB.NET) contemplam as novas funcionalidades que foram incluídas dentro da versão 3.5 do .NET Framework, mais precisamente na versão 3.0 do C# e versão 9.0 do Visual Basic .NET. Essas funcionalidades são extremamente utilizadas dentro do LINQ e você deve estar a vontade com elas antes de começar a trabalhar efetivamente com o mesmo.