Prima di spiegare i diversi tipi di dati disponibili in C #, è importante ricordare che C # è un linguaggio fortemente tipizzato. Ciò significa che ogni variabile, costante, parametro di input, tipo restituito e in generale ogni espressione che restituisce un valore ha un tipo.
Ogni tipo contiene informazioni che verranno incorporate dal compilatore nel file eseguibile come metadati che verranno utilizzati da Common Language Runtime (CLR) per garantire l'indipendenza dai tipi quando alloca e recupera memoria.
Se vuoi sapere quanta memoria alloca un tipo specifico, puoi usare l'operatore sizeof come segue:
static void Main()
{
var size = sizeof(int);
Console.WriteLine($"int size:{size}");
size = sizeof(bool);
Console.WriteLine($"bool size:{size}");
size = sizeof(double);
Console.WriteLine($"double size:{size}");
size = sizeof(char);
Console.WriteLine($"char size:{size}");
}
L'output mostrerà il numero di byte allocati da ciascuna variabile.
int size:4
bool size:1
double size:8
char size:2
Le informazioni relative a ciascuna tipologia sono:
- Lo spazio di archiviazione richiesto.
- I valori massimo e minimo. Ad esempio, il tipo Int32 accetta valori compresi tra 2147483648 e 2147483647.
- Il tipo di base da cui eredita.
- La posizione in cui verrà allocata la memoria per le variabili in fase di esecuzione.
- I tipi di operazioni consentite.
I membri (metodi, campi, eventi e così via) contenuti nel tipo. Ad esempio, se controlliamo la definizione del tipo int, troveremo la struttura e i membri seguenti:
namespace System
{
[ComVisible(true)]
public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<Int32>, IEquatable<Int32>
{
public const Int32 MaxValue = 2147483647;
public const Int32 MinValue = -2147483648;
public static Int32 Parse(string s, NumberStyles style, IFormatProvider provider);
...
}
}
Gestione della memoria
Quando più processi sono in esecuzione su un sistema operativo e la quantità di RAM non è sufficiente per contenerlo tutto, il sistema operativo mappa parti del disco rigido con la RAM e inizia a memorizzare i dati nel disco rigido. Il sistema operativo utilizzerà tabelle specifiche in cui gli indirizzi virtuali vengono mappati ai corrispondenti indirizzi fisici per eseguire la richiesta. Questa capacità di gestire la memoria è chiamata memoria virtuale.
In ogni processo, la memoria virtuale disponibile è organizzata nelle seguenti 6 sezioni ma per la rilevanza di questo argomento ci concentreremo solo sullo stack e sull'heap.
Stack
Lo stack è una struttura dati LIFO (last in, first out), con dimensioni dipendenti dal sistema operativo (per impostazione predefinita, per macchine ARM, x86 e x64 Windows riserva 1 MB, mentre Linux riserva da 2 MB a 8 MB a seconda versione).
Questa sezione di memoria è gestita automaticamente dalla CPU. Ogni volta che una funzione dichiara una nuova variabile, il compilatore alloca un nuovo blocco di memoria grande quanto la sua dimensione nello stack e quando la funzione è finita, il blocco di memoria per la variabile viene deallocato.
Heap
Questa regione di memoria non è gestita automaticamente dalla CPU e la sua dimensione è maggiore dello stack. Quando viene richiamata la nuova parola chiave, il compilatore inizia a cercare il primo blocco di memoria disponibile che si adatta alla dimensione della richiesta. e quando lo trova, viene contrassegnato come riservato utilizzando la funzione C incorporata malloc () e restituisce il puntatore a quella posizione. È anche possibile deallocare un blocco di memoria utilizzando la funzione C incorporata free (). Questo meccanismo causa la frammentazione della memoria e deve utilizzare i puntatori per accedere al blocco di memoria corretto, è più lento dello stack per eseguire le operazioni di lettura / scrittura.
Tipi personalizzati e incorporati
Mentre C # fornisce un set standard di tipi incorporati che rappresentano numeri interi, booleani, caratteri di testo e così via, puoi usare costrutti come struct, class, interface ed enum per creare i tuoi tipi.
Un esempio di tipo personalizzato che utilizza il costrutto struct è:
struct Point
{
public int X;
public int Y;
};
Tipi di valore e riferimento
Possiamo classificare il tipo C # nelle seguenti categorie:
- Tipi di valore
- Tipi di riferimento
Tipi di valore I tipi di
valore derivano dalla classe System.ValueType e le variabili di questo tipo contengono i loro valori all'interno della loro allocazione di memoria nello stack. Le due categorie di tipi di valore sono struct ed enum.
L'esempio seguente mostra il membro del tipo boolean. Come puoi vedere non c'è un riferimento esplicito alla classe System.ValueType, questo accade perché questa classe è ereditata dalla struttura.
namespace System
{
[ComVisible(true)]
public struct Boolean : IComparable, IConvertible, IComparable<Boolean>, IEquatable<Boolean>
{
public static readonly string TrueString;
public static readonly string FalseString;
public static Boolean Parse(string value);
...
}
}
Tipi di riferimento
D'altra parte, i tipi di riferimento non contengono i dati effettivi memorizzati in una variabile, ma l'indirizzo di memoria dell'heap in cui è memorizzato il valore. Le categorie dei tipi di riferimento sono classi, delegati, array e interfacce.
In fase di esecuzione, quando viene dichiarata una variabile del tipo di riferimento, essa contiene il valore null fino a quando non viene assegnato un oggetto che è stato creato utilizzando le parole chiave new.
L'esempio seguente mostra i membri del tipo generico List.
namespace System.Collections.Generic
{
[DebuggerDisplay("Count = {Count}")]
[DebuggerTypeProxy(typeof(Generic.Mscorlib_CollectionDebugView<>))]
[DefaultMember("Item")]
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{
...
public T this[int index] { get; set; }
public int Count { get; }
public int Capacity { get; set; }
public void Add(T item);
public void AddRange(IEnumerable<T> collection);
...
}
}
Nel caso in cui si desideri scoprire l'indirizzo di memoria di un oggetto specifico, la classe System.Runtime.InteropServices fornisce un modo per accedere agli oggetti gestiti dalla memoria non gestita. Nell'esempio seguente, utilizzeremo il metodo statico GCHandle.Alloc () per allocare un handle a una stringa e quindi il metodo AddrOfPinnedObject per recuperarne l'indirizzo.
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");
L'output sarà
Memory address:39723832
Riferimenti
Documentazione ufficiale: https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019