1. Einführung
Stell dir vor, du hast ein großes Datenarray, zum Beispiel aus einer Datei gelesene Bytes. Du möchtest einen Teil davon verarbeiten — einen Abschnitt als String lesen oder einfach einen Datenbereich an eine Methode übergeben. Im "alten" Stil hättest du diese Bytes in ein neues Array kopiert — das ist langsam und verbraucht Speicher. Und wenn es Dutzende oder Hunderte solcher Abschnitte gibt?
Hier kommt der Held ins Spiel — Span<T>. Das ist eine spezielle Struktur, die einen Slice (view) über ein Array (oder jeden zusammenhängenden Speicherblock) beschreibt, ohne die Daten zu kopieren, sondern einfach auf den entsprechenden Bereich zeigt. Wichtig: Span<T> ist keine neue Collection, sondern ein sicheres "Fenster" in ein bestehendes Array!
Hauptmerkmale und Einschränkungen von Span<T>
- Span<T> ist eine Struktur (value type), die ein "Fenster" auf Speicher darstellt und Elemente nicht kopiert.
- Zeigt nur auf zusammenhängenden Speicher: Array, Teil eines Arrays, stackalloc-Block, String-Speicher über spezielle Methoden oder Speicher in unsafe-Code.
- Span<T> lebt auf dem Stack. Du kannst ihn nicht in einem Klassenfeld speichern oder direkt aus async-Methoden zurückgeben.
- Garantiert Type-Safety: Zugriff auf Speicher ohne manuelles Pointer-Handling (sofern du nicht unsafe verwendest).
Kurz zu den neuen Möglichkeiten in C# 14
C# 14 erweitert die Arbeit mit Slices: bequeme Ranges .. und Endindizes ^1, verbesserte Pattern-Matching-Patterns für Arrays und Slices und weiteren "syntaktischen Zucker". Dazu kommen wir am Ende zurück.
2. Wie erstellt man Span<T>?
Beginnen wir mit einem einfachen Array und erstellen einen Span:
int[] numbers = { 10, 20, 30, 40, 50, 60 };
Span<int> midSpan = new Span<int>(numbers, 2, 3); // ab dem 3. Element (Index 2) 3 Elemente nehmen
Jetzt zeigt midSpan auf die Elemente 30, 40, 50 — das ist keine Kopie, sondern ein Blick auf die "lebenden" Array-Elemente. Wenn du sie über den Span änderst, änderst du das Originalarray.
midSpan[0] = 100;
Console.WriteLine(numbers[2]); // Gibt 100 aus
Warum nicht direkt Array-Slices verwenden?
Klassisches LINQ-Slicing erzeugt ein neues Array:
int[] slice = numbers.Skip(2).Take(3).ToArray(); // <-- hier wird eine Kopie erstellt!
Das ist zeit- und speicherintensiv. Span löst das Problem überflüssiger Kopien — und funktioniert nicht nur mit Zahlen.
3. Noch mehr Wege, Span<T> zu erstellen
Existierendes Array
Span<int> mySpan = numbers; // Implizite Konvertierung vom Array zu Span
Teil eines Arrays
int[] numbers = {10, 20, 30, 40, 50, 60};
Span<int> part = numbers.AsSpan(1, 4); // 4 Elemente ab Index 1: {20, 30, 40, 50}
Stack-Speicher (stackalloc)
Span<T> erlaubt das Allozieren von Arrays auf dem Stack:
Span<byte> buffer = stackalloc byte[128];
for (int i = 0; i < buffer.Length; i++)
buffer[i] = (byte)i;
Stack-Speicher ist schnell und wird automatisch beim Verlassen der Methode freigegeben. Die Größe sollte jedoch vernünftig sein — Megabytes auf dem Stack sind nicht akzeptabel.
Strings und ReadOnlySpan<char>
Strings in .NET sind immutable, daher verwenden wir ReadOnlySpan<char>:
string greeting = "Hello, C# world!";
ReadOnlySpan<char> span = greeting.AsSpan(7, 8); // "C# world"
Console.WriteLine(span.ToString()); // C# world
4. Lass uns ein Beispiel zusammenbauen!
using System;
class Program
{
static void Main()
{
int[] orderTotals = { 100, 200, 300, 400, 500, 600, 700 };
Console.WriteLine("Die gesamte Bestellhistorie: ");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
Console.WriteLine("Bestellungen von 2 bis 4 anzeigen (Indizes 1-3):");
Span<int> recentOrders = orderTotals.AsSpan(1, 3);
foreach (int t in recentOrders)
Console.Write(t + " ");
Console.WriteLine();
// Daten über Span mutieren
recentOrders[1] = 999;
Console.WriteLine("Nach der Änderung über Span:");
foreach (int total in orderTotals)
Console.Write(total + " ");
Console.WriteLine();
}
}
Der Slice recentOrders arbeitet tatsächlich direkt mit dem Array — es ist keine Kopie.
5. Sicherheit, Performance und Checks
- Sparsamer Umgang mit Speicher: keine unnötigen Kopien.
- Bounds-Checks schützen vor Zugriffen außerhalb des Arrays.
- JIT-Optimierungen sorgen für sehr schnellen Zugriff (ohne unsafe).
Interaktion mit Methoden
Methoden können Span<T> oder ReadOnlySpan<T> akzeptieren. Wenn du die Daten nicht verändern willst — benutze ReadOnlySpan<T>.
static int Sum(Span<int> slice)
{
int sum = 0;
foreach (var item in slice)
sum += item;
return sum;
}
int[] data = { 1, 2, 3, 4, 5, 6, 7 };
Console.WriteLine(Sum(data.AsSpan(2, 3))); // 3+4+5=12
6. Verwendung von Ranges (range)
Seit C# 8 gibt es Ranges .. und Endindizes ^ — sie funktionieren hervorragend mit Slices.
int[] arr = { 10, 20, 30, 40, 50, 60 };
Span<int> span = arr[2..5]; // Indizes 2,3,4 - also 30, 40, 50
Bei Ranges ist der Startindex inklusiv, der Endindex exklusiv.
Endindizes
int lastElement = arr[^1]; // letztes Element (60)
Span<int> lastTwo = arr[^2..]; // die letzten zwei Elemente (50, 60)
Ranges und Strings
string code = "SpanMagic!";
var mid = code[4..9]; // "Magic"
Dieser Slice ist ein ReadOnlySpan<char>; um einen String zu bekommen, rufe ToString() auf.
7. Moderne Patterns für die Arbeit mit Slices (C# 14)
Neue Patterns erleichtern das Zerlegen von Arrays und Slices.
if (arr is [10, 20, .. var rest]) // .. fängt den "Tail" des Arrays ein
{
Console.WriteLine("Anfang passt, Tail:");
foreach (var x in rest)
Console.WriteLine(x);
}
if (arr is [.., 50, 60])
Console.WriteLine("Array endet mit 50, 60");
Vergleich: Array, ArraySegment und Span
| Typ | Mutierbar | Kopiert Daten? | Auf dem Stack möglich | Klasse/Struktur | Felder in Klassen möglich |
|---|---|---|---|---|---|
|
ja | - | nein | Klasse | ja |
|
ja | nein | nein | Struktur | ja |
|
ja | nein | ja | Struktur | nein |
|
nein | nein | ja | Struktur | nein |
Mit anderen Worten: Span<T> ist die Evolution von ArraySegment<T>: performanter und sicherer.
8. Typische Fehler und Fallstricke
Der Versuch, ein Span<T> in einem Klassenfeld zu speichern. Das geht nicht: Felder leben auf dem Heap, aber Span<T> muss auf dem Stack leben. Verwende ArraySegment<T> oder Indizes zur Adressierung.
Zurückgeben/Speichern eines Span<T> aus async-Methoden. Das ist nicht erlaubt: Asynchronität bricht den Stack. Gib die Daten anders weiter — zum Beispiel als Array, Memory<T>/ReadOnlyMemory<T>.
Falscher Range bei der Erstellung eines Slices. Das Überschreiten der Grenzen führt zur Laufzeit-Ausnahme. Prüfe stets Länge und Grenzen bevor du einen Slice bildest.
Vergessen, dass der Span das Original widerspiegelt. Jede Änderung über Span<T> ändert das Ausgangsarray. Wenn du eine unabhängige Kopie brauchst — kopiere die Daten explizit.
GO TO FULL VERSION