1. Einführung
Wenn du mit Arrays, Strings und Byte-Buffern arbeitest, kommt oft die Aufgabe auf, einen Teil dieser Daten „anzuschauen“. Zum Beispiel eine Substring zu nehmen, einen Array-Slice zu holen oder einen Teil eines eingehenden Streams zu verarbeiten. In älteren .NET-Versionen musste man dafür entweder die Daten kopieren (neues Array/Substring erzeugen) oder man schrieb Code, der das Array vom gewünschten Index bis zum Ende durchläuft. Das ist weder schön für Performance noch für Lesbarkeit.
Hier ein Beispiel des alten Ansatzes: wir müssen nur einen Teil eines großen Arrays an eine Methode übergeben:
// Alter Ansatz — wir kopieren einen Teil des Arrays (ineffizient!)
int[] source = new int[] { 1, 2, 3, 4, 5, 6, 7, 8 };
int[] subArray = source.Skip(2).Take(4).ToArray(); // es wird ein neues Array erstellt
Wenn man also effizient einen „Slice“ eines Arrays (oder sogar eines Stücks eines Strings) übergeben möchte, ohne unnötige Objekte zu erzeugen, sind die alten Mittel von C# klar im Nachteil — vor allem bei großen Datenmengen.
Und hier kommt der Held des Tages — Span<T>!
2. Was ist Span<T>? Grundidee
Span<T> ist ein Typ, der einen zusammenhängenden Speicherbereich eines Typs T repräsentiert. Sein Ziel ist es, dir einen schnellen, sicheren und effizienten Weg zu geben, um mit Teilen von Arrays, Strings, Strukturen und sogar unmanaged Speicher (z. B. Speicher außerhalb der verwalteten .NET-Umgebung) zu arbeiten.
Die Hauptsache bei Span<T> ist das „Slicing ohne neue Arrays zu erstellen“. Stell dir ein Lineal vor, mit dem du beliebige Abschnitte desselben Arrays messen kannst, ohne Daten zu kopieren und mit minimaler Gefahr, bei Indizes Fehler zu machen.
Kurz:
- Span<T> — ein „Window“ oder „View“ auf einen Speicherabschnitt, mit dem man bequem und sicher arbeiten kann.
- Keine neue Allokation — Ressourcen sparen und weniger Arbeit für die GC.
- Funktioniert nicht nur mit Arrays, sondern auch mit Teilen von Strings, stackalloc-Blöcken und sogar mit unmanaged Memory.
- Kann nicht in normalen Klassenfeldern gespeichert werden: es ist ein stack-only struct.
Warum ist das wichtig?
Bei High-Performance-Aufgaben (Dateiparsing, Verarbeitung großer Buffer, Kryptographie, Serialisierung) können bereits wenige Kopien von Arrays einen großen Performanceverlust bedeuten und die Garbage Collection stark belasten. Außerdem zeigst du deinen Kolleg:innen, dass du modernes C# und .NET im Griff hast!
3. Basisnutzung von Span<T>: erster Slice
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Erstelle ein Span für einen Teil des Arrays (z. B. Elemente von 2 bis 5 inkl.)
Span<int> middle = new Span<int>(numbers, 2, 4); // Indizes: 2, 3, 4, 5
// Wir sehen das Subarray: 3, 4, 5, 6
Console.WriteLine(string.Join(", ", middle.ToArray())); // 3, 4, 5, 6
// Änderung am Span ändert das Original-Array!
middle[1] = 999;
Console.WriteLine(numbers[3]); // 999
Wichtig! Span<T> kopiert die Daten nicht, sondern zeigt nur auf einen „Teil“ des Arrays. Alle Änderungen sind sowohl im ursprünglichen Array als auch im Span sichtbar.
4. Hauptwege, ein Span<T> zu erstellen
Auf Basis eines Arrays:
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> span = arr; // volle Länge
Span<int> slice = arr.AsSpan(1, 3); // Elemente 20, 30, 40
Auf Basis eines Array-Teils:
Span<int> part = new Span<int>(arr, 2, 2); // Elemente 30, 40
stackalloc: Speicher auf dem Stack reservieren (sehr schnell und landet nicht auf dem Heap):
Span<byte> buffer = stackalloc byte[128];
buffer[0] = 42;
Mit der Methode .Slice():
Span<int> subSpan = span.Slice(1, 2); // Elemente 20, 30
Visuelle Darstellung eines „Slices“
Ursprüngliches Array: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
<--- Span: 3, 4, 5, 6 --->
5. Einschränkungen und Besonderheiten von Span<T>
- Stack-only! Man kann es nicht in normalen Klassenfeldern speichern oder als Teil einer Closure — es ist ein stack-basierter Typ.
- Kann nicht als Feld einer Klasse verwendet oder aus async-Methoden zurückgegeben werden (der Compiler wirft einen Fehler).
- Kann nicht in Lambdas/anonymous methods captured werden — benutze es „hier und jetzt“.
- Kann nicht direkt serialisiert oder zwischen Threads übergeben werden.
Das liegt daran, dass Span auf beliebigen Speicherbereichen zeigen kann, und wenn er plötzlich in den Heap „umziehen“ würde, könnten unsichere Zustände entstehen.
6. Immutability: ReadOnlySpan<T>
Manchmal willst du nur auf einen Speicherbereich schauen, aber ihn nicht verändern. Dafür gibt es die unveränderliche Variante — ReadOnlySpan<T>.
string text = "Hello, Span!";
ReadOnlySpan<char> letters = text.AsSpan(7, 4); // 'S', 'p', 'a', 'n'
Console.WriteLine(string.Join(", ", letters.ToArray())); // S, p, a, n
// letters[0] = 'Z'; // Fehler: Indexer ist read-only!
Typisches Szenario — sichere Übergabe eines „Stücks“ eines Strings oder Arrays an Code, der es nicht ändern darf (und soll).
7. Praxisbeispiel: Arrays und Strings „slicen“
Angenommen, das ist ein Daten-Analyzer, der aus einem langen String eine Substring extrahiert, darin Zahlen sucht und deren Summe zurückgibt (ohne unnötige Kopien beim anfänglichen Slicing):
using System;
class Program
{
static void Main()
{
// Angenommen, der Benutzer hat eine lange String mit Zahlen eingegeben, getrennt durch Leerzeichen
string input = "12 34 56 78 90 123 456 789";
// Wir wollen die Summe der Zahlen nur aus der "Mitte" berechnen, z. B. 56 78 90
// Wir nehmen eine Substring (kopieren sie aber nicht!)
ReadOnlySpan<char> center = input.AsSpan(6, 8); // Indizes können dynamisch berechnet werden
// Zahlen mit Split parsen (erzeugt temporäres Array)
string[] numbers = center.ToString().Split(' ');
int sum = 0;
foreach (var str in numbers)
{
if (int.TryParse(str, out int num))
sum += num;
}
Console.WriteLine($"Summe der zentralen Zahlen: {sum}");
}
}
Moderne Parsing-Bibliotheken für CSV und JSON nutzen Span für hohe Performance bei großen String-Mengen — jetzt wisst ihr, worauf deren "Magie" beruht.
8. Nützliche Feinheiten
Span vs. Array-Kopieren
// Alter Weg: wir kopieren ein Stück des Arrays
int[] arr = Enumerable.Range(0, 1000000).ToArray();
int[] firstThousand = arr.Take(1000).ToArray(); // neues Array mit 1000 Elementen erstellt
// Neuer Weg: Span
Span<int> bestThousand = arr.AsSpan(0, 1000); // gar kein Kopieren!
bestThousand[0] = 42; // ändert auch arr
Der Unterschied ist besonders sichtbar bei intensivem Datei-Parsing, Verarbeitung von Netzwerk-Buffern oder Arbeit mit Binärdaten.
Anwendung in realen Aufgaben: warum man Span kennen sollte
- High-Performance-Parsen und Verarbeitung von String-/Binary-Daten. Moderne Serialisierungsbibliotheken (z. B. System.Text.Json, Span in der Microsoft-Dokumentation) nutzen Span, um schneller zu arbeiten.
- Buffering und File-IO (große Buffer ohne Kopien zerlegen).
- Arbeiten mit begrenztem Speicher (embedded, IoT) — offizielle Dokumentation zu Memory/Spans.
- Algorithmen für Bild- und Audioverarbeitung, wo Geschwindigkeit und keine unnötigen Allokationen wichtig sind.
- Beschleunigung des Parsings von CSV, JSON, XML mit Hilfe von Span — besonders in .NET 8/9.
In Interviews tauchen Fragen zu Span auf, seit es in .NET Core 2.1+ eingeführt wurde, und in .NET 9 wird Wissen darüber immer häufiger erwartet.
Visuelle Darstellung: wo Span vs. Array
+--------------------+
| int[] Array |
| 1 2 3 4 5 6 7 8 |
+--------------------+
^ ^
| |
[ 2, 3, 4, 5 ] <-- Span<int> "Memory-Window" (Slice)
Span<T> ist kein separates Array, sondern eine „durchsichtige Linse“ auf einen Teil der Daten.
Unterschied zu anderen Collections: Vergleich
| Typ | Speichert Daten? | Kann Elemente ändern? | Kann Größe ändern? | Kopiert beim Slicing? | Wo lebt es? |
|---|---|---|---|---|---|
|
Ja | Ja | Nein | Ja (durch .Take) | Heap |
|
Ja | Ja | Ja | Ja | Heap |
|
Nein | Ja | Nein | Nein | Stack |
|
Nein | Nein | Nein | Nein | Stack |
9. Typische Fehler bei der Arbeit mit Span/ReadOnlySpan
Fehler Nr. 1: Versuch, ein Span als Feld einer Klasse zu speichern. Der Compiler meldet „Span type may not be used in this context“. Das ist beabsichtigt: Span in einem Feld zu speichern ist unsicher.
Fehler Nr. 2: Ein Span aus einer async-Methode zurückgeben. Das geht nicht, weil async-Methoden auf den Heap „laufen“ können. Verwende stattdessen ein Array oder einen anderen Typ.
Fehler Nr. 3: Vergessen, dass Änderungen über Span das ursprüngliche Array ändern. Das kann unerwartet Daten „von außen“ verändern und zu schwer zu findenden Bugs führen.
GO TO FULL VERSION