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.

Você vai aprender
  • O que é uma janela (Span<T>) e como criá-la com AsSpan, Slice e 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 sem Substring, e o erro CS8331 ao 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.
  • stackalloc e 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 em List<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 99 na 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 inteirovendas.AsSpan() ou Span<int> s = vendas;
A partir de uma posição, com N itensvendas.AsSpan(1, 3) ou s.Slice(1, 3) (slice = fatia)
Com intervalos[1..4], s[^2..], s[..3]
Com valores fixos C# 12Span<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
LinhaO 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:

ProibidoErro
Ser campo ou propriedade de uma class (ou de um struct comum)CS8345
Ser usado dentro de uma lambdaCS8175
Ser guardado como objectCS0029
Ser item de uma coleção, como List<Span<int>>CS9244
Ser usado depois de um await num método asyncCS4007: 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 copiarSpan<T> (AsSpan, Slice, [a..b])
Recortar e converter partes de um textoReadOnlySpan<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árioSpan<T> s = stackalloc T[n];
Guardar o pedaço num campo, numa lista ou atravessar awaitMemory<T> ou um array
Uma cópia independentearray[a..b] ou span.ToArray()
Código simples, sem dados enormesArrays, List, Split e Substring continuam ótimos. Span é otimização.

Quando dá errado

Tentar alterar um ReadOnlySpan

Este código tem um erro de propósito.
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

Este código tem um erro de propósito.
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

Este código tem um erro de propósito.
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

Este código tem um erro de propósito.
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

FormaExemplo
Janela sobre arraySpan<int> s = arr;, arr.AsSpan(), arr.AsSpan(1, 3)
Janela sobre textoReadOnlySpan<char> t = texto;, texto.AsSpan(0, 3)
Fatiars.Slice(1, 3), s[1..4], s[^2..]
Ler / escrevers[0], s[0] = 5; (só Span)
Tamanhos.Length, s.IsEmpty
Procurars.IndexOf(x), s.Contains(x), t.StartsWith("Rec")
Preencher, ordenars.Fill(0), s.Sort(), s.Reverse()
Copiar para foras.ToArray(), t.ToString(), s.CopyTo(destino)
Converter textoint.Parse(t), int.TryParse(t, out var n)
TemporárioSpan<int> s = stackalloc int[10];
De longa duraçãoMemory<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:

Span<int> s = vendas.;

2. Qual tipo de janela se usa sobre uma string?

t = "Ana;29";

3. Que método do span faz uma cópia num array novo?

int[] copia = s.;

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), Slice ou intervalos (s[1..4]). Num array, a[1..4] copia; num span, não.
  • ReadOnlySpan<char> recorta textos sem Substring, e int.Parse aceita 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 virar object (CS0029). Para guardar, use Memory<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.