Dziś trochę o podstawach C# ale myślę, że wszyscy znajdą coś wartościowego w tym wpisie bo chcę pokazać jak to działa od środka CLR. Na początek przykład boxing’u:
static void Main(string[] args) { int value = 3; object referencedType = value; }
Boxing to nic innego jak utworzenie typu referencyjnego na podstawie value type. Object to typ referencyjny przechowywany na stercie, z kolei integer to zwykły value type przechowywany na stosie. Opisowo, boxing składa się z 3 operacji:
-
Alokacja pamięci na stercie. Oczywiście musimy zarezerwować pamięć dla Integer’a oraz dla dodatkowych pól wymaganych dla każdego typu referencyjnego (m.in. Sync block Index). Każdy obiekt na stercie (nie tylko po boxing’u) ma te dodatkowe pola – taka jest wewnętrzna architektura. Już w tym momencie widać, że value type są bardziej oszczędne jeśli chodzi o pamięć.
-
Wartość ze stosu jest kopiowana na stertę, w miejsce zaalokowanej pamięci w pierwszym kroku.
-
Wskaźnik na nowo utworzony obiekt zostaje zwrócony.
Jak widać, po mimo, że w c# nie wygląda to skomplikowanie, w rzeczywistości jest to proces dużo bardziej skomplikowany niż może wydawać się. Przyjrzyjmy się teraz wygenerowanemu IL:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint .maxstack 1 .locals init ( [0] int32 'value', [1] object referencedType) L_0000: nop L_0001: ldc.i4.3 L_0002: stloc.0 L_0003: ldloc.0 L_0004: box int32 L_0009: stloc.1 L_000a: ret }
To co nas interesuje to tak naprawdę:
L_0001: ldc.i4.3 L_0002: stloc.0 L_0003: ldloc.0 L_0004: box int32 L_0009: stloc.1
Instrukcja ldc umieszcza wartość 3 na tzw. execution stack. Jeśli pojęcie jest obce, zachęcam do poczytania o nim na MSDN. W skrócie jest to taki wewnętrzny stos, na którym IL pracuje. Instrukcja stloc.0 zdejmuje wartość ze stosu i umieszcza ją w zmiennej o indeksie 0 czyli w value (patrz kod c# wyżej).
Kolejna instrukcja (ldloc.0) ładuje zmienną o indeksie zero (value), umieszczając ją na wspomnianym execution stack. Następnie mamy oczekiwaną instrukcję box, która wykonuje oczywiście boxing. Instrukcja stloc.1 umieszcza wynik w zmiennej o indeksie jeden czyli w referencedType.
Oczywiście należy unikać boxing’u ponieważ wiążę się to z alokacją dodatkowej pamięci a potem z usunięciem tych zasobów przez GC.
Przyjrzyjmy się teraz “analogicznej” operacji uboxingu:
int value = 3; object referencedType = value; int unboxedValue = (int) referencedType;
IL:
L_000a: ldloc.1 L_000b: unbox.any int32 L_0010: stloc.2
Kod IL powinien być już jasny: załadowanie zmiennej o indeksie 1 na execution stack, wykonanie uboxing’u oraz zdjęcie wyniku do zmiennej o indeksie dwa. Unboxing składa się z następujących operacji:
1. Jeśli wartością jest NULL, zostaje wyrzucony wyjątek NullReferenceException. Poniższy kod zakończy się wyjątkiem:
object referencedType = null; int unboxedValue = (int) referencedType;
2. Jeśli typ na stercie nie jest taki sam jak żądany podczas uboxing’u wyrzucany jest wyjątkiem InvalidCastException. Przykład:
object referencedType = 242f; int unboxedValue = (int) referencedType;
Warto zaznaczyć, że typy muszą być identyczne. Jeśli został umieszczony Int16 należy użyć Int16 a nie np. Int32, który jest o szerszym zasięgu i mogłoby się wydawać, że powinien zadziałać.
3. Zwracany jest wskaźnik do wartości umieszczonej na starcie, którą potem zwykle jest kopiowana.
Teoretycznie uboxing jest operacją dużo SZYBSZĄ niż boxing. CLR nie wymaga aby wartość po uboxing’u była kopiowana. To co unboxing robi to zwrócenie wskaźnika do obiektu unboxed, który znajduję w instancji. Innymi słowy operacja zwraca wskaźnik na value type, który znajduje się w obiekcie referencyjnym. W C# jednak unboxing zawsze wiąże się z kopiowaniem wartości ponieważ nie ma możliwości wykonania unboxing’u bez przypisania go do zmiennej. C++\CLI pozwala wykonać unboxing’u bez kopiowania – w c# jest to po prostu niemożliwe i jak widać po IL, zawsze jest dodawana operacja wykonująca kopiowanie danych.
Ze względu na te wszystkie operacje, kluczowe staje się rozpoznanie sytuacji w której boxing\unboxing są wykonywane. Jeśli mamy metodę przyjmującą object zawsze przed przekazaniem value type, jest dokonywany boxing:
private void Method(object argument) { } Method(3); // boxing
To chyba był prosty przykład… Kolejny przykład, może jednak zadziwić:
int value = 3; Type type=value.GetType();
Niestety taka niepozorna operacja również powoduje boxing. Dlaczego? GetType jest niewirtualną metodą zadeklarowaną w System.Object, który jest typem referencyjny. Z tego względu, aby wykonać taką metodę, należy przekazać wskaźnik this. Zasada jest prosta: wszystkie metody, zarówno wirtualne jak niewirtualne zadeklarowane w typie referencyjnym (jakim jest System.Object), wymagają boxingu. A co w takim przypadku:?
struct Color { public override string ToString() { return "test"; } } static void Main(string[] args) { Color color; string str = color.ToString(); }
Boxing nie zostanie wykonywany. Przyjrzyjmy się jeszcze kolejnemu przykładowi:
class Program { struct Color { } static void Main(string[] args) { Color color; string str = color.ToString(); } }
Pierwszy przykład przeładowuje ToString i nie wykonuje bazowej implementacji. Z tego względu nie ma potrzeby wykonania kodu znajdującego się w referencyjnym typie System.Object. Drugi przykład nie przeładowuje metody zatem niezbędne jest wykonanie bazowej implementacji, znajdującej się w System.Object.
W c# rzutowane i boxing mają taką samą składnie jednak jak widać znaczącą się to różnią. Szczególnie następujący kod może być mylący:
struct ValueType { public void ChangeValue(int value) { Value = value; } public int GetValue() { return Value; } private int Value; } static void Main(string[] args) { object referencedType = new ValueType(); ((ValueType)referencedType).ChangeValue(10); Console.WriteLine( ((ValueType)referencedType).GetValue()); }
Dla referencyjnych typów, na ekranie wartość 10 zostałaby wyświetlona. Ze względu na boxing a nie proste rzutowanie, za każdym razem obiekt jest kopiowany, skutkując wartością 0 wyświetloną na ekranie.
W kilku miejscach masz ‘o’ zamiast ‘0’. Zwłaszcza ostatnie zdanie może zmylić.
To jest zero, po prostu tak font jest wyswietlany w niektorych przegladarkach:/
Może warto pogrubić ‘0’ 😉
Nom potestuje cos 🙂 Albo po prostu slownie pisac “zero”jesli to tylko mozliwe.
“W C# jednak unboxing zawsze wiąże się z kopiowaniem wartości ponieważ nie ma możliwości wykonania unboxing’u bez przypisania go do zmiennej. “,
a następnie:
1.object referencedType = new ValueType();
2.((ValueType)referencedType).ChangeValue(10);
3.Console.WriteLine( ((ValueType)referencedType).GetValue());
z tego co rozumiem:
ValueType(wartościowy) jest boxowany do object.
2. referencedType jest unboxowany do ValueType i do niego przez metodę jest przypiswana wartość 10.
Czyli unboxing się wykonał bez przypisania do zmiennej. Fakt, że bez sensu, ponieważ ginie jako garbage, niemniej jednak sama operacja zaszła.
Czy pisząc, że nie ma możliwości wykonania unboxingu bez przypisania do zmiennej, miałeś na myśli to, że się unboxing wykona ale wartość zginie w pamięci ?.