Zmienne typu wartościowego (ValueType) są najprostszymi strukturami danych dostępnymi w frameworku .NET. W odróżnieniu od typów referencyjnych ich wartość jest przechowywana na stosie co znacznie ułatwia i przyspiesza dostęp do danych. W przypadku typów referencyjnych na stosie jest przechowywana jedynie referencja na obiekt znajdujący się na stercie (heap).
Z uwagi na to, że w sieci jest dużo informacji na ten temat przedstawię tylko ciekawostki warte zapamiętania.
Typy wartościowe możemy podzielić na trzy główne kategorie:
- Typy wbudowane
- Typy wyliczeniowe (Enumeration)
- Typy tworzone przez użytkownika (Struktury)
Wszystkie typy wartościowe są pochodnymi klasy System.ValueType. Główną cechą zmiennych tego typu jest fakt, że po przypisaniu jednej zmiennej do innej dostajemy kopię danego obiektu. Zatem po takiej operacji dostajemy dwie niezależne zmienne. Podobna sytuacja ma miejsce podczas przekazywania takich zmiennych jako parametry w funkcjach.
Domyślna implementacja metody ValueType.Equals do porównywania wartości wykorzystuje system refleksji. Nadpisanie tej metody w określonych typach może znacznie zwiększyć wydajność ich porównywania.
Typy wbudowane
Pierwszą ważną kwestią jest inicjalizacji zmiennych lokalnych, gdyż brak inicjalizacji powoduje błąd podczas kompilacji. Typy wartościowe będące polami klas nie muszą być inicjalizowany. Przy tworzeniu obiektu danej klasy mają przypisaną domyślną wartość dla danego typu.
Nazwy typów int, long, double itd są tylko aliasami na typy, odpowiednio System.Int32, System.Int64, System.Double. Wszystkie aliasy możemy zobaczyć tutaj.
Każdy typ wartościowy posiada domyślny konstruktor który możemy wywołać:
[csharp]int i = new int();
double d = new double();[/csharp]
Po wywołaniu konstruktora zostanie utworzony obiekt z domyślną wartością.
Enum
Kolejnym typem wartościowym jest typ wyliczeniowy. Domyślnym typem dla typu enumeracyjnego jest int, aby wskazać inny typ podajemy go po nazwie, do dyspozycji mamy wszystkie typy całkowite za wyjątkiem typu char
[csharp]enum Days : long { Mon=1, Tue, Wed, Thu, Fri, Sat, Sun };[/csharp]
Elementy typu wyliczeniowego możemy rzutować na typ w jakim wartości są przechowywane (w tym przypadku long) jak i możliwa jest konwersja w drugą stronę.
[csharp] long mon = (long) Days.Mon;
Days d = (Days) mon;[/csharp]
Domyślną wartością każdego typu enumeracyjnego jest 0, zatem warto o tym pamiętać przypisując wartości do poszczególnych jej elementów.
[csharp]Days days = new Days(); //Domyślny konstruktor
Days days = 0; //nie wymaga rzutowania
Days days = (Days)1; //rzutowanie wymagane
[/csharp]
Słowem kluczowym new możemy zastąpić dany typ enumeracyjny zagnieżdżony w klasie na nowy.
Flags
Aby móc typem enumeracyjnym posługiwać się jako flagami musimy dodać do niego atrybut [Flags]. Warto pamiętać aby do konkretnych elementów typu wyliczeniowego przypisać wartości będące kolejnymi potęgami 2, w przeciwnym wypadku typ może zachowywać się nie tak jak byśmy tego oczekiwali.
[csharp][Flags]
enum Colors : short
{
Black = 0,
Red = 1,
Green = 2,
Blue = 4,
All = Black | Red | Green | Blue
}
[/csharp]
Struct
Struktury są typem definiowanym przez użytkownika. Struktury posiadają kilka podstawowych cech o których warto pamiętać:
- rozmiar struktury musi być nie większy niż 16 B (bajtów)
- struktura nie może posiadać bezparametrowego konstruktora
- w strukturach nie ma dziedziczenia tak jak w klasach jednakże struktura jest pochodną klasy System.Object
- sturuktua nie może być bazą dla klasy jak również struktura nie może dziedziczyć po innej strukturze lub klasie
- mimo braku dziedziczenia struktury mogą implementować interfejsy.
Dzięki atrybutom możemy określić niestandardowe rozłożenie struktury w pamięci (coś na wzór union w C/C++)
[csharp]
[StructLayout(LayoutKind.Explicit)]
struct TestExplicit
{
[FieldOffset(0)]
public long lg;
[FieldOffset(0)]
public int i1;
[FieldOffset(4)]
public int i2;
} [/csharp]
W powyższym przypadku pole lg wskazuje na ten sam obszar pamięci co pola i1 i i2. Zatem zmiana pola lg zmienia wartości pól i1 i i2, jak również zmiana pól i1 i/lub i2 zmienia wartość pole lg.
Nullable
Domyślnie do typu wartościowego nie można przypisać wartości null. Jednak platforma .NET dostarcza nam typ generyczyn umożliwiający takie przypisanie, mowa tu o strukturze Nullable<T>.
[csharp]
Nullable<int> nint = null;
int? a = null; // skrócony zapis
[/csharp]
Boxing/Unboxing
Boxing i unboxing to mechanizmy o których istnieniu warto wiedzieć. Prostym przykładem boxing’u jest poniższy kod:
[csharp]Point p = new Point();
object obj = (object)p;
p.X = 5;
Console.WriteLine(p);
Console.WriteLine(obj);
[/csharp]
Po wykonaniu tego kodu na ekranie ujrzymy:
{X=5,Y=0} {X=0,Y=0}
Jak widzimy po przypisaniu struktury typu Point do zmiennej typu object uzyskaliśmy dwa różne obiekty, a to za sprawą faktu, że przy przypisaniu struktury do zmiennej typu referencyjnego obiekt został skopiowany do sterty i uzyskaliśmy dwie kopie tego samego obiektu jedna na stosie, a drugą na stercie. W wyniku czego zmiana pierwszego nie wpływa na drugi obiekt.
Więcej informacji jak również przykładowe ilustracje przybliżające działanie mechanizmu boxingu można znaleźć w The C# Value Type and Boxing .
Wpis powstał na podstawie: Value Types, Structs Tutorial, Enumeration Types, Boxing and Unboxing, Nullable Types.