Span dan Memory

Pemotongan tanpa alokasi dan struct ref.

Daftar isi

Topik ini membahas pemotongan data tanpa alokasi baru menggunakan tipe Span<T> dan Memory<T>, beserta aturan cakupan untuk ref struct (struct bertanda ref). Cara pemotongan yang biasa menyalin data ke memori baru, sedangkan kedua tipe tersebut hanya menunjuk memori yang sudah ada. Setelah topik ini diselesaikan, potongan array dan string dapat dibuat tanpa menimbulkan biaya alokasi, perubahan lewat potongan dapat dilacak ke array asalnya, dan larangan pada Span<T> dapat dibaca dari pesan error kompilatornya.

Pemotongan tanpa alokasi#

Cara paling umum mengambil sebagian data adalah menyalinnya ke memori baru. Metode Substring pada string dan penyalinan array menghasilkan objek baru di heap (area memori untuk objek), sehingga setiap pemanggilannya menimbulkan alokasi. Pada kode yang berjalan sekali, biaya itu kecil, tetapi pada pemrosesan data yang berulang, alokasi kecil yang terus menerus membebani kerja garbage collector (GC). Kompilator dan pustaka .NET menyediakan tipe Span<T> untuk jalan keluarnya. Span<T> adalah struct ringan yang hanya menyimpan alamat awal memori dan panjangnya, sedangkan isinya tetap berada pada array atau string asal. Metode Slice serta pengindeksan rentang menghasilkan slice (potongan), yaitu jendela pandang atas memori yang sama tanpa satu byte pun disalin.

Program berikut memotong array dan string dengan cara tersebut, lalu menunjukkan bahwa penulisan lewat potongan mengubah array asalnya.

csharp Program.cs
// Pemotongan tanpa alokasi: slice berbagi memori dengan array asalnya.
int[] suhu = [28, 30, 32, 31, 29, 27];

Span<int> jendela = suhu.AsSpan(1, 4);
Console.WriteLine($"Panjang array asal : {suhu.Length}");
Console.WriteLine($"Panjang slice      : {jendela.Length}");
Console.WriteLine($"Elemen slice       : {jendela[0]}, {jendela[1]}, {jendela[2]}, {jendela[3]}");

jendela[0] = 40; // penulisan lewat slice mengubah memori asalnya
Console.WriteLine($"Array asal sesudah : {string.Join(", ", suhu)}");

string catatan = "log-suhu-2026-11-05";
ReadOnlySpan<char> bagian = catatan.AsSpan(4, 4);
Console.WriteLine($"Potongan string    : {bagian.ToString()}");

Statistik statistik = new Statistik(suhu);
Console.WriteLine($"Jumlah via ref struct: {statistik.Jumlah()}");

// ref struct: struct yang wajib hidup di stack sehingga tidak dapat disimpan di heap.
readonly ref struct Statistik
{
    private readonly ReadOnlySpan<int> data;

    public Statistik(ReadOnlySpan<int> data)
    {
        this.data = data;
    }

    public int Jumlah()
    {
        int total = 0;
        foreach (int nilai in data)
        {
            total += nilai;
        }
        return total;
    }
}
text Keluaran
Panjang array asal : 6
Panjang slice      : 4
Elemen slice       : 30, 32, 31, 29
Array asal sesudah : 28, 40, 32, 31, 29, 27
Potongan string    : suhu
Jumlah via ref struct: 187

Pemanggilan suhu.AsSpan(1, 4) menghasilkan potongan yang mencakup empat elemen mulai dari indeks 1, yaitu nilai 30, 32, 31, dan 29. Penulisan jendela[0] = 40 tidak menyentuh salinan mana pun, melainkan menulis langsung ke elemen indeks 1 pada array suhu, sehingga baris berikutnya menampilkan 28, 40, 32, 31, 29, 27. Inilah akibat berbagi memori: satu penulisan terlihat dari dua sisi. Pada bagian string, catatan.AsSpan(4, 4) menghasilkan ReadOnlySpan<char>, yaitu potongan string yang hanya dapat dibaca, dan pemanggilan ToString() pada akhirnya hanya ada untuk menampilkan potongan itu ke layar. Baris terakhir memanggil struct Statistik yang dihitung langsung dari span tanpa satu alokasi pun, dan pemanggilan string.Join serta ToString yang mengalokasikan string adalah biaya penampilan keluaran, bukan biaya pemotongannya.

Batasan ref struct pada Span#

