.NET Language Integrated Query
Setembro de 2005
Don Box
Arquiteto, Microsoft Corporation
Anders Hejlsberg
Distinto Engenheiro, Microsoft Corporation
Resumo: descubra as facilidades de consulta de propósito geral, adicionadas ao .NET Framework que se aplicam a todas as fontes de informações, e não apenas a dados relacionais ou XML. Essa facilidade é chamada LINQ (.NET Language Integrated Query).
.NET Language Integrated Query
Após duas décadas, a indústria alcançou um ponto estável na evolução das tecnologias de programação OO (Orientadas a objetos). Os programadores agora aceitam naturalmente recursos como classes, objetos e métodos. Observando-se a geração atual e futura de tecnologias, torna-se aparente que o próximo grande desafio na tecnologia de programação é reduzir a complexidade em acessar e integrar informações que não são nativamente definidas utilizando a tecnologia OO. As duas fontes mais comuns de informações não-OO são bancos de dados relacionais e XML.
Em vez de adicionar recursos relacionais ou específicos do XML a nossas linguagens e tempo de execução de programação, com o projeto LINQ, nós obtemos uma abordagem mais geral e estamos adicionando facilidades de consulta de propósito geral ao .NET Framework que se aplicam a todas as fontes de informações, não apenas a dados relacionais ou XML. Essa facilidade é chamada LINQ (.NET Language Integrated Query).
Utilizamos o tempo consulta integrada de linguagem para indicar que a consulta é um recurso integrado das linguagens de programação primárias do desenvolvedor (por exemplo, C#, Visual Basic). A consulta integrada de linguagem permite expressões de consulta que se beneficiam de metadados valiosos, verificação de sintaxe em tempo de compilação, tipagem estática e IntelliSense que estavam previamente disponíveis apenas ao código imperativo. A consulta integrada de linguagem também permite que uma facilidade de consulta declarativa de propósito geral seja declarada a todas as informações em memória, não apenas as informações derivadas de fontes externas.
A .NET Language Integrated Query define um conjunto de operadores padrão de consulta de propósito geral que permite que operações de travessia, de filtragem e de projeção sejam expressas do modo direto, porém declarativo, em qualquer linguagem de programação baseada em .NET. Os operadores padrão de consulta permitem que consultas sejam aplicadas a qualquer fonte de informações baseada em IEnumerable<T>. A LINQ permite que terceiros aumentem o conjunto de operadores padrão de consulta com novos operadores específicos de domínio que são apropriados para o domínio ou tecnologia almejados. Mais importante, os terceiros agora são livres para substituir os operadores padrão de consulta com suas implementações próprias, que fornecem serviços adicionais tais como avaliação remota, tradução de consultas, otimização, etc. Ao aderir às convenções do padrão LINQ, tais implementações se aproveitam da mesma integração de linguagem e suporte a ferramentas dos operadores padrão de consulta.
A extensibilidade da arquitetura de consulta é utilizada no próprio projeto LINQ para fornecer implementações que funcionam tanto sobre dados XML quanto SQL. Os operadores de consulta sobre o XML (XLinq) utilizam uma facilidade XML em memória eficiente e fácil de usar para fornecer funcionalidade XPath/XQuery na linguagem de programação anfitriã. Os processos de consulta sobre dados relacionais (DLinq) são criados baseados na integração de definições de esquema SQL com no sistema de tipos da CLR. Essa integração fornece tipagem forte sobre os dados relacionais, mantendo ainda o expressivo poder do modelo relacional e o desempenho na avaliação de consultas feitas diretamente no sistema de armazenamento.
Iniciando com operadores padrão de consulta
Para ver a consulta integrada de linguagem em funcionamento, começaremos com um simples programa C# 3.0 que utiliza operadores padrão de consulta para processar o conteúdo de uma array:
using System;
using System.Query;
using System.Collections.Generic;
class app {
static void Main() {
string[] names = { "Burke", "Connor", "Frank",
"Everett", "Albert", "George",
"Harris", "David" };
IEnumerable<string> expr = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
foreach (string item in expr)
Console.WriteLine(item);
}
}
Se você compilasse e executasse esse programa, você veria isto como resultado:
BURKE
DAVID
FRANK
Para entender como a consulta integrada de linguagem funciona, precisamos dissecar a primeira instrução de nosso programa.
IEnumerable<string> expr = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
A variável local expr é inicializada com uma expressão de consulta. Uma expressão de consulta opera sobre uma ou mais fontes de informações aplicando um ou mais operadores de consulta dentre os operadores padrão de consulta ou específicos de domínio. Essa expressão utiliza três de nossos operadores padrão de consulta: Where, OrderBy e Select.
O Visual Basic 9.0 também suporta a LINQ. Eis a instrução precedente escrita em Visual Basic 9.0:
Dim expr As IEnumerable(Of String) = _
Select s.ToUpper() _
From s in names _
Where s.Length = 5 _
Order By s
Tanto as instruções C# quanto as Visual Basic mostradas aqui utilizam sintaxe de consulta. Como a instrução foreach, a sintaxe de consulta é uma abreviatura declarativa conveniente sobre o código que você poderia escrever manualmente. As instruções acima são semanticamente idênticas à seguinte sintaxe explícita mostrada em C#:
IEnumerable<string> expr = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Os argumentos para os operadores Where, OrderBy e Select são chamados expressões lambda, que são fragmentos de código bem parecidos com os delegates. Eles permitem que os operadores padrão de consulta sejam definidos individualmente como métodos e colocados juntos utilizando notação de ponto. Juntos, esses métodos formam a base de uma linguagem de consulta extensível.
Recursos de linguagem que suportam o projeto LINQ
A LINQ é criada inteiramente sobre recursos de linguagem de propósito geral, alguns dos quais são novos para o C# 3.0 e Visual Basic 9.0 Cada um desses recursos têm sua própria utilidade, mas coletivamente esses recursos fornecem um modo extensível de definir consultas e APIs para consultas. Nesta seção, exploramos esses recursos de linguagem e como contribuem para um estilo de consultas muito mais diretas e declarativas.
Expressões Lambda e árvores de expressões
Muitos operadores de consultas permitem que o usuário forneça uma função que realiza filtragem, projeção ou extração de chave. As facilidades de consulta são criadas sobre o conceito de expressões lambda, que concede aos desenvolvedores um modo conveniente de escrever funções que possam ser passadas como argumentos para subseqüente avaliação. As expressões lambda são similares aos delegates CLR e devem aderir a uma assinatura de método definida por um tipo delegate. Para ilustrar isso, podemos expandir a instrução acima em um comando equivalente, porém, mais explícito utilizando o tipo delegate Func.
Func<string, bool> filter = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();
IEnumerable<string> expr = names.Where(filter)
.OrderBy(extract)
.Select(project);
As expressões lambda são a evolução natural de métodos anônimos do C# 2.0. Por exemplo, poderíamos escrever o exemplo anterior utilizando métodos anônimos como este:
Func<string, bool> filter = delegate (string s) {
return s.Length == 5;
};
Func<string, string> extract = delegate (string s) {
return s;
};
Func<string, string> project = delegate (string s) {
return s.ToUpper();
};
IEnumerable<string> expr = names.Where(filter)
.OrderBy(extract)
.Select(project);
Em geral, o desenvolvedor é livre para utilizar métodos nomeados, métodos anônimos ou expressões lambda com operadores de consulta. As expressões lambda têm a vantagem de fornecer uma sintaxe mais direta e compacta. Mais importante, as expressões lambda podem ser compiladas como código ou dados, o que permite que expressões lambda sejam processadas em tempo de execução por otimizadores, tradutores e avaliadores.
A LINQ define um tipo distinto, Expression<T> (no namespace System.Expressions), que indica que o formato de árvore de expressões é desejado para uma determinada expressão em vez de em um corpo de método tradicional baseado em IL. Árvores de expressão são representações eficientes para definir expressões lambda como dados em memória e tornam a estrutura da expressão transparente e explícita.
A decisão de se o compilador emitirá IL executável ou árvore de expressão é determinada pelo como a expressão lambda é utilizada. Quando uma expressão lambda é atribuída a uma variável, campo ou parâmetro cujo tipo é um delegate, o compilador emite IL que é idêntico ao de um método anônimo. Quando uma expressão lambda é atribuída a uma variável, campo ou parâmetro cujo tipo é Expression<T>, o compilador emite uma árvore de expressão em vez disso.
Por exemplo, considere as duas instruções de variável a seguir:
Func<int, bool> f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;
A variável f é uma referência para um delegate que é diretamente executável:
bool isSmall = f(2); // isSmall is now true
A variável é uma referência para uma árvore de expressão que não é diretamente executável:
bool isSmall = e(2); // compile error, expressions == data
Diferentemente dos delegates, que são de fato códigos opacos, podemos interagir com a árvore de expressões da mesma forma que qualquer outra estrutura de dados em nosso programa. Por exemplo, este programa:
Expression<Func<int, bool>> filter = n => n < 5;
BinaryExpression body = (BinaryExpression)filter.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ConstantExpression right = (ConstantExpression)body.Right;
Console.WriteLine("{0} {1} {2}",
left.Name, body.NodeType, right.Value);
Decompõe a árvore de expressões no tempo de execução e imprime a seqüência:
n LT 5
Essa capacidade de tratar expressões como dados em tempo de execução é essencial para permitir um ecossistema de bibliotecas de terceiros que aproveitam as abstrações de consulta base que fazem parte da plataforma. A implementação de acesso a dados DLinq aproveita essa facilidade para traduzir árvores de expressões em instruções T-SQL adequadas para avaliação no armazenamento.
Métodos de extensão
As expressões lambda são peças importantes da arquitetura de consulta. Os métodos de extensão são outra. Os métodos de extensão combinam a flexibilidade de "duck typing" que se tornou popular em linguagens dinâmicas com desempenho e validação em tempo de compilação de linguagens estaticamente tipadas. Com métodos de extensão, terceiros podem aumentar o contrato público de um tipo existente com novos métodos, enquanto ainda permitem que autores de tipos particulares forneçam sua própria implementação especializada para esses métodos.
Os métodos de extensão são definidos em classes estáticas como métodos estáticos, mas são marcados com o atributo [System.Runtime.CompilerServices.Extension] em metadados CLR. As linguagens devem fornecer uma sintaxe direta para métodos de extensão. Em C#, os métodos de extensão são indicados pelo modificador this que deve ser aplicado ao primeiro parâmetro do método de extensão. Vamos ver a definição do operador de consulta mais simples, Where:
namespace System.Query {
using System;
using System.Collections.Generic;
public static class Sequence {
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate) {
foreach (T item in source)
if (predicate(item))
yield return item;
}
}
}
O tipo do primeiro parâmetro de um método de extensão indica a que tipo a extensão se aplica. No exemplo acima, o método de extensão Where estende o tipo IEnumerable<T>. Como Where é um método estático, podemos chamá-lo diretamente, exatamente como qualquer outro método estático.
IEnumerable<string> expr = Sequence.Where(names, s => s.Length < 6);
Entretanto, o que torna os métodos de extensão únicos é que podem também ser chamados utilizando-se a sintaxe de instância:
IEnumerable<string> expr = names.Where(s => s.Length < 6);
Os métodos de extensão são resolvidos no tempo de compilação com base nos métodos de extensão que estão no escopo. Quando um nome reservado é importado com a instrução using do C# ou a instrução Import do VB, todos os métodos de extensão definidos por classes estáticas derivadas deste nome reservado são trazidos para o escopo.
Os operadores padrão de consulta são definidos como métodos de extensão no tipo System.Query.Sequence. Ao examinar os operadores padrão de consulta, você perceberá que todos, exceto um deles, são definidos em termos da interface IEnumerable<T> (a exceção é OfType, que será descrito posteriormente). Isso significa que cada fonte de informações compatível com IEnumerable<T> obtém os operadores padrão de consulta simplesmente adicionando a seguinte instrução using em C#:
using System.Query; // makes query operators visible
Os usuários que desejam substituir os operadores padrão de consulta para um tipo específico podem (a) definir seus próprios métodos de mesmo nome no tipo específico com assinaturas compatíveis ou (b) definir novos métodos de extensão com mesmo nome que estendam o tipo específico. Os usuários que querem evitar os operadores padrão de consulta todos juntos podem simplesmente não colocar System.Query no escopo e escrever seus próprios métodos de extensão para IEnumerable<T>.
Os métodos de extensão recebem a mais baixa prioridade em termos de resolução e são utilizados apenas se não houver correspondente adequado no tipo de alvo e em seus tipos de base. Isso permite que os tipos definidos pelo usuário forneçam seus próprios operadores de consulta que têm precedência sobre os operadores padrão. Por exemplo, considere a coleção personalizada mostrada aqui:
public class MySequence : IEnumerable<int> {
public IEnumerator<int> GetEnumerator() {
for (int i = 1; i <= 10; i++)
yield return i;
}
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
public IEnumerable<int> Where(Func<int, bool> filter) {
for (int i = 1; i <= 10; i++)
if (filter(i))
yield return i;
}
}
Dada esta definição de classe, o programa a seguir:
MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
Console.WriteLine(item);
Utilizará a implementação MySequence.Where, e não o método de extensão, conforme os métodos de instância obtêm precedência sobre métodos de extensão.
O operador OfType foi mencionado anteriormente como sendo o único operador padrão que não estende uma fonte de informações baseada em IEnumerable<T>. Vamos observar o operador de consulta OfType:
public static IEnumerable<T> OfType<T>(this IEnumerable source) {
foreach (object item in source)
if (item is T)
yield return (T)item;
}
O OfType aceita não apenas fontes baseados em IEnumerable<T>, mas também fontes que são escritos usando a interface IEnumerable não parametrizada, presente na versão 1 do .NET Framework. O operador OfType permite que os usuários apliquem os operadores padrão de consulta às coleções .NET clássicas como esta:
// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();
// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();
Neste exemplo, a variável modern processa a mesma seqüência de valores que classic; entretanto, seu tipo é compatível com código IEnumerable<T> moderno, incluindo os operadores padrão para consulta.
O operador OfType também é útil para fontes de informações mais novas, à medida que permite a filtragem de valores de um fonte baseado em um tipo. Ao produzir a nova seqüência, OfType simplesmente omite membros da seqüência original que não são compatíveis com o argumento do tipo. Considere este simples programa que extrai seqüências de uma array heterogênea:
object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();
Quando enumeramos a variável justStrings em uma instrução foreach, obtemos uma seqüência de duas strings "Hello" e "World".
Avaliação de consulta adiada
Leitores observadores podem ter notado que o operador Where padrão é implementado utilizando o conceito yield introduzido no C# 2.0. Essa técnica de implementação é comum para todos os operadores padrão que retornam seqüências de valores. O uso de yield tem um benefício interessante: a consulta não é realmente avaliada até que a iteração aconteça, com uma instrução foreach, ou manualmente, utilizando os métodos GetEnumerator e MoveNext. Esse retardo na avaliação permite que as consultas sejam mantidas como valores baseados em IEnumerable<T>, podendo ser avaliadas várias vezes, e, à cada vez, processando resultados potencialmente diferentes.
Para muitos aplicativos, esse é exatamente o comportamento desejado. Para aplicativos que desejam colocar em cache os resultados da avaliação da consulta, dois operadores, ToList e ToArray, são fornecidos que forçam a avaliação imediata da consulta e retornam um List<T> ou uma array contendo os resultados da avaliação da consulta.
Para ver como o adiamento da avaliação da consulta funciona, considere este programa que executa uma consulta simples sobre uma array:
// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };
// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');
// evaluate the query
foreach (string item in ayes)
Console.WriteLine(item);
// modify the original information source
names[0] = "Bob";
// evaluate the query again, this time no "Allen"
foreach (string item in ayes)
Console.WriteLine(item);
A consulta é avaliada a cada vez que a variável ayes é reiterada. Para indicar que uma cópia em cache dos resultados é necessária, podemos simplesmente anexar um operador ToList ou ToArray a uma consulta como esta:
// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };
// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();
// iterate over the cached query results
foreach (string item in ayes)
Console.WriteLine(item);
// modifying the original source has no effect on ayes
names[0] = "Bob";
// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
Console.WriteLine(item);
Ambos ToArray e ToList forçam a imediata avaliação de consulta, como fazem os operadores que retornam valores singulares (por exemplo, First, ElementAt, Sum, Average, All, Any).
Inicializando valores compostos
Expressões lambda e métodos de extensão nos fornecem tudo que precisamos para consultas que simplesmente filtram membros fora de uma seqüência de valores. A maioria das expressões de consulta também realizam projeção sobre esses membros, transformando com eficiência membros da seqüência original em membros cujo valor e tipo podem diferir do original. Para dar suporte à escrita dessas transformações, a LINQ confia em um novo conceito chamado expressões de inicialização de objetos para criar novas instâncias de tipos estruturados. No restante deste documento, assumiremos que o seguinte tipo foi definido:
public class Person {
string name;
int age;
bool canCode;
public string Name {
get { return name; } set { name = value; }
}
public int Age {
get { return age; } set { age = value; }
}
public bool CanCode {
get { return canCode; } set { canCode = value; }
}
}
As expressões de inicialização de objetos nos permitem facilmente construir valores com base nos campos públicos e propriedades de um tipo. Por exemplo, para criar um novo valor do tipo Person, podemos escrever esta instrução:
Person value = new Person {
Name = "Chris Smith", Age = 31, CanCode = false
};
Semanticamente, esta instrução é equivalente à seguinte seqüência de instruções:
Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;
As expressões de inicialização de objetos são um recurso importante para consulta integrada de linguagem, à medida que permitem a construção de novos valores estruturados em contextos onde apenas expressões são permitidas (como expressões lambda e árvores de expressão). Por exemplo, considere esta expressão de consulta que cria um novo valor Person para cada valor na seqüência de entrada:
IEnumerable<Person> expr = names.Select(s => new Person {
Name = s, Age = 21, CanCode = s.Length == 5 });
A sintaxe de inicialização de objetos é também conveniente para a inicialização de arrayes de valores estruturados. Por exemplo, considere esta variável como um array que é inicializado utilizando inicializadores de objetos individuais:
static Person[] people = {
new Person { Name="Allen Frances", Age=11, CanCode=false },
new Person { Name="Burke Madison", Age=50, CanCode=true },
new Person { Name="Connor Morgan", Age=59, CanCode=false },
new Person { Name="David Charles", Age=33, CanCode=true },
new Person { Name="Everett Frank", Age=16, CanCode=true }
};
Valores e tipos estruturados
O projeto LINQ suporta um estilo de programação centrado em dados nos quais alguns tipos existem primariamente para fornecer uma "forma" estática sobre um valor estruturado, em vez de um objeto totalmente explodido com estado e comportamento. Levando essa premissa para a sua conclusão lógica, é freqüente o caso em que os desenvolvedores se preocupam apenas com a estrutura do dado, sendo de pouco uso a necessidade de nomear um tipo para essa estrutura. Isso leva à introdução de tipos anônimos que permitem que novas estruturas sejam definidas "inline" com sua inicialização.
No C#, a sintaxe para tipos anônimos é idêntica à sintaxe de inicialização de objetos, exceto quando o nome do tipo é omitido. Por exemplo, considere as duas instruções a seguir:
object v1 = new Person {
Name = "Chris Smith", Age = 31, CanCode = false
};
object v2 = new { // note the omission of type name
Name = "Chris Smith", Age = 31, CanCode = false
};
As variáveis v1 e v2 apontam para um objeto na memória cujo tipo CLR possui três propriedades públicas: Name, Age e CanCode. A diferença das variáveis é que v2 se refere a uma instância do tipo anônimo. Em termos de CLR, os tipos anônimos não são diferentes de qualquer outro tipo. O que torna os tipos anônimos especiais é que não possuem nome significativo em nossa linguagem de programação - o único modo de criar instâncias de um tipo anônimo é utilizar a sintaxe mostrada acima.
Para permitir que as variáveis se refiram a instâncias de tipos anônimos, mas ainda se beneficiem da tipagem estática, o C# introduz a palavra-chave var que pode ser utilizada em lugar do nome do tipo para instruções de variáveis locais. Por exemplo, considere este programa C# 3.0 válido:
var s = "Bob";
var n = 32;
var b = true;
A palavra-chave var diz ao compilador para inferir o tipo da variável a partir do tipo estático da expressão utilizada para inicializar a variável. Nesse exemplo, os tipos de s, n e b são string, int e bool, respectivamente. Este programa é idêntico ao seguinte:
string s = "Bob";
int n = 32;
bool b = true;
A palavra-chave var é uma conveniência para variáveis cujos tipos possuem nomes significativos, mas é uma necessidade para variáveis que se referem a instâncias de tipos anônimos.
var value = new {
Name = "Chris Smith", Age = 31, CanCode = false
};
No exemplo acima, a variável value é do tipo anônimo, cuja definição é equivalente ao seguinte pseudo-C#:
internal class ??? {
string _Name;
int _Age;
bool _CanCode;
public string Name {
get { return _Name; } set { _Name = value; }
}
public int Age{
get { return _Age; } set { _Age = value; }
}
public bool CanCode {
get { return _CanCode; } set { _CanCode = value; }
}
}
Os tipos anônimos não podem ser compartilhados na fronteira entre assemblies; entretanto, o compilador assegura que há no máximo um tipo anônimo de uma determinada seqüência de pares de propriedades nome/tipo dentro de cada assembly.
Como os tipos anônimos são freqüentemente utilizados em projeções para selecionar um ou mais membros de um valor estruturado existente, podemos simplesmente fazer referência a campos ou propriedades de outro valor na inicialização de um tipo anônimo. Isso resulta em um novo tipo anônimo que obtêm uma propriedade cujo nome, tipo e valor são todos copiados a partir da propriedade ou campo referenciado.
Por exemplo, considere este exemplo que cria um novo valor estruturado combinando propriedades de outros valores:
var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };
var couple = new {
Husband = new { bob.Name, bob.Age },
Wife = new { Name = jane.FirstName, jane.Age }
};
int ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name; // wn == "Jane"
A referência de campos ou propriedades mostrada abaixo é simplesmente uma sintaxe conveniente para a escrita da seguinte forma mais explícita:
var couple = new {
Husband = new { Name = bob.Name, Age = bob.Age },
Wife = new { Name = jane.FirstName, Age = jane.Age }
};
Em ambos os casos, a variável couple obtém sua própria cópia das propriedades Name e Age a partir de bob e jane.
Os tipos anônimos são mais freqüentemente utilizados na cláusula select de uma consulta. Por exemplo, considere a consulta a seguir:
var expr = people.Select(p => new {
p.Name, BadCoder = p.Age == 11
});
foreach (var item in expr)
Console.WriteLine("{0} is a {1} coder",
item.Name,
item.BadCoder ? "bad" : "good");
Nesse exemplo, pudemos criar uma nova projeção sobre o tipo Person que correspondeu exatamente à forma que precisávamos para nosso código de processamento, e ainda nos deu os benefícios de um tipo estático.
Mais operadores padrão de consulta
No topo das facilidades das consultas básicas descritas acima, existem um número de operadores que fornecem modos úteis de manipular seqüências e compor consultas, dando ao usuário um alto grau de controle sobre o resultado dentro do framework dos operadores padrão de consulta.
Classificação e agrupamento
Em geral, a avaliação de uma expressão de consulta resulta em uma seqüência de valores que são produzidos em alguma ordem que está intrínseca nas fontes de informações subjacentes. Para dar aos desenvolvedores o controle sobre a ordem na qual esses valores são produzidos, alguns operadores padrão de consulta são definidos para controlar esta ordem. O mais básico desses operadores é o operador OrderBy.
Os operadores OrderBy e OrderByDescending podem ser aplicados a qualquer fonte de informações e permitem que o usuário forneça uma função de extração de chave que produz o valor que é utilizado para classificar os resultados. OrderBy e OrderByDescending também aceitam uma função de comparação opcional que pode ser utilizada para impor uma ordem parcial sobre as chaves. Vejamos um exemplo básico:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
// unity sort
var s1 = names.OrderBy(s => s);
var s2 = names.OrderByDescending(s => s);
// sort by length
var s3 = names.OrderBy(s => s.Length);
var s4 = names.OrderByDescending(s => s.Length);
As primeiras duas expressões de consulta produzem novas seqüências que são baseadas na classificação dos membros da fonte com base na comparação de strings. As segundas duas consultas produzem novas seqüências que são baseadas na classificação dos membros da fonte com base no tamanho de cada string.
Para permitir critérios de classificação múltipla, tanto OrderBy e OrderByDescending retornam SortedSequence<T> em vez do genérico IEnumerable<T>. Dois operadores são definidos apenas sobre SortedSequence<T>, chamados ThenBy e ThenByDescending, que aplicam um critério de classificação adicional (subordinado). ThenBy/ThenByDescending por si sós retornam SortedSequence<T>, permitindo que qualquer número de operadores ThenBy/ThenByDescending seja aplicado:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);
Avaliar a consulta referenciada por s1 nesse exemplo produziria a seguinte seqüência de valores:
"Burke", "David", "Frank",
"Albert", "Connor", "George", "Harris",
"Everett"
Além da família de operadores OrderBy, os operadores padrão de consulta também incluem um operador Reverse. Reverse simplesmente enumera sobre uma seqüência e gera os mesmos valores na ordem inversa. Diferente de OrderBy, Reverse não considera os valores atuais em si na determinação da ordem, em vez disso depende somente da ordem em que os valores são produzidos pela fonte subjacente.
O operador OrderBy impõe uma ordem de classificação sobre uma seqüência de valores. Os operadores padrão de consulta também incluem o operador GroupBy, que impõe um particionamento sobre uma seqüência de valores com base em uma função de extração de chave. O operador GroupBy retorna uma seqüência de valores Grouping, uma para cada valor de chave distinto que foi encontrado. Cada Grouping contém tanto a chave como o grupo de valores mapeado para a chave. O contrato público para Grouping parece com isto:
public sealed class Grouping<K, T> {
public Grouping(K key, IEnumerable<T> group);
public Grouping();
public K Key { get; set; }
public IEnumerable<T> Group { set; get; }
}
O aplicativo mais simples de GroupBy se parece com isto:
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// group by length
var grouping = names.GroupBy(s => s.Length);
foreach (Grouping<int, string> group in grouping) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (string value in group.Group)
Console.WriteLine(" {0}", value);
}
Quando executado, este programa imprime o seguinte:
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
Strings of length 7
Everett
Como Select, GroupBy permite que você forneça uma função de projeção que é utilizada para preencher membros dos grupos.
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
// group by length
var grouping = names.GroupBy(s => s.Length,
s => s[0]);
foreach (Grouping<int, char> group in grouping) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (char value in group.Group)
Console.WriteLine(" {0}", value);
}
Essa variante imprime o seguinte:
Strings of length 6
A
C
G
H
Strings of length 5
B
D
F
Strings of length 7
E
Note, a partir desse exemplo, que o tipo projetado não precisa ser o mesmo da fonte. Nesse caso, criamos um agrupamento de inteiros para caracteres a partir de uma seqüência de strings.
Operadores de agregação
Vários operadores padrão de consulta são definidos para agregação de uma seqüência de valores em um único valor. O operador de agregação mais geral é Fold, que é definido assim:
public static U Fold<T, U>(this IEnumerable<T> source,
U seed, Func<U, T, U> func) {
U result = seed;
foreach (T element in source)
result = func(result, element);
return result;
}
O operador Fold simplifica a realização de um cálculo sobre uma seqüência de valores. Fold funciona chamando a expressão lambda uma vez para cada membro da camada subjacente. Cada vez que Fold chama a expressão lambda, ele transmite tanto o membro da seqüência quanto um valor agregado (o valor inicial é baseado no parâmetro semente para Fold). O resultado da expressão lambda substitui o valor anteriormente agregado, e Fold retorna o resultado final da expressão lambda.
Por exemplo, esse programa utiliza Fold para acumular a contagem total de caracteres sobre uma array de strings:
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int count = names.Fold(0, (c, s) => c + s.Length);
// count == 46
Além do operador Fold de propósito geral, os operadores padrão de consulta também incluem um operador Count de propósito geral e quatro operadores de agregação numéricos (Min, Max, Sum e Average) que simplificam essas operações de agregação comum. As funções de agregação numérica trabalham sobre seqüências de tipos numéricos (por exemplo, int, double, decimal) ou sobre seqüências de valores arbitrários à medida que uma função que projeta membros da seqüência em um tipo numérico é fornecida.
Este programa ilustra ambas as formas do operador Sum recém descrito:
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
"Everett", "Frank", "George", "Harris"};
int total1 = numbers.Sum(); // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46
Observe que a segunda instrução Sum é equivalente ao exemplo anterior com uso de Fold.
Select vs. SelectMany
O operador Select requer a função de transformação para produzir um valor para cada valor na seqüência da fonte. Se sua função de transformação retornar um valor que é ele mesmo uma seqüência, o cliente deverá cruzar as subseqüências manualmente. Por exemplo, considere o programa que quebra strings em tokens utilizando o método String.Split existente.
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.Select(s => s.Split(' '));
foreach (string[] line in tokens)
foreach (string token in line)
Console.Write("{0}.", token);
Quando executado, este programa imprime o seguinte texto:
Albert.was.here.Burke.slept.late.Connor.is.happy.
De modo ideal, gostaríamos que nossa consulta retornasse uma seqüência aglutinada de tokens e não expusesse o string[] intermediária ao cliente. Para conseguir isso, utilizamos o operador SelectMany em vez do operador Select. O operador SelectMany funciona de modo similar ao operador Select. A diferença é que se espera que a função de transformação retorne uma seqüência que então é expandida pelo operador SelectMany. Eis nosso programa reescrito utilizando SelectMany:
string[] text = { "Albert was here",
"Burke slept late",
"Connor is happy" };
var tokens = text.SelectMany(s => s.Split(' '));
foreach (string token in tokens)
Console.Write("{0}.", token);
O uso de SelectMany faz com que cada seqüência intermediária seja expandida com parte da avaliação normal.
Sintaxe da consulta
A instrução foreach existente do C# fornece uma sintaxe declarativa para a interação sobre métodos IEnumerable/IEnumerator do .NET Framework. A instrução foreach é estritamente opcional, mas tem provado ser um mecanismo de linguagem muito conveniente e popular.
Com base neste precedente, a sintaxe de consulta simplifica expressões de consulta com uma sintaxe declarativa para os operadores de consulta mais comuns: Where, Select, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending e ThenByDescending.
Vamos começar observando uma consulta simples com a qual começamos este documento:
IEnumerable<string> expr = names
.Where(s => s.Length == 5)
.OrderBy(s => s)
.Select(s => s.ToUpper());
Utilizando a sintaxe de consulta, podemos reescrever essa mesma instrução assim:
IEnumerable<string> expr = from s in names
where s.Length == 5
orderby s
select s.ToUpper();
Como a instrução foreach do C#, as expressões de sintaxe de consulta são mais compactas e fáceis de ler, mas são completamente opcionais. Cada expressão que pode ser escrita em sintaxe de consulta possui uma sintaxe correspondente (apesar de mais prolixa) utilizando a notação de ponto.
Vamos começar observando a estrutura básica de uma expressão de consulta. Cada expressão de consulta sintática em C# começa com uma cláusula from e termina com uma cláusula select ou group. A cláusula from inicial pode ser seguida por zero ou mais cláusulas from ou where. Cada cláusula from é como um gerador que introduz uma variável de interação que varia sobre uma seqüência, e cada cláusula where é um filtro que exclui itens do resultado. A cláusula final select ou group pode ser precedida por uma cláusula orderby que especifica um ordenamento para o resultado. A gramática simplificada para uma expressão de consulta simples é a seguinte:
from itemName in srcExpr
((from itemName in srcExpr) | (where predExpr))*
(orderby (keyExpr (ascending|descending)?)+)?
((select selExpr) | (group selExpr by keyExpr))
Por exemplo, considere estas duas expressões de consulta:
var query1 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
select new {
p.Name, Senior = p.Age > 30, p.CanCode
};
var query2 = from p in people
where p.Age > 20
orderby p.Age descending, p.Name
group new {
p.Name, Senior = p.Age > 30, p.CanCode
} by p.CanCode;
O compilador trata essas expressões de consulta como se fossem escritas utilizando a seguinte notação de ponto explícita:
var query1 = people.Where( p => p.Age > 20 )
.OrderByDescending( p => p.Age )
.ThenBy( p => p.Name)
.Select( p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
var query2 = people.Where( p => p.Age > 20 )
.OrderByDescending( p => p.Age )
.ThenBy( p => p.Name )
.GroupBy( p => p.CanCode,
p => new {
p.Name,
Senior = p.Age > 30,
p.CanCode
});
As expressões de consulta realizam uma tradução mecânica com base em nomes de métodos. A implementação do operador de consulta exato escolhido depende tanto do tipo de variável sendo consultada quanto dos métodos de extensão que estão no escopo.
As expressões de consulta mostradas até aqui têm utilizado apenas um gerador. Quando mais de um gerador é utilizado, cada gerador subseqüente é avaliado no contexto de seu antecessor. Por exemplo, considere esta leve modificação em nossa consulta:
var query = from s1 in names where s1.Length == 5
from s2 in names where s1 == s2
select s1 + " " + s2;
Quando executado frente a este array de entrada:
string[] names = { "Burke", "Connor", "Frank", "Everett",
"Albert", "George", "Harris", "David" };
Obtemos os seguintes resultados:
Burke Burke
Frank Frank
David David
A expressão de consulta acima expande para a seguinte expressão em notação de ponto:
var query = names.Where(s1 => s1.Length == 5)
.SelectMany(s1 =>
names.Where(s2 => s1 == s2)
.Select(s2 => s1 + " " + s2)
);
Observe que o uso de SelectMany faz com que a expressão interna de consulta seja aplanada no resultado externo.
Nossa gramática simplificada para expressões de consulta do início desta seção omitiu um recurso muito útil. É freqüentemente útil tratar os resultados de uma consulta como um gerador para uma consulta subseqüente. Para dar suporte a isso, as expressões de consulta utilizam a palavra-chave into para unir uma nova expressão de consulta após uma cláusula select ou group. Eis a gramática simplificada que ilustra como as palavras-chave into se ajustam no restante da sintaxe:
from itemName in srcExpr
((from itemName in srcExpr) | (where predExpr))*
(orderby (keyExpr (ascending|descending)?)+)?
((select selExpr) | (group selExpr by keyExpr))
(
into itemName
((from itemName in srcExpr) | (where predExpr))*
(orderby (keyExpr (ascending|descending)?)+)?
((select selExpr) | (group selExpr by keyExpr))
)*
A palavra-chave into é especialmente útil para processar posteriormente os resultados de uma cláusula group by. Por exemplo, considere este programa:
var query = from item in names
orderby item
group item by item.Length into lengthGroups
orderby lengthGroups.Key descending
select lengthGroups;
foreach (var group in query) {
Console.WriteLine("Strings of length {0}", group.Key);
foreach (var val in group.Group)
Console.WriteLine(" {0}", val);
}
O resultado do programa é o seguinte:
Strings of length 7
Everett
Strings of length 6
Albert
Connor
George
Harris
Strings of length 5
Burke
David
Frank
Esta seção descreveu como o C# implementa expressões de consulta. Outras linguagens podem preferir dar suporte a operadores de consulta adicionais com sintaxe explícita.
É importante observar que a sintaxe de consulta de forma alguma é ligada aos operadores padrão de consulta. É um recurso puramente sintático que se aplica a qualquer coisa que preencha o padrão LINQ implementando métodos subjacentes com nomes e assinaturas apropriados. Os operadores padrão de consulta descritos acima fazem isso utilizando métodos de extensão para aumentar a interface IEnumerable<T>. Os desenvolvedores podem explorar a sintaxe de consulta sobre o tipo que desejarem, e eles também podem assegurar a aderência ao padrão do LINQ, tanto pela implementação direta dos métodos necessários quanto pela adição de métodos de extensão.
Essa extensibilidade é explorada no próprio projeto LINQ pela provisão de duas APIs com recursos LINQ, chamadas DLinq, que implementa o padrão LINQ para acesso a dados com base em SQL, e XLinq, que permite consultas LINQ sobre dados XML. Ambas as tarefas são descritas nas seções a seguir.
Integração SQL
A .NET Language Integrated Query pode ser utilizada para consultar armazenamentos de dados relacionais sem deixar a sintaxe ou ambiente em tempo de compilação da linguagem de programação local. Essa facilidade, batizada de DLinq, se beneficia da integração de informações do esquema SQL em metadados CLR. Essa integração compila definições de tabelas e visões SQL em tipos CLR que podem ser acessados de qualquer linguagem.
O DLinq define dois atributos principais, [Table] e [Column], que indicam que tipos CLR e propriedades correspondem a dados SQL externos. O atributo [Table] pode ser aplicado a uma classe e associa o tipo CLR com uma tabela ou visão nomeada do SQL. O atributo [Column] pode ser aplicado a qualquer campo ou propriedade e associa o membro com uma coluna nomeada do SQL. Ambos os atributos são parametrizados para permitir que metadados específicos do SQL sejam retidos. Por exemplo, considere esta simples definição do esquema SQL:
create table People (
Name nvarchar(32) primary key not null,
Age int not null,
CanCode bit not null
)
create table Orders (
OrderID nvarchar(32) primary key not null,
Customer nvarchar(32) not null,
Amount int
)
O equivalente do CLR se parece com isto:
[Table(Name="People")]
public class Person {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string Name;
[Column]
public int Age;
[Column]
public bool CanCode;
}
[Table(Name="Orders")]
public class Order {
[Column(DbType="nvarchar(32) not null", Id=true)]
public string OrderID;
[Column(DbType="nvarchar(32) not null")]
public string Customer;
[Column]
public int? Amount;
}
Observe neste exemplo que colunas nullables mapeiam para tipos nullables no CLR (tipos nullables que apareceram primeiro na versão 2 do .NET Framework), e aqueles para os tipos SQL não possuem uma correspondência 1:1 com um tipo CLR (por exemplo, nvarchar, char, text); o tipo SQL original é retido nos metadados CLR.
Para emitir uma consulta contra um armazenamento relacional, a implementação DLinq do padrão LINQ traduz a consulta de sua árvore de expressão em uma expressão SQL e em um objeto DbCommand do ADO.NET adequado para a avaliação remota. Por exemplo, considere esta consulta simples:
// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
"Initial Catalog=petdb;Integrated Security=sspi");
// grab variables that represent the remote tables that
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders = context.GetTable<Order>();
// build the query
var query = from c in custs, o in orders
where o.Customer == c.Name
select new {
c.Name,
o.OrderID,
o.Amount,
c.Age
};
// execute the query
foreach (var item in query)
Console.WriteLine("{0} {1} {2} {3}",
item.Name, item.OrderID,
item.Amount, item.Age);
O tipo DataContext fornece um tradutor leve que faz o trabalho de traduzir os operadores padrão de consulta para SQL. DataContext utiliza o IDbConnection ADO.NET para acessar O armazenamento e pode ser inicializado tanto com um objeto de conexão ADO.NET estabelecido ou com uma string de conexão que possa ser utilizada para criar uma.
O método GetTable fornece variáveis compatíveis com IEnumerable que podem ser utilizadas em expressões de consulta para representar a tabela ou visão remota. Chamadas ao GetTable não resultam em qualquer interação com o banco de dados - em vez disso, representam o potencial de interagir com as tabelas ou visões remotas utilizando expressões de consulta. Em nosso exemplo acima, a consulta não é transmitida ao armazenamento até que o programa itere a expressão de consulta, neste caso utilizando a instrução foreach em C#. Quando o programa faz a primeira iteração da consulta, o mecanismo DataContext traduz a árvore de expressões na seguinte instrução SQL que é enviada ao armazenamento:
SELECT [t0].[Age], [t1].[Amount],
[t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]
É importante notar que ao criar recurso de consulta diretamente na linguagem de programação local, os desenvolvedores obtêm o pleno poder do modelo relacional sem ter de preparar estaticamente os relacionamentos no tipo CLR. O mapeamento objeto/relacional feito de modo estático pode também se beneficiar dos recursos deste núcleo para consultas, caso o usuário que deseje essa funcionalidade.
Integração XML
A .NET Language Integrated Query para XML (XLinq) permite que os dados XML sejam consultados utilizando-se operadores padrão de consulta, bem como operadores específicos de árvore que fornecem navegação similar ao XPath por meio de descendentes, ancestrais e irmãos. Fornece uma representação em memória para o XML que se integra à infra-estrutura do leitor/escritor System.Xml e é mais fácil de utilizar que o W3C DOM. Há três tipos que fazem a maior parte do trabalho de integração do XML com consultas: XName, XElement e XAttribute.
XName fornece um modo fácil para lidar com identificadores de namespaces qualificados (QNames) utilizados como nomes tanto de elementos quanto de atributos. XName manuseia de forma transparente a atomização eficiente dos identificadores e permite que tanto símbolos como strings planas sejam utilizados sempre que um QName é necessário.
Os elementos e atributos XML são representados utilizando XElement e XAttribute respectivamente. XElement e XAttribute suportam a sintaxe de construção normal, permitindo que os desenvolvedores escrevam expressões XML utilizando uma sintaxe natural:
var e = new XElement("Person",
new XAttribute("CanCode", true),
new XElement("Name", "Loren David"),
new XElement("Age", 31));
var s = e.ToString();
Isso corresponde ao seguinte XML:
<Person CanCode="true">
<Name>Loren David</Name>
<Age>31</Age>
</Person>
Observe que nenhum padrão de fábrica baseado em DOM foi necessário para criar a expressão XML, e que a implementação ToString produziu o XML textual. Os elementos XML podem também ser construídos a partir de um XmlReader existente ou a partir de um string literal:
var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
<Name>Loren David</Name>
<Age>31</Age>
</Person>");
XElement também suporta a emissão de XML com uso do tipo XmlWriter existente.
O XElement se encaixa aos operadores de consulta, permitindo que os desenvolvedores escrevam consultas para informações não-XML e produzam resultados XML construindo XElements no corpo de uma cláusula select:
var query = from p in people
where p.CanCode
select new XElement("Person",
new XAttribute("Age", p.Age),
p.Name);
Essa consulta retorna uma seqüência de XElements. Para permitir que XElements seja construído a partir do resultado deste tipo de consulta, o construtor XElement permite que seqüências de elementos sejam passadas como argumentos diretamente:
var x = new XElement("People",
from p in people
where p.CanCode
select
new XElement("Person",
new XAttribute("Age", p.Age),
p.Name));
Essa expressão XML resulta no seguinte XML:
<People>
<Person Age="11">Allen Frances</Person>
<Person Age="59">Connor Morgan</Person>
</People>
A instrução acima possui uma tradução direta para o Visual Basic. Entretanto, o Visual Basic 9.0 também suporta o uso de literais XML, que permitem que expressões de consulta sejam expressas com uso de uma sintaxe XML declarativa diretamente a partir do Visual Basic. O exemplo anterior poderia ser construído com a instrução do Visual Basic:
Dim x = _
<People>
Select <Person Age=(p.Age) >p.Name</Person> _
From p In people _
Where p.CanCode _
</People>
Os exemplos mostraram até agora como construir novos valores XML utilizando uma consulta integrada à linguagem. Os tipos XElement e XAttribute também simplificam a extração de informações de estruturas XML. XElements fornece métodos de acesso que permitem que expressões de consulta sejam aplicadas sobre os eixos XPath tradicionais. Por exemplo, a consulta a seguir extrai apenas os nomes de um XElement mostrado acima:
IEnumerable<string> justNames =
from e in x.Descendants("Person")
select e.Value;
//justNames = ["Allen Frances", "Connor Morgan"]
Para extrair valores estruturados a partir do XML, simplesmente utilizamos uma expressão inicializadora de objetos em nossa cláusula select:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int)e.Attribute("Age")
};
Observe que tanto o XAttribute quando XElement dão suporte a operadores de elenco explícitos para extrair o valor como um tipo primitivo. Para lidar com dados ausentes, podemos simplificar o elenco para um tipo nullable:
IEnumerable<Person> persons =
from e in x.Descendants("Person")
select new Person {
Name = e.Value,
Age = (int?)e.Attribute("Age") ?? 21
};
Neste caso, utilizamos um valor padrão de 21 quando o atributo Age está faltando.
O Visual Basic 9.0 fornece suporte direto de linguagem para os métodos Elements, Attribute e Descendants de XElement, permitindo que dados baseados em XML sejam acessados utilizando uma sintaxe mais compacta e direta. Podemos utilizar essa funcionalidade para escrever a instrução C# anterior desta forma:
Dim persons = _
Select new Person {
.Name = e.Name
.Age = e.@Age ?? 21
}
From e In x...Person
Em Visual Basic, a expressão e.Name encontra todos os XElements com o nome Name, a expressão e.@Age encontra o XAttribute com o nome Age, enquanto a expressão, x...Person obtém todos os itens na coleção Descendants de x com o nome Person.
Resumo
A .NET Language Integrated Query adiciona recursos de consulta ao CLR e linguagens que o almejam. A facilidade de consulta é criada sobre expressões lambda e árvores de expressões que permitem que predicados, projeções e expressões de extração de chaves sejam utilizados como código executável opaco ou como dados transparentes em memória adequados para processamento ou tradução em ambientes de produção. Os operadores padrão de consulta definidos pelo projeto LINQ funcionam sobre qualquer fonte de informações baseada em IEnumerable<T>, e são integrados com ADO.NET (DLinq ) e System.Xml (XLinq) para permitir que dados relacionais e XML ganhem os benefícios da consulta integrada na linguagem.
Barra lateral: Operadores padrão de consulta em um Nutshell
|
OfType
|
Filtro baseado na afiliação do tipo
|
|
Select/SelectMany
|
Projeção baseada na função de transformação
|
|
Where
|
Filtro baseado na função de predicação
|
|
Count
|
Contagem baseada em função de predicação opcional
|
|
All/Any
|
Quantificação universal/existencial baseada na função de predicação
|
|
First/FirstOrDefault
|
Membro inicial de acesso com base em função de predicação opcional
|
|
ElementAt
|
Membro de acesso em posição especificada
|
|
Take/Skip
|
Membros de acesso antes/depois de posição especificada
|
|
TakeWhile/SkipUntil
|
Membros de acesso antes/depois que uma função de predicado é satisfeita
|
|
GroupBy
|
Partição baseada em função de extração de chave
|
|
ToDictionary
|
Cria dicionário de chave/valor com base em função de extração de chave
|
|
OrderBy/ThenBy
|
Classificação em ordem ascendente com base em função de extração de chave e função de comparação opcional
|
|
OrderByDescending/
ThenByDescending
|
Classificação em ordem descendente com base em função de extração de chave e função de comparação opcional
|
|
Reverse
|
Inverte a ordem de uma seqüência
|
|
Fold
|
Agrega valor sobre múltiplos valores com base na função de agregação
|
|
Min/Max/Sum/Average
|
Funções de agregação numéricas
|
|
Distinct
|
Filtra membros duplicados
|
|
Except
|
Filtra elementos que são membros do conjunto especificado
|
|
Intersect
|
Filtra elementos que não são membros do conjunto especificado
|
|
Union
|
Combina membros distintos de dois conjuntos
|
|
Concat
|
Concatena os valores de duas seqüências
|
|
ToArray/ToList
|
Colocam em buffer os resultados de consulta em array ou List<T>
|
|
Range
|
Cria uma seqüência de números em um intervalo
|
|
Repeat
|
Cria uma seqüência de múltiplas cópias de um determinado valor
|