Przed wyjaśnieniem różnych typów danych dostępnych w C #, ważne jest, aby wspomnieć, że C # jest językiem o jednoznacznie określonym typie. Oznacza to, że każda zmienna, stała, parametr wejściowy, typ zwracany i ogólnie każde wyrażenie, którego wynikiem jest wartość, ma typ.
Każdy typ zawiera informacje, które zostaną osadzone przez kompilator w pliku wykonywalnym jako metadane, które będą używane przez środowisko uruchomieniowe języka wspólnego (CLR), aby zagwarantować bezpieczeństwo typów podczas przydzielania i odzyskiwania pamięci.
Jeśli chcesz wiedzieć, ile pamięci przydziela określony typ, możesz użyć operatora sizeof w następujący sposób:
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}");
}
Dane wyjściowe pokażą liczbę bajtów przydzielonych przez każdą zmienną.
int size:4
bool size:1
double size:8
char size:2
Informacje związane z każdym typem to:
- Wymagana przestrzeń magazynowa.
- Wartości maksymalne i minimalne. Na przykład typ Int32 akceptuje wartości od 2147483648 do 2147483647.
- Typ podstawowy, z którego dziedziczy.
- Lokalizacja, w której pamięć dla zmiennych zostanie przydzielona w czasie wykonywania.
- Rodzaje operacji, które są dozwolone.
Elementy członkowskie (metody, pola, zdarzenia itp.) Zawarte w typie. Na przykład, jeśli sprawdzimy definicję typu int, znajdziemy następującą strukturę i składowe:
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);
...
}
}
Zarządzanie pamięcią
Kiedy w systemie operacyjnym działa wiele procesów, a ilość pamięci RAM nie jest wystarczająca, aby wszystko pomieścić, system operacyjny mapuje części dysku twardego z pamięcią RAM i rozpoczyna przechowywanie danych na dysku twardym. System operacyjny użyje niż określone tabele, w których adresy wirtualne są mapowane na odpowiadające im adresy fizyczne w celu wykonania żądania. Ta możliwość zarządzania pamięcią nazywana jest pamięcią wirtualną.
W każdym procesie dostępna pamięć wirtualna jest zorganizowana w następujących 6 sekcjach, ale ze względu na znaczenie tego tematu skupimy się tylko na stosie i stercie.
Stos
Stos jest strukturą danych LIFO (ostatnie weszło, pierwsze wyszło), z wielkością zależną od systemu operacyjnego (domyślnie dla maszyn ARM, x86 i x64 system Windows rezerwuje 1 MB, podczas gdy Linux rezerwuje od 2 MB do 8 MB w zależności od wersja).
Ta sekcja pamięci jest automatycznie zarządzana przez procesor. Za każdym razem, gdy funkcja deklaruje nową zmienną, kompilator przydziela na stosie nowy blok pamięci o wielkości odpowiadającej jej rozmiarowi, a po zakończeniu funkcji blok pamięci dla zmiennej jest zwalniany.
Sterta
Ten region pamięci nie jest zarządzany automatycznie przez procesor, a jego rozmiar jest większy niż stos. Po wywołaniu słowa kluczowego new kompilator zaczyna szukać pierwszego wolnego bloku pamięci, który pasuje do rozmiaru żądania. a kiedy go znajdzie, jest oznaczany jako zarezerwowany za pomocą wbudowanej funkcji malloc () w języku C i zwraca wskaźnik do tej lokalizacji. Możliwe jest również zwolnienie bloku pamięci za pomocą wbudowanej funkcji free () w języku C. Mechanizm ten powoduje fragmentację pamięci i musi używać wskaźników, aby uzyskać dostęp do odpowiedniego bloku pamięci, jest wolniejszy niż stos, aby wykonać operacje odczytu / zapisu.
Typy niestandardowe i wbudowane
Podczas gdy C # zapewnia standardowy zestaw wbudowanych typów reprezentujących liczby całkowite, wartości logiczne, znaki tekstowe i tak dalej, możesz użyć konstrukcji, takich jak struct, class, interface i enum, aby utworzyć własne typy.
Przykład niestandardowego typu używającego konstrukcji struct:
struct Point
{
public int X;
public int Y;
};
Typy wartości i odwołań
Możemy podzielić typ C # na następujące kategorie:
- Typy wartości
- Typy referencyjne
Typy wartości Typy
wartości pochodzą z klasy System.ValueType, a zmienne tego typu zawierają swoje wartości w ramach alokacji pamięci na stosie. Dwie kategorie typów wartości to struct i enum.
Poniższy przykład przedstawia element członkowski typu boolean. Jak widać, nie ma wyraźnego odwołania do klasy System.ValueType, dzieje się tak, ponieważ ta klasa jest dziedziczona przez strukturę.
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);
...
}
}
Typy odwołań
Z drugiej strony, typy odwołań nie zawierają rzeczywistych danych przechowywanych w zmiennej, ale adres pamięci sterty, w której przechowywana jest wartość. Kategorie typów odwołań to klasy, delegaci, tablice i interfejsy.
W czasie wykonywania, gdy deklarowana jest zmienna typu referencyjnego, zawiera ona wartość null do momentu przypisania do niej obiektu, który został utworzony przy użyciu słów kluczowych new.
Poniższy przykład przedstawia elementy członkowskie List typu ogólnego.
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);
...
}
}
Jeśli chcesz znaleźć adres pamięci konkretnego obiektu, klasa System.Runtime.InteropServices zapewnia dostęp do zarządzanych obiektów z niezarządzanej pamięci. W poniższym przykładzie użyjemy statycznej metody GCHandle.Alloc () do przydzielenia uchwytu do łańcucha, a następnie metody AddrOfPinnedObject w celu pobrania jego adresu.
string s1 = "Hello World";
GCHandle gch = GCHandle.Alloc(s1, GCHandleType.Pinned);
IntPtr pObj = gch.AddrOfPinnedObject();
Console.WriteLine($"Memory address:{pObj.ToString()}");
Wynik będzie
Memory address:39723832
Referencje
Oficjalna dokumentacja: https://docs.microsoft.com/en-us/cpp/build/reference/stack-stack-allocations?view=vs-2019