C# LINQ: Juntar
Na lojinha da Ana, os clientes ficam numa lista e os pedidos em outra. O pedido não guarda o nome do cliente, só o número dele (o ClienteId). Para escrever "Ana pagou 45,90", o programa precisa juntar as duas listas: para cada pedido, achar o cliente que tem aquele número. O LINQ faz isso com Join, GroupJoin e, no .NET 10, LeftJoin e RightJoin. E quando as listas combinam pela posição (o 1º nome com a 1ª nota), quem resolve é o Zip.
- O que é uma chave e como o
Joincasa os itens de duas listas por ela. - O mesmo
Joinna sintaxe de consulta:join … in … on … equals …, e por que a ordem dos lados importa (errosCS1937eCS1938). - O
GroupJoin(e ojoin … into): cada cliente com a lista dos seus pedidos, inclusive quem não tem nenhum. - O
LeftJoine oRightJoin.NET 10, e o jeito antigo comDefaultIfEmpty. - Chaves compostas (duas informações ao mesmo tempo) e o
Zip, que junta pela posição.
Esta aula usa o que você viu em O que é LINQ, Where e Select e Count e Sum, além dos records e das lambdas (c => c.Id). Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia antes do código
Pense numa pilha de comandas de lanchonete. Cada comanda diz "mesa 3: dois sucos". Num caderno à parte, o garçom anotou "mesa 3: Ana". Para saber o que a Ana pediu, ele pega cada comanda, olha o número da mesa e procura esse número no caderno. O número da mesa é a chave: a informação que aparece nas duas listas e liga uma à outra.
Juntar (em inglês, join) é exatamente isso: para cada item de uma lista, achar os itens da outra lista que têm a mesma chave, e montar um resultado com as duas partes. Bancos de dados fazem isso o tempo todo (o JOIN do SQL), e o LINQ faz o mesmo com as listas do seu programa.
Em toda a aula, os dados são estes dois records. Repare que o pedido 104 é de um cliente número 9, que não existe, e que a Carla (número 3) não fez pedido nenhum. Esses dois "sobrando" vão mostrar a diferença entre cada tipo de junção.
record Cliente(int Id, string Nome); record Pedido(int Numero, int ClienteId, decimal Valor); // clientes: 1 Ana, 2 Bruno, 3 Carla // pedidos: 101 (cliente 1, 45,90) 102 (cliente 2, 12,50) // 103 (cliente 1, 30,00) 104 (cliente 9, 99,00)
1. Join: casar pela chave
O método Join (juntar) recebe a outra lista e três lambdas: como achar a chave de um lado, como achar a chave do outro e o que montar com cada par que casou.
List<Cliente> clientes =
[
new(1, "Ana"),
new(2, "Bruno"),
new(3, "Carla"),
];
List<Pedido> pedidos =
[
new(101, 1, 45.90m),
new(102, 2, 12.50m),
new(103, 1, 30.00m),
new(104, 9, 99.00m),
];
var juntos = clientes.Join(
pedidos,
c => c.Id,
p => p.ClienteId,
(c, p) => $"Pedido {p.Numero}: {c.Nome} pagou {p.Valor}");
foreach (var linha in juntos)
{
Console.WriteLine(linha);
}
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run join1.cs Pedido 101: Ana pagou 45,90 Pedido 103: Ana pagou 30,00 Pedido 102: Bruno pagou 12,50
- clientes.Join(
- A lista "de fora" (em inglês, outer). O resultado segue a ordem dela: primeiro tudo da Ana, depois do Bruno.
- pedidos,
- A lista "de dentro" (inner), a que vai ser procurada.
- c => c.Id,
- Seletor da chave de fora: "a chave de um cliente é o
Iddele". - p => p.ClienteId,
- Seletor da chave de dentro: "a chave de um pedido é o
ClienteId". As duas chaves têm de ser do mesmo tipo (aqui,int). - (c, p) => …
- O seletor do resultado: recebe um cliente e um pedido que casaram e devolve o que você quiser. Aqui, um texto.
Olhe o que não apareceu:
- A Carla não tem pedido, então não formou par nenhum e sumiu.
- O pedido 104 aponta para o cliente 9, que não existe; também sumiu.
Esse comportamento tem nome: junção interna (inner join). Só entra no resultado quem tem par dos dois lados. Na seção 3 você vai ver como manter quem ficou sem par.
O Join percorre a lista de fora na ordem dela e, para cada item, devolve os pares na ordem em que aparecem na lista de dentro. Por isso o pedido 103 veio antes do 102: os dois da Ana vêm juntos, porque a Ana é a primeira cliente. Quer outra ordem? Use OrderBy depois.
A mesma coisa na sintaxe de consulta
O LINQ tem uma segunda forma de escrever, parecida com SQL (a "sintaxe de consulta", que você conheceu em O que é LINQ). Nela, a junção usa as palavras join, on e equals:
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno"), new(3, "Carla")];
List<Pedido> pedidos = [new(101, 1, 45.90m), new(102, 2, 12.50m), new(103, 1, 30.00m), new(104, 9, 99.00m)];
var juntos =
from c in clientes
join p in pedidos on c.Id equals p.ClienteId
select new { c.Nome, p.Numero, p.Valor };
foreach (var item in juntos)
{
Console.WriteLine(item);
}
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run join2.cs
{ Nome = Ana, Numero = 101, Valor = 45,90 }
{ Nome = Ana, Numero = 103, Valor = 30,00 }
{ Nome = Bruno, Numero = 102, Valor = 12,50 }
| Linha | O que faz |
|---|---|
from c in clientes | "Para cada cliente c": a lista de fora. |
join p in pedidos | "Junte com cada pedido p": a lista de dentro. |
on c.Id equals p.ClienteId | A condição: a chave do cliente é igual à chave do pedido. À esquerda do equals fica a lista de fora; à direita, a de dentro. |
select new { c.Nome, p.Numero, p.Valor } | Monta um tipo anônimo (um objeto sem nome de classe) com três campos. O WriteLine mostra os campos entre chaves. |
As duas formas geram o mesmo resultado; o compilador transforma a de consulta numa chamada a Join. Muita gente acha a de consulta mais fácil de ler quando há junções, porque o on … equals … fica parecido com a frase em português.
equals, não ==
Dentro do join, a comparação é sempre a palavra equals ("é igual a"). O == não é aceito ali, e a ordem dos lados é fixa. Os dois erros estão em "Quando dá errado".
Missão: a lista vendas só tem o número do produto. Troque o laço por um Join (ou join … equals) com produtos para mostrar o nome, a quantidade e o total de cada venda: Caneta x 10 = 25,00.
List<Produto> produtos = [new(1, "Caderno", 15.00m), new(2, "Caneta", 2.50m), new(3, "Borracha", 1.20m)];
List<Venda> vendas = [new(2, 10), new(1, 2), new(2, 4)];
// Troque este laço por um Join que mostre o NOME do produto
foreach (var v in vendas)
{
Console.WriteLine($"Produto {v.ProdutoId} x {v.Quantidade}");
}
record Produto(int Id, string Nome, decimal Preco);
record Venda(int ProdutoId, int Quantidade);
List<Produto> produtos = [new(1, "Caderno", 15.00m), new(2, "Caneta", 2.50m), new(3, "Borracha", 1.20m)];
List<Venda> vendas = [new(2, 10), new(1, 2), new(2, 4)];
var linhas = vendas.Join(
produtos,
v => v.ProdutoId,
p => p.Id,
(v, p) => $"{p.Nome} x {v.Quantidade} = {p.Preco * v.Quantidade}");
foreach (var linha in linhas)
{
Console.WriteLine(linha);
}
record Produto(int Id, string Nome, decimal Preco);
record Venda(int ProdutoId, int Quantidade);
Na solução, a lista de fora é vendas, e por isso o resultado sai na ordem das vendas (Caneta, Caderno, Caneta):
$ dotnet run vendas.cs Caneta x 10 = 25,00 Caderno x 2 = 30,00 Caneta x 4 = 10,00
2. GroupJoin: cada um com a sua lista
Às vezes você não quer um par por linha, e sim cada cliente com todos os pedidos dele num pacote só: "Ana: 2 pedidos, total 75,90". É o GroupJoin (juntar agrupando). A diferença está no último lambda: em vez de um pedido, ele recebe a coleção dos pedidos daquele cliente.
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno"), new(3, "Carla")];
List<Pedido> pedidos = [new(101, 1, 45.90m), new(102, 2, 12.50m), new(103, 1, 30.00m), new(104, 9, 99.00m)];
var porCliente = clientes.GroupJoin(
pedidos,
c => c.Id,
p => p.ClienteId,
(c, seus) => new { c.Nome, Quantos = seus.Count(), Total = seus.Sum(p => p.Valor) });
foreach (var r in porCliente)
{
Console.WriteLine($"{r.Nome}: {r.Quantos} pedido(s), total {r.Total}");
}
Console.WriteLine("--- com into ---");
var consulta =
from c in clientes
join p in pedidos on c.Id equals p.ClienteId into seus
select new { c.Nome, Numeros = string.Join(", ", seus.Select(p => p.Numero)) };
foreach (var r in consulta)
{
Console.WriteLine($"{r.Nome}: [{r.Numeros}]");
}
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run grupo.cs Ana: 2 pedido(s), total 75,90 Bruno: 1 pedido(s), total 12,50 Carla: 0 pedido(s), total 0 --- com into --- Ana: [101, 103] Bruno: [102] Carla: []
- clientes.GroupJoin(
- Mesma ideia do
Join: a lista de fora é a dos clientes. - pedidos, c => c.Id, p => p.ClienteId,
- A lista de dentro e as duas chaves, iguaizinhas às do
Join. - (c, seus) =>
- A diferença:
seusé umIEnumerable<Pedido>, a coleção de todos os pedidos do clientec. Pode estar vazia. - seus.Count(), seus.Sum(p => p.Valor)
- Como
seusé uma coleção, dá para usar nela qualquer operador do LINQ, como os de agregar. - join … into seus
- Na sintaxe de consulta, o
into("para dentro de") transforma ojoinnumGroupJoin: os pedidos de cada cliente vão para dentro deseus.
Repare na Carla: desta vez ela aparece, com 0 pedidos e total 0. O GroupJoin devolve um resultado para cada item da lista de fora, tenha ele par ou não. Já o pedido 104 (do cliente 9) continua de fora, porque nenhum cliente o "puxou".
GroupJoin × GroupBy
O GroupBy agrupa uma lista pelo valor de uma chave. O GroupJoin agrupa a segunda lista em torno de cada item da primeira. Com pedidos.GroupBy(p => p.ClienteId), a Carla não apareceria (ela não tem pedido) e o cliente 9 apareceria (ele tem). Com o GroupJoin a partir de clientes, é o contrário.
3. Manter quem ficou sem par: LeftJoin e RightJoin .NET 10
E se você quer uma linha por pedido, como no Join, mas sem perder a Carla? Isso é a junção à esquerda (left join): todo item da lista da esquerda (a de fora) aparece; quando não há par, o outro lado vem como null. Até o .NET 9 isso exigia uma receita de três passos. O .NET 10 ganhou os métodos LeftJoin e RightJoin:
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno"), new(3, "Carla")];
List<Pedido> pedidos = [new(101, 1, 45.90m), new(102, 2, 12.50m), new(103, 1, 30.00m), new(104, 9, 99.00m)];
// LeftJoin (.NET 10)
var esquerda = clientes.LeftJoin(
pedidos,
c => c.Id,
p => p.ClienteId,
(c, p) => $"{c.Nome} -> {(p is null ? "sem pedido" : p.Numero.ToString())}");
foreach (var l in esquerda) Console.WriteLine(l);
Console.WriteLine("--- jeito antigo ---");
var antigo =
from c in clientes
join p in pedidos on c.Id equals p.ClienteId into seus
from p in seus.DefaultIfEmpty()
select $"{c.Nome} -> {(p is null ? "sem pedido" : p.Numero.ToString())}";
foreach (var l in antigo) Console.WriteLine(l);
Console.WriteLine("--- RightJoin ---");
var direita = clientes.RightJoin(
pedidos,
c => c.Id,
p => p.ClienteId,
(c, p) => $"Pedido {p.Numero} -> {c?.Nome ?? "cliente desconhecido"}");
foreach (var l in direita) Console.WriteLine(l);
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run esquerda.cs Ana -> 101 Ana -> 103 Bruno -> 102 Carla -> sem pedido --- jeito antigo --- Ana -> 101 Ana -> 103 Bruno -> 102 Carla -> sem pedido --- RightJoin --- Pedido 101 -> Ana Pedido 102 -> Bruno Pedido 103 -> Ana Pedido 104 -> cliente desconhecido
| Trecho | O que faz |
|---|---|
clientes.LeftJoin(pedidos, …) | Todo cliente aparece. Quem não tem pedido aparece uma vez, com p valendo null. |
p is null ? "sem pedido" : … | Por isso o seletor precisa tratar o null. Com as referências anuláveis ligadas, o compilador sabe que ali p é Pedido?: usar p.Numero direto dá o aviso CS8602: Desreferência de uma referência possivelmente nula. |
join … into seus + from p in seus.DefaultIfEmpty() | A receita antiga: um GroupJoin, e depois "abrir" cada grupo; o DefaultIfEmpty() ("padrão se vazio") troca um grupo vazio por um grupo com um único null. |
clientes.RightJoin(pedidos, …) | O espelho: todo pedido aparece, e o cliente pode vir null (o pedido 104). Por isso o c?.Nome ?? "…" (veja ?. e ??). |
O LeftJoin e o RightJoin só existem do .NET 10 em diante. Num projeto .NET 8, clientes.LeftJoin(…) dá o erro CS1061 (‘List<Cliente>’ não contém uma definição para "LeftJoin"…). Por isso você ainda vai ver muito o join … into + DefaultIfEmpty() em tutoriais e projetos: ele faz a mesma coisa.
4. Chave composta: casar por duas coisas
A Ana tem duas lojas, Centro e Bairro, e o preço do caderno é diferente em cada uma. Para achar o estoque certo, não basta o nome do produto: a chave é produto e loja juntos. Monte as duas chaves como tipos anônimos com os mesmos nomes de campo, na mesma ordem:
List<Preco> precos = [new("Caderno", "Centro", 15.00m), new("Caderno", "Bairro", 14.00m), new("Caneta", "Centro", 2.50m)];
List<Estoque> estoques = [new("Caderno", "Bairro", 7), new("Caneta", "Centro", 40)];
var juntos =
from p in precos
join e in estoques on new { p.Produto, p.Loja } equals new { e.Produto, e.Loja }
select $"{p.Produto} na loja {p.Loja}: {e.Quantidade} un. a {p.Valor}";
foreach (var l in juntos) Console.WriteLine(l);
record Preco(string Produto, string Loja, decimal Valor);
record Estoque(string Produto, string Loja, int Quantidade);
$ dotnet run chave.cs Caderno na loja Bairro: 7 un. a 14,00 Caneta na loja Centro: 40 un. a 2,50
O caderno do Centro não tem estoque cadastrado, então sumiu (é um join interno). Dois tipos anônimos são iguais quando todos os campos são iguais; é isso que faz a chave composta funcionar. Na forma de método, o mesmo seria p => new { p.Produto, p.Loja } e e => new { e.Produto, e.Loja }. Uma tupla também serve: p => (p.Produto, p.Loja).
5. Zip: juntar pela posição
Às vezes as duas listas não têm chave nenhuma: elas simplesmente andam lado a lado. O 1º aluno tirou a 1ª nota, o 2º tirou a 2ª, e assim por diante. Para isso existe o Zip, nome que vem do zíper: os dentes dos dois lados se encaixam um a um.
string[] alunos = ["Ana", "Bruno", "Carla"];
double[] notas = [9.5, 7.0, 8.0];
foreach (var par in alunos.Zip(notas))
{
Console.WriteLine(par);
}
foreach (var (aluno, nota) in alunos.Zip(notas))
{
Console.WriteLine($"{aluno} tirou {nota}");
}
var frases = alunos.Zip(notas, (a, n) => $"{a}: {n:N1}");
Console.WriteLine(string.Join(" | ", frases));
string[] turmas = ["A", "B", "A"];
foreach (var (a, n, t) in alunos.Zip(notas, turmas))
{
Console.WriteLine($"{a} ({t}) = {n}");
}
int[] curtos = [1, 2];
Console.WriteLine(alunos.Zip(curtos).Count());
$ dotnet run zip.cs (Ana, 9,5) (Bruno, 7) (Carla, 8) Ana tirou 9,5 Bruno tirou 7 Carla tirou 8 Ana: 9,5 | Bruno: 7,0 | Carla: 8,0 Ana (A) = 9,5 Bruno (B) = 7 Carla (A) = 8 2
- alunos.Zip(notas)
- Devolve tuplas
(First, Second):(Ana, 9,5),(Bruno, 7)…First("primeiro") vem dealunos,Second("segundo") vem denotas. - var (aluno, nota)
- Desconstrói cada tupla em duas variáveis com nomes melhores que
FirsteSecond. - Zip(notas, (a, n) => …)
- Com um segundo argumento, o
Zipmonta o resultado que você quiser em vez da tupla. - Zip(notas, turmas)
- Também junta três listas de uma vez, em tuplas de três.
- alunos.Zip(curtos).Count()
- Deu
2: oZippara quando a lista mais curta acaba. A Carla ficou sem par e foi descartada em silêncio.
Zip não avisa quando sobra gente
Se você esqueceu uma nota, o último aluno simplesmente some do resultado, sem erro e sem aviso. Quando as listas precisam ter o mesmo tamanho, confira antes: if (alunos.Length != notas.Length) ….
Missão: use Zip para juntar nomes e idades e mostrar uma linha por pessoa: Ana tem 29 anos.
string[] nomes = ["Ana", "Bruno", "Carla"]; int[] idades = [29, 34, 21]; // Use Zip para juntar nomes e idades Console.WriteLine(nomes.Length);
string[] nomes = ["Ana", "Bruno", "Carla"];
int[] idades = [29, 34, 21];
foreach (var (nome, idade) in nomes.Zip(idades))
{
Console.WriteLine($"{nome} tem {idade} anos");
}
Qual usar?
| Você quer… | Use | Quem fica de fora |
|---|---|---|
| Um par por linha, só quem tem par dos dois lados | Join / join … equals | Os sem par dos dois lados |
| Cada item da 1ª lista com a coleção dos seus pares | GroupJoin / join … into | Os sem par da 2ª lista |
| Um par por linha, mantendo todos da 1ª lista | LeftJoin .NET 10 (antes: into + DefaultIfEmpty) | Os sem par da 2ª lista |
| Um par por linha, mantendo todos da 2ª lista | RightJoin .NET 10 | Os sem par da 1ª lista |
| Casar por duas informações | Chave composta: new { a, b } equals new { a, b } | Depende do tipo de junção |
| Casar pela posição (1º com 1º) | Zip | O que sobra da lista mais longa |
| Buscar um item pela chave, muitas vezes | Um Dictionary (ToDictionary(c => c.Id)) | — |
foreach e um if?
Funciona, mas compara cada cliente com todos os pedidos: com 1.000 clientes e 1.000 pedidos, são um milhão de comparações. O Join monta uma tabela de busca com as chaves da lista de dentro e procura nela, o que é muito mais rápido em listas grandes, além de ficar mais curto de ler.
Quando dá errado
Os lados do equals trocados
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno"), new(3, "Carla")];
List<Pedido> pedidos = [new(101, 1, 45.90m), new(102, 2, 12.50m), new(103, 1, 30.00m), new(104, 9, 99.00m)];
var juntos =
from c in clientes
join p in pedidos on p.ClienteId equals c.Id
select c.Nome;
Console.WriteLine(juntos.Count());
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run troca.cs /home/ana/cs/troca.cs(6,26): error CS1937: O nome "p" não está no escopo à esquerda de "equals". Considere trocar as expressões em cada lado de "equals". /home/ana/cs/troca.cs(6,45): error CS1938: O nome "c" não está no escopo à direita de "equals". Considere trocar as expressões em cada lado de "equals". Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: no join, o lado esquerdo do equals só enxerga a variável do from (c), e o direito só enxerga a do join (p). Em inglês: The name 'p' is not in scope on the left side of 'equals'. Como consertar: faça o que a própria mensagem sugere e troque os lados: on c.Id equals p.ClienteId.
Usar == no lugar de equals
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno"), new(3, "Carla")];
List<Pedido> pedidos = [new(101, 1, 45.90m), new(102, 2, 12.50m), new(103, 1, 30.00m), new(104, 9, 99.00m)];
var juntos =
from c in clientes
join p in pedidos on c.Id == p.ClienteId
select c.Nome;
Console.WriteLine(juntos.Count());
record Cliente(int Id, string Nome);
record Pedido(int Numero, int ClienteId, decimal Valor);
$ dotnet run igual.cs /home/ana/cs/igual.cs(6,45): error CS0744: Palavra-chave contextual esperada 'equals' /home/ana/cs/igual.cs(6,45): error CS1525: Termo de expressão inválido "select" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "esperava a palavra equals". O compilador leu c.Id == p.ClienteId como uma expressão só, ficou esperando o equals e tropeçou no select (o segundo erro é consequência do primeiro). Como consertar: on c.Id equals p.ClienteId. O join só sabe comparar por igualdade; para outras condições (maior que, contém…), use from duas vezes e um where.
Chaves de tipos diferentes
List<Cliente> clientes = [new(1, "Ana"), new(2, "Bruno")]; List<Pedido> pedidos = [new(101, "1", 45.90m)]; var juntos = clientes.Join(pedidos, c => c.Id, p => p.ClienteId, (c, p) => c.Nome); Console.WriteLine(juntos.Count()); record Cliente(int Id, string Nome); record Pedido(int Numero, string ClienteId, decimal Valor);
$ dotnet run tipos.cs /home/ana/cs/tipos.cs(4,23): error CS0411: Os argumentos de tipo do método "Enumerable.Join<TOuter, TInner, TKey, TResult>(IEnumerable<TOuter>, IEnumerable<TInner>, Func<TOuter, TKey>, Func<TInner, TKey>, Func<TOuter, TInner, TResult>)" não podem ser inferidos com base no uso. Tente especificar explicitamente os argumentos de tipo. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: a mensagem assusta, mas o ponto é o TKey, que aparece duas vezes: as duas chaves precisam ser do mesmo tipo. Aqui uma é int (c.Id) e a outra é string (p.ClienteId), e o compilador não consegue escolher um TKey único. Como consertar: deixe as chaves do mesmo tipo, de preferência nos próprios dados (int ClienteId). Se não der, converta num dos lados: c => c.Id.ToString().
Sumiu gente do resultado (sem erro nenhum)
O programa compila e roda, mas a Carla não aparece, ou o último aluno do Zip some. O que quer dizer: não é bug do LINQ: o Join é interno (só quem tem par) e o Zip para na lista mais curta. Como consertar: para manter todos da primeira lista, use GroupJoin ou LeftJoin; no Zip, confira os tamanhos antes.
Referência
| Forma | Exemplo | Resultado |
|---|---|---|
Join | a.Join(b, x => x.Id, y => y.AId, (x, y) => …) | Um item por par que casou |
join (consulta) | from x in a join y in b on x.Id equals y.AId select … | Igual ao Join |
GroupJoin | a.GroupJoin(b, x => x.Id, y => y.AId, (x, ys) => …) | Um item por elemento de a, com a coleção ys |
join … into | join y in b on x.Id equals y.AId into ys | Igual ao GroupJoin |
LeftJoin .NET 10 | a.LeftJoin(b, …, (x, y) => …) | Todos de a; y pode ser null |
RightJoin .NET 10 | a.RightJoin(b, …, (x, y) => …) | Todos de b; x pode ser null |
| Left join antigo | … into ys from y in ys.DefaultIfEmpty() | Igual ao LeftJoin |
| Chave composta | on new { x.A, x.B } equals new { y.A, y.B } | Casa só se as duas partes forem iguais |
Zip | a.Zip(b), a.Zip(b, (x, y) => …), a.Zip(b, c) | Pares (ou trios) pela posição; para na mais curta |
Exercícios
1. Complete a junção na sintaxe de consulta:
2. Que palavra transforma o join num GroupJoin?
3. Quantos itens tem new[] { 1, 2, 3, 4 }.Zip(new[] { "a", "b" })?
4. Com clientes.Join(pedidos, …), o que acontece com um cliente que não fez pedido?
5. Você quer uma linha por pedido, inclusive pedidos de clientes que não existem, com "cliente desconhecido". Partindo de clientes, qual método do .NET 10 serve?
6. Por que join p in pedidos on p.ClienteId equals c.Id não compila?
Missão final: mostre cada turma com a quantidade e os nomes dos seus alunos, no formato Turma A (Manhã): 3 aluno(s) [Ana, Carla, Davi]. A turma C não tem ninguém, mas precisa aparecer: use GroupJoin (ou join … into).
List<Turma> turmas = [new("A", "Manhã"), new("B", "Tarde"), new("C", "Noite")];
List<Aluno> alunos = [new("Ana", "A"), new("Bruno", "B"), new("Carla", "A"), new("Davi", "A")];
// Use GroupJoin (ou join ... into) para mostrar cada turma com os seus alunos.
// Turmas sem ninguém também precisam aparecer.
record Turma(string Codigo, string Turno);
record Aluno(string Nome, string Turma);
List<Turma> turmas = [new("A", "Manhã"), new("B", "Tarde"), new("C", "Noite")];
List<Aluno> alunos = [new("Ana", "A"), new("Bruno", "B"), new("Carla", "A"), new("Davi", "A")];
var resultado = turmas.GroupJoin(
alunos,
t => t.Codigo,
a => a.Turma,
(t, seus) => $"Turma {t.Codigo} ({t.Turno}): {seus.Count()} aluno(s) [{string.Join(", ", seus.Select(a => a.Nome))}]");
foreach (var linha in resultado)
{
Console.WriteLine(linha);
}
record Turma(string Codigo, string Turno);
record Aluno(string Nome, string Turma);
A solução, rodando na máquina da Ana:
$ dotnet run turmas.cs Turma A (Manhã): 3 aluno(s) [Ana, Carla, Davi] Turma B (Tarde): 1 aluno(s) [Bruno] Turma C (Noite): 0 aluno(s) []
Resumo
- Juntar é casar os itens de duas listas por uma chave que aparece nas duas (o
Iddo cliente e oClienteIddo pedido). As chaves precisam ser do mesmo tipo (senão,CS0411). Join(oujoin … in … on … equals …) é a junção interna: só entra quem tem par dos dois lados. Noequals, a lista de fora fica à esquerda (senão,CS1937/CS1938), e==não vale (CS0744).GroupJoin(oujoin … into) dá cada item da primeira lista com a coleção dos seus pares, mesmo vazia.LeftJoineRightJoin.NET 10 mantêm todos de um lado, comnullno outro; antes, a receita erainto+DefaultIfEmpty().- Chave composta:
new { x.A, x.B } equals new { y.A, y.B }. Zipjunta pela posição, devolve tuplas(First, Second)e para quando a lista mais curta acaba, sem avisar.
Próxima aula: Pattern Matching, o jeito moderno de perguntar "que forma tem este objeto?".