C# Span<T>
Quando você escreve vendas[1..4] num array, o C# copia aqueles itens para um array novo. Quando faz texto.Substring(0, 3), cria uma string nova. Para poucos dados, tanto faz. Mas um programa que lê um arquivo de um milhão de linhas e corta cada uma em pedaços cria milhões de cópias que logo vão para o lixo. O Span<T> (em inglês, "extensão", "vão") é uma janela sobre um trecho de um array ou de um texto: você olha e mexe naquele pedaço sem copiar nada.
- O que é uma janela (
Span<T>) e como criá-la comAsSpan,Slicee intervalos. - Que mexer na janela muda o array (e que
array[1..4], ao contrário, copia). ReadOnlySpan<char>: ler pedaços de um texto semSubstring, e o erroCS8331ao tentar alterar.- Escrever métodos que recebem
ReadOnlySpan<T>e aceitam arrays, pedaços e[1, 2, 3], e as conversões novas do C# 14. stackalloce por que um span tem regras especiais (CS8345,CS8175,CS0029).
Esta aula usa arrays, índices e intervalos (^1, 1..4), structs e valor × referência. Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia antes do código
A Ana tem um caderno com as vendas de cada dia do mês. A contadora pede: "me mostre só a segunda semana". A Ana pode tirar xerox daquelas páginas (custa papel e tempo, e se ela corrigir o caderno depois, a cópia fica desatualizada) ou pode pôr um marcador no começo da semana e dizer "são estas 7 linhas, a partir daqui".
A cópia é o vendas[7..14] de um array. O marcador é o Span: ele guarda só onde começa e quantos itens tem. Os dados continuam no caderno original. Olhar pelo marcador é olhar o caderno; escrever pelo marcador é escrever no caderno.
1. A primeira janela: AsSpan
int[] vendas = [10, 20, 30, 40, 50, 60];
Span<int> meio = vendas.AsSpan(1, 3);
Console.WriteLine($"Janela: {string.Join(", ", meio.ToArray())} (tamanho {meio.Length})");
meio[0] = 99;
Console.WriteLine($"Array: {string.Join(", ", vendas)}");
int[] copia = vendas[1..4];
copia[0] = -1;
Console.WriteLine($"Cópia: {string.Join(", ", copia)}");
Console.WriteLine($"Array: {string.Join(", ", vendas)}");
Span<int> fim = vendas.AsSpan()[^2..];
foreach (int v in fim) Console.Write($"{v} ");
Console.WriteLine();
$ dotnet run janela.cs Janela: 20, 30, 40 (tamanho 3) Array: 10, 99, 30, 40, 50, 60 Cópia: -1, 30, 40 Array: 10, 99, 30, 40, 50, 60 50 60
- Span<int>
- Uma janela sobre
ints. O<int>é o tipo dos itens, como emList<int>(generics). - vendas.AsSpan(1, 3)
- "Como span, a partir da posição 1, com 3 itens." Nada é copiado: a janela aponta para dentro de
vendas. - meio[0] = 99;
- A posição 0 da janela é a posição 1 do array. Gravar na janela grava no array: veja o
99na segunda linha. - vendas[1..4]
- Num array, um intervalo cria um array novo (uma cópia). Mudar a cópia não mexeu em
vendas. - vendas.AsSpan()[^2..]
- Num span, o intervalo cria outra janela, sem copiar: os dois últimos itens.
- meio.ToArray()
- Quando você quer uma cópia (aqui, para o
string.Join), peça explicitamente.
| Para criar a janela… | Escreva |
|---|---|
| Do array inteiro | vendas.AsSpan() ou Span<int> s = vendas; |
| A partir de uma posição, com N itens | vendas.AsSpan(1, 3) ou s.Slice(1, 3) (slice = fatia) |
| Com intervalo | s[1..4], s[^2..], s[..3] |
| Com valores fixos C# 12 | Span<int> s = [1, 2, 3]; |
A janela tem Length, aceita [i], foreach, IndexOf, Contains, Fill, Sort e Reverse, como um array. O que ela não pode é crescer: o tamanho é o do trecho.
Missão: o array tem as vendas de segunda a domingo. Crie uma janela (Span<int>) sobre os dois últimos dias (posição 5, 2 itens) e some só ela, para mostrar Fim de semana: 110.
int[] vendas = [12, 8, 15, 9, 20, 45, 65];
int total = 0;
foreach (int v in vendas) total += v; // isto soma a semana toda
Console.WriteLine($"Fim de semana: {total}");
int[] vendas = [12, 8, 15, 9, 20, 45, 65];
Span<int> fimDeSemana = vendas.AsSpan(5, 2);
int total = 0;
foreach (int v in fimDeSemana) total += v;
Console.WriteLine($"Fim de semana: {total}");
2. ReadOnlySpan<char>: pedaços de texto sem cópia
Uma string não pode ser alterada (veja Strings), então a janela sobre ela é ReadOnlySpan<char>: uma janela só de leitura. Ela serve para recortar um texto sem criar um Substring a cada pedaço. A Ana lê uma linha de um cadastro, "Ana;29;Recife":
string linha = "Ana;29;Recife";
ReadOnlySpan<char> resto = linha;
int pos = resto.IndexOf(';');
ReadOnlySpan<char> nome = resto[..pos];
resto = resto[(pos + 1)..];
pos = resto.IndexOf(';');
int idade = int.Parse(resto[..pos]);
ReadOnlySpan<char> cidade = resto[(pos + 1)..];
Console.WriteLine($"Nome: {nome}");
Console.WriteLine($"Idade daqui a 1 ano: {idade + 1}");
Console.WriteLine($"Cidade: {cidade.ToString().ToUpper()}");
Console.WriteLine(cidade.StartsWith("Rec"));
$ dotnet run linha.cs Nome: Ana Idade daqui a 1 ano: 30 Cidade: RECIFE True
| Linha | O que faz |
|---|---|
ReadOnlySpan<char> resto = linha; | Uma janela sobre o texto inteiro. A string vira span sozinha. |
resto.IndexOf(';') | Onde está o primeiro ; (posição 3). |
resto[..pos] | A janela do começo até antes do ;: Ana. Nenhuma string nova foi criada. |
resto = resto[(pos + 1)..]; | Anda a janela para depois do ;. Agora ela mostra 29;Recife. |
int.Parse(resto[..pos]) | O int.Parse (e o TryParse, o decimal.Parse…) aceita um span direto: converte o 29 sem criar a string "29". |
cidade.ToString() | Quando você precisa de uma string de verdade (aqui, para o ToUpper), o ToString() faz a cópia. |
No $"Nome: {nome}", a interpolação aceita o span e escreve o texto dele. Compare com o jeito de sempre, linha.Split(';'): ele cria um array e três strings novas. Para uma linha, não faz diferença; para um arquivo enorme, o span evita milhões de cópias.
Missão: a ficha "Bruno;34" tem nome e idade. Com um ReadOnlySpan<char>, ache o ; com IndexOf, pegue o nome com [..pos] e converta a idade com int.Parse(texto[(pos + 1)..]).
string ficha = "Bruno;34";
// crie o ReadOnlySpan<char> e recorte a ficha
Console.WriteLine($"Ficha: {ficha}");
string ficha = "Bruno;34";
ReadOnlySpan<char> texto = ficha;
int pos = texto.IndexOf(';');
ReadOnlySpan<char> nome = texto[..pos];
int idade = int.Parse(texto[(pos + 1)..]);
Console.WriteLine($"Nome: {nome}");
Console.WriteLine($"Idade: {idade}");
3. Métodos que recebem ReadOnlySpan<T>
O melhor uso do span no dia a dia é como parâmetro. Um método que recebe ReadOnlySpan<int> aceita um array inteiro, um pedaço de array e até uma lista de valores escrita na hora, tudo sem cópia:
int[] notas = [7, 8, 10, 6, 9, 5];
Console.WriteLine(Soma(notas));
Console.WriteLine(Soma(notas.AsSpan(0, 3)));
Console.WriteLine(Soma([1, 2, 3]));
Console.WriteLine(notas.Total());
static int Soma(ReadOnlySpan<int> valores)
{
int total = 0;
foreach (int v in valores) total += v;
return total;
}
static class Extensoes
{
public static int Total(this ReadOnlySpan<int> valores)
{
int t = 0;
foreach (var v in valores) t += v;
return t;
}
}
$ dotnet run soma.cs 45 25 6 45
- Soma(notas)
- O array vira
ReadOnlySpan<int>sozinho: 7 + 8 + 10 + 6 + 9 + 5 = 45. - Soma(notas.AsSpan(0, 3))
- Só os três primeiros: 25. Sem o span, você precisaria de outro método com "início" e "quantidade", ou de uma cópia.
- Soma([1, 2, 3])
- Uma expressão de coleção direto no argumento.
- notas.Total()
- Um método de extensão escrito para
ReadOnlySpan<int>, chamado num array. Isso é novo no C# 14.
Spans "de primeira classe" C# 14
Até o C# 13, a conversão de array para span só acontecia em alguns lugares, como na chamada Soma(notas). O C# 14 a transformou numa conversão da própria linguagem, que vale também para quem recebe um método de extensão e para a inferência de tipos genéricos. No laboratório, o mesmo soma.cs num projeto com <LangVersion>13</LangVersion> no .csproj não compila:
$ dotnet run /home/ana/projetos/Spans/Program.cs(6,19): error CS1929: "int[]" não contém uma definição para "Total" e a melhor sobrecarga do método de extensão "Extensoes.Total(ReadOnlySpan<int>)" requer um receptor do tipo "System.ReadOnlySpan<int>" [/home/ana/projetos/Spans/Spans.csproj] Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Com o C# 14 (o padrão do .NET 10), compila e mostra os 45 da última linha. Na prática: você vai escrever menos .AsSpan(), e os métodos do .NET que recebem spans passam a ser escolhidos sozinhos para arrays.
Span ou ReadOnlySpan no parâmetro?
Se o método só lê os valores, use ReadOnlySpan<T>: ele aceita mais coisas (inclusive strings, no caso de char) e promete a quem chama que nada vai ser alterado. Use Span<T> só quando o método precisa escrever no trecho, como um Zerar(Span<int> s).
4. stackalloc: um array temporário sem lixo
Às vezes você precisa de um array pequeno só por um instante, dentro de um método. Com stackalloc ("alocar na pilha"), ele é criado num lugar de memória que é liberado sozinho quando o método termina, sem trabalho para o coletor de lixo. O resultado é um Span:
Span<int> contagem = stackalloc int[3];
string votos = "ABACABBA";
foreach (char v in votos) contagem[v - 'A']++;
Console.WriteLine($"A: {contagem[0]}, B: {contagem[1]}, C: {contagem[2]}");
$ dotnet run votos.cs A: 4, B: 3, C: 1
O v - 'A' transforma a letra em posição ('A' vira 0, 'B' vira 1…), porque um char é um número por dentro (veja char e bool). Use stackalloc só para tamanhos pequenos e conhecidos (dezenas ou centenas de itens): a pilha é pequena, e um tamanho grande derruba o programa.
5. As regras do span: um ref struct
Uma janela só faz sentido enquanto aquilo que ela mostra existe. Um span sobre memória de stackalloc, por exemplo, ficaria apontando para o nada depois que o método termina. Para garantir que isso nunca aconteça, Span<T> é um tipo especial, um ref struct, que só pode viver em variáveis locais e parâmetros. O compilador proíbe tudo que o faria durar mais:
| Proibido | Erro |
|---|---|
Ser campo ou propriedade de uma class (ou de um struct comum) | CS8345 |
| Ser usado dentro de uma lambda | CS8175 |
Ser guardado como object | CS0029 |
Ser item de uma coleção, como List<Span<int>> | CS9244 |
Ser usado depois de um await num método async | CS4007: A instância do tipo "System.Span<int>" não pode ser preservada entre o limite "await" ou "yield". |
Precisa guardar um pedaço por mais tempo, num campo ou numa lista? Use Memory<T>, o "irmão de longa duração" do span (vendas.AsMemory(1, 3)), ou simplesmente um array.
Qual usar?
| Você quer… | Use |
|---|---|
| Um pedaço de array para mexer agora, sem copiar | Span<T> (AsSpan, Slice, [a..b]) |
| Recortar e converter partes de um texto | ReadOnlySpan<char> + IndexOf + int.Parse(span) |
Um parâmetro que aceite array, pedaço ou [1, 2, 3] | ReadOnlySpan<T> (ou Span<T> para escrever) |
| Um array pequeno e temporário | Span<T> s = stackalloc T[n]; |
Guardar o pedaço num campo, numa lista ou atravessar await | Memory<T> ou um array |
| Uma cópia independente | array[a..b] ou span.ToArray() |
| Código simples, sem dados enormes | Arrays, List, Split e Substring continuam ótimos. Span é otimização. |
Quando dá errado
Tentar alterar um ReadOnlySpan
ReadOnlySpan<char> nome = "Ana"; nome[0] = 'E';
$ dotnet run leitura.cs /home/ana/cs/leitura.cs(2,1): error CS8331: Não é possível atribuir a propriedade 'this' ou usá-lo como o lado direito de uma atribuição ref porque é uma variável somente leitura Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: a mensagem é confusa (fala de 'this' porque nome[0] é um indexador do próprio span), mas o recado é "isto é somente leitura". Uma string nunca muda. Como consertar: para montar um texto diferente, crie outro: "E" + nome[1..].ToString(), ou use um char[] (que aceita Span<char>) e depois new string(array).
Span como campo de uma classe
class Caixa
{
public Span<int> Parte;
}
$ dotnet run campo.cs /home/ana/cs/campo.cs(3,12): error CS8345: O campo ou a propriedade autoimplementada não pode ser do tipo 'Span<int>', a menos que seja um membro de instância de uma struct de referência. Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: um objeto de classe pode durar o programa inteiro, e o span não pode (seção 5). "Struct de referência" é a tradução de ref struct. Como consertar: guarde um Memory<int>, ou o array junto com o início e o tamanho, e crie o span só na hora de usar.
Span dentro de uma lambda
Span<int> s = [1, 2, 3]; Func<int> f = () => s[0]; Console.WriteLine(f());
$ dotnet run lambda.cs /home/ana/cs/lambda.cs(2,21): error CS8175: Não é possível usar a referência local 's' em um método anônimo, expressão lambda ou expressão de consulta Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: uma lambda pode ser guardada e chamada muito depois, quando a janela talvez já não valha. Por isso também não dá para usar span dentro de consultas LINQ com lambdas. Como consertar: copie o valor necessário para uma variável comum antes (int primeiro = s[0];) e use-a na lambda, ou troque o span por um array.
Guardar o span como object
Span<int> s = [1, 2, 3]; object o = s;
$ dotnet run objeto.cs /home/ana/cs/objeto.cs(2,12): error CS0029: Não é possível converter implicitamente tipo "System.Span<int>" em "object" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: guardar um struct como object o coloca numa "caixa" na memória de longa duração (o boxing), e o span não pode ir para lá. Como consertar: use o span como span, ou converta antes com s.ToArray().
Posição fora da janela
int[] numeros = [1, 2, 3]; Span<int> s = numeros; Console.WriteLine(s[3]);
$ dotnet run fora.cs Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array. at Program.<Main>$(String[] args) in /home/ana/cs/fora.cs:line 3
O que quer dizer: a mesma exceção de um array. O span confere os limites: ele é rápido, mas não é inseguro. Como consertar: as posições vão de 0 a s.Length - 1.
Referência
| Forma | Exemplo |
|---|---|
| Janela sobre array | Span<int> s = arr;, arr.AsSpan(), arr.AsSpan(1, 3) |
| Janela sobre texto | ReadOnlySpan<char> t = texto;, texto.AsSpan(0, 3) |
| Fatiar | s.Slice(1, 3), s[1..4], s[^2..] |
| Ler / escrever | s[0], s[0] = 5; (só Span) |
| Tamanho | s.Length, s.IsEmpty |
| Procurar | s.IndexOf(x), s.Contains(x), t.StartsWith("Rec") |
| Preencher, ordenar | s.Fill(0), s.Sort(), s.Reverse() |
| Copiar para fora | s.ToArray(), t.ToString(), s.CopyTo(destino) |
| Converter texto | int.Parse(t), int.TryParse(t, out var n) |
| Temporário | Span<int> s = stackalloc int[10]; |
| De longa duração | Memory<int> m = arr.AsMemory(1, 3); e m.Span na hora de usar |
Exercícios
1. Complete para criar uma janela sobre 2 itens de vendas, a partir da posição 4:
2. Qual tipo de janela se usa sobre uma string?
3. Que método do span faz uma cópia num array novo?
4. int[] a = [1, 2, 3]; Span<int> s = a.AsSpan(1, 2); s[0] = 9;. Quanto vale a[1]?
5. E com int[] b = a[1..3]; b[0] = 9;?
6. Por que class Caixa { Span<int> parte; } não compila?
Missão final: escreva o método static double Media(ReadOnlySpan<double> valores) e use-o três vezes: com as 3 primeiras notas (notas.AsSpan(0, 3)), com as 3 últimas e com o array inteiro. A saída deve ser 1º semestre: 8, 2º semestre: 7 e Ano: 7,5.
double[] notas = [7, 8, 9, 6, 5, 10];
// Console.WriteLine($"1º semestre: {Media(notas.AsSpan(0, 3))}");
// Console.WriteLine($"2º semestre: {Media(notas.AsSpan(3, 3))}");
// Console.WriteLine($"Ano: {Media(notas)}");
// escreva aqui: static double Media(ReadOnlySpan<double> valores) { ... }
double[] notas = [7, 8, 9, 6, 5, 10];
Console.WriteLine($"1º semestre: {Media(notas.AsSpan(0, 3))}");
Console.WriteLine($"2º semestre: {Media(notas.AsSpan(3, 3))}");
Console.WriteLine($"Ano: {Media(notas)}");
static double Media(ReadOnlySpan<double> valores)
{
double soma = 0;
foreach (double v in valores) soma += v;
return soma / valores.Length;
}
A solução, na máquina da Ana. Um único método serviu para os dois semestres e para o ano, sem copiar nenhuma nota:
$ dotnet run semestres.cs 1º semestre: 8 2º semestre: 7 Ano: 7,5
Resumo
Span<T>é uma janela sobre um trecho de memória (array,stackalloc): guarda onde começa e quantos itens tem. Nada é copiado, e gravar na janela grava no original.- Crie com
AsSpan(início, quantos),Sliceou intervalos (s[1..4]). Num array,a[1..4]copia; num span, não. ReadOnlySpan<char>recorta textos semSubstring, eint.Parseaceita span. Tentar gravar dáCS8331.- Parâmetros
ReadOnlySpan<T>aceitam arrays, pedaços e[1, 2, 3]. No C# 14, a conversão array → span vale também para métodos de extensão (em C# 13:CS1929). - Span é um
ref struct: não pode ser campo de classe (CS8345), ir para lambda (CS8175) nem virarobject(CS0029). Para guardar, useMemory<T>. - Span é otimização: use quando há muitos dados ou muitas cópias. No código simples, arrays e strings continuam ótimos.
Próxima aula: Novidades do C# 8 ao 14, um passeio pelos recursos que o C# ganhou em cada versão.