Tipul Span
Tipul Span reprezintă o regiune continuă de memorie. Scopul acestui tip este de a crește performanța și eficiența utilizării memoriei. Span permite evitarea alocărilor suplimentare de memorie la operarea cu seturi de date. Deoarece Span este o structură, obiectul acestui tip este plasat pe stivă, nu în heap.
Crearea unui Span
Pentru a crea un obiect Span, putem folosi unul dintre constructorii săi:
- Span(): creează un obiect Span gol
- Span(T item): creează un obiect Span cu un element item
- Span(T[] array): creează un obiect Span dintr-un array array
- Span(void* pointer, int length): creează un obiect Span care primește length bytes de memorie începând de la pointer
- Span(T[] array, int start, int length): creează un obiect Span care primește length elemente din array, începând de la indexul start
De exemplu, o creare simplă a unui Span:
Span<string> people = new Span<string>(new string[] { "Tom", "Bob", "Sam" });
În acest caz, Span va stoca referințe la trei șiruri de caractere.
Span este adesea creat pe baza altor seturi de date:
string[] people = { "Tom", "Alice", "Bob" };
Span<string> peopleSpan = new Span<string>(people);
Putem, de asemenea, să atribuim direct un array, care va fi implicit convertit în Span:
string[] people = { "Tom", "Alice", "Bob" };
Span<string> peopleSpan = people;
Putem apoi accesa, seta sau itera prin date, la fel ca în cazul unui array:
string[] people = { "Tom", "Alice", "Bob" };
Span<string> peopleSpan = people;
peopleSpan[1] = "Ann"; // setarea valorii unui element
Console.WriteLine(peopleSpan[2]); // obținerea unui element
Console.WriteLine(peopleSpan.Length); // obținerea lungimii Span
// iterația prin Span
foreach (var s in peopleSpan)
{
Console.WriteLine(s);
}
Dacă Span se comportă la exterior ca un array, care este avantajul său sau când ne poate fi util? Să luăm un exemplu simplu: avem un array cu valorile temperaturilor zilnice pe parcursul unei luni și trebuie să obținem două seturi - setul temperaturilor din prima decadă și din ultima decadă. Folosind array-uri, am putea proceda astfel:
int[] temperatures =
{
10, 12, 13, 14, 15, 11, 13, 15, 16, 17,
18, 16, 15, 16, 17, 14, 9, 8, 10, 11,
12, 14, 15, 15, 16, 15, 13, 12, 12, 11
};
int[] firstDecade = new int[10]; // alocăm memorie pentru prima decadă
int[] lastDecade = new int[10]; // alocăm memorie pentru a doua decadă
Array.Copy(temperatures, 0, firstDecade, 0, 10); // copiem datele în primul array
Array.Copy(temperatures, 20, lastDecade, 0, 10); // copiem datele în al doilea array
Pentru a stoca datele, creăm două array-uri suplimentare pentru temperaturile zilnice din fiecare decadă. Cu metoda Array.Copy, datele din array-ul inițial temperatures sunt copiate în cele două array-uri.
Dar în acest caz, suntem forțați să alocăm memorie suplimentară pentru cele două array-uri, care de fapt conțin aceleași date ca temperatures, dar în părți separate ale memoriei.
Span permite lucrul cu memoria mai eficient și evită alocările inutile de memorie. Folosind Span în loc de array-uri:
int[] temperatures =
{
10, 12, 13, 14, 15, 11, 13, 15, 16, 17,
18, 16, 15, 16, 17, 14, 9, 8, 10, 11,
12, 14, 15, 15, 16, 15, 13, 12, 12, 11
};
Span<int> temperaturesSpan = temperatures;
Span<int> firstDecade = temperaturesSpan.Slice(0, 10); // fără alocare de memorie pentru date
Span<int> lastDecade = temperaturesSpan.Slice(20, 10); // fără alocare de memorie pentru date
Pentru a crea obiecte Span derivate, folosim metoda Slice, care extrage o parte din Span și o returnează sub forma unui alt obiect Span. Acum, obiectele Span firstDecade și lastDecade lucrează cu aceleași date ca temperaturesSpan, dar nu se alocă memorie suplimentară.
În toate cele trei cazuri, lucrăm cu același array temperatures. Putem chiar să modificăm datele într-un Span și acestea se vor schimba și în celălalt:
int[] temperatures =
{
10, 12, 13, 14, 15, 11, 13, 15, 16, 17,
18, 16, 15, 16, 17, 14, 9, 8, 10, 11,
12, 14, 15, 15, 16, 15, 13, 12, 12, 11
};
Span<int> temperaturesSpan = temperatures;
Span<int> firstDecade = temperaturesSpan.Slice(0, 10);
temperaturesSpan[0] = 25; // schimbăm în temperatureSpan
Console.WriteLine(firstDecade[0]); // 25
Cum este posibil acest lucru? Pentru a înțelege cum funcționează Span, putem consulta codul sursă al tipului. În special, putem vedea următoarea proprietate:
public readonly ref struct Span<T>
{
//....
public ref T this[int index] { get { ... } }
//....
}
Aici vedem că indexatorul returnează o referință ref, ceea ce ne permite accesul direct la obiect și modificarea acestuia.
În acest caz, avantajele lipsei alocării suplimentare de memorie pentru stocarea obiectelor sunt minime. Dar în lucrul mai intensiv cu datele, câștigul în performanță va crește inevitabil.
Metode Span
Metodele principale ale Span:
- void Fill(T value): umple toate elementele Span cu valoarea value
- T[] ToArray(): convertește Span într-un array
- Span<T> Slice(int start, int length): extrage length elemente din Span începând de la indexul start sub forma unui alt Span
- void Clear(): golește Span
- void CopyTo(Span<T> destination): copiază elementele din Span-ul curent în alt Span
- bool TryCopyTo(Span<T> destination): copiază elementele din Span-ul curent în alt Span și returnează un boolean care indică dacă operațiunea de copiere a reușit
ReadOnlySpan
Structura ReadOnlySpan este similară cu Span, dar este destinată datelor neschimbabile. De exemplu:
string text = "hello, world";
string worldString = text.Substring(startIndex: 7, length: 5); // alocare de memorie pentru caractere
ReadOnlySpan<char> worldSpan = text.AsSpan().Slice(start: 7, length: 5); // fără alocare de memorie pentru caractere
// worldSpan[0] = 'a'; // Nu se poate modifica
Console.WriteLine(worldSpan[0]); // afișează primul caracter
// iterația prin caractere
foreach (var c in worldSpan)
{
Console.Write(c);
}
În acest caz, folosim metoda AsSpan() pentru a converti șirul text într-un obiect ReadOnlySpan<char> și apoi extragem din acesta intervalul de caractere "world". Deoarece ReadOnlySpan este destinat doar pentru citire, nu putem modifica datele prin intermediul său, dar le putem accesa. În rest, lucrul cu ReadOnlySpan se face la fel ca și cu Span.
Limitările Span
Ca o structură definită cu modificatorul ref, Span are câteva limitări: nu poate fi atribuit unei variabile de tip Object, dynamic sau unei variabile de tip interfață. Nu poate fi un câmp într-un obiect de tip referință (doar în cadrul structurilor ref). Nu poate fi utilizat în operațiuni await sau yield.