Span<T> adalah contoh ref struct, yaitu struct yang wajib hidup di stack (tumpukan memori panggilan) dan tidak boleh berpindah ke heap. Aturan ini ditetapkan karena sebuah span dapat menunjuk memori yang hidup di stack, misalnya memori dari stackalloc, sehingga runtime melarang segala pola yang membuat span berumur lebih panjang dari cakupan variabelnya. Tipe Statistik pada Program.cs di atas memanfaatkan aturan itu dengan sengaja: struct tersebut dideklarasikan readonly ref struct, menyimpan ReadOnlySpan<int> pada field-nya, dan menghitung jumlah data tanpa mengalokasikan apa pun.

Larangan tersebut muncul sebagai error kompilator ketika span dipakai di tempat yang berumur lebih panjang. Dua pola yang paling sering ditemui adalah field kelas biasa dan variabel span yang melintasi batas await pada metode async.

csharp Contoh: error CS8345 pada field kelas biasa
// tidak-dijalankan
class Wadah
{
    // error CS8345: field bertipe Span<T> tidak boleh ada di kelas biasa
    private Span<int> data;
}
csharp Contoh: error CS4007 saat span melintasi batas await
// tidak-dijalankan
async Task Kerja()
{
    Span<int> data = stackalloc int[4];
    await Task.Delay(1); // error CS4007: span tidak boleh melintasi batas await
    Console.WriteLine(data.Length);
}

Pesan error CS8345 menyatakan bahwa field atau properti tidak boleh bertipe Span<T> kecuali berada di dalam ref struct, sedangkan CS4007 menyatakan bahwa instance Span<T> tidak dapat dipertahankan melintasi batas await atau yield. Kedua pesan itu bukan batasan teknis yang sembarangan, melainkan pelindung: span yang berumur lebih panjang dari memori yang ditunjuknya akan menunjuk data yang sudah tidak sah.

Memory untuk kebutuhan di luar stack#

Span<T> cocok untuk pemrosesan seketika, tetapi status ref struct menutup beberapa pintu, misalnya field kelas dan metode async. Memory<T> mengisi celah itu. Tipe ini juga hanya menyimpan alamat dan panjang tanpa menyalin isi, tetapi bukan ref struct, sehingga boleh disimpan pada field, dikirim antar metode, dan digunakan di dalam metode async. Untuk membaca atau menulis isinya, properti Span menghasilkan span dari memori tersebut, dan pemanggilan Slice memangkasnya dengan cara yang sama seperti pada span.

csharp Contoh: Memory<T> dan Slice
// Memory<T> menyimpan alamat dan panjang, lalu Slice memangkasnya tanpa menyalin isi.
int[] stok = [12, 7, 4, 9, 15];
Memory<int> memori = stok.AsMemory();

Memory<int> potongan = memori.Slice(1, 3);
Span<int> jendela = potongan.Span;
jendela[1] = 40;

Console.WriteLine($"Panjang potongan : {potongan.Length}");
Console.WriteLine($"Array asal       : {string.Join(", ", stok)}");
text Keluaran
Panjang potongan : 3
Array asal       : 12, 7, 40, 9, 15

Potongan Memory<int> mencakup elemen indeks 1 sampai 3, kemudian jendela[1] menulis elemen indeks 2 pada array stok, yaitu nilai 4 menjadi 40. Sama seperti sebelumnya, satu penulisan terlihat dari dua sisi karena tidak ada salinan di antaranya.

Kesalahan umum#

  • Mengira slice menyalin data. Penulisan lewat span mengubah memori asalnya, sehingga kode lain yang membaca array asal melihat perubahan itu. Apabila salinan yang bebas memang diinginkan, panggil ToArray() untuk membuat array baru.
  • Mengira indeks span mengikuti indeks array asal. Pada contoh array suhu, jendela[1] menunjuk suhu[2], karena indeks span dimulai dari nol di awal potongannya. Hitung ulang indeksnya atau kembali ke array asal ketika indeks global dibutuhkan.
  • Menyimpan span di kelas atau mengirimnya ke metode async. Kompilator menolak pola itu dengan error CS8345 atau CS4007. Perbaikannya dengan mengganti tipe field atau variabel menjadi Memory<T>, lalu mengambil span dari properti Span hanya di titik pemrosesan.

Ringkasan#

  • Span<T> dan Memory<T> hanya menyimpan alamat dan panjang, sehingga Slice serta pengindeksan rentang memotong data tanpa alokasi.
  • Penulisan lewat span mengubah memori asalnya, sedangkan ReadOnlySpan<char> menyediakan potongan string yang hanya dapat dibaca.
  • Span<T> adalah ref struct yang wajib hidup di stack, sehingga dilarang menjadi field kelas biasa (error CS8345) dan tidak boleh melintasi batas await (error CS4007).
  • Memory<T> bukan ref struct, sehingga dapat menyimpan potongan pada field dan metode async, dan properti Span menghasilkan span dari memori tersebut.

Referensi#