Co oznaczają pojęcia boxing i unboxing? Jeśli ktoś kiedyś przygotowywał się do rozmowy kwalifikacyjnej czy do zaliczenia z podstaw języka C# na studiach, na pewno słyszał/czytał, że powyższe pytanie to pewniak na 100% o to spytają. Może i tak było, ale czy oprócz wykutych regułek orientujecie się, kiedy boxing występuję w praktyce na placu boju. W artykule omówię znane mi przykłady boxingu.
Teoria
Na początek zanim przejdę do praktyki w ramach wyrównanie wiedzy omówię dwa podstawowe terminy powiązane z bieżącym wpisem.
Boxing – jawna/niejawna konwersja typu wartościowego na typ referencyjny
Unboxing – jawna konwersja typu referencyjnego na typ wartościowy
Pluginy do VS i R#
Visual Studio jest zazwyczaj codziennym środowiskiem pracy programisty C#. Wykorzystajmy to i wspomóżmy sobie wykrywanie ukrytych alokacji poprzez instalacje jednej z wtyczek:
Wtyczki analizują kod w C# w celu znalezienia ukrytych alokacji. Miejsca występowania boxingu prezentowane są podkreśleniem, po najechaniu na zmienną otrzymamy szczegółowy komunikat o wykrytym problemie.
Praktyka (przypadki występowania)
W analizowanych przykładach będę bazował na poniższych strukturach danych.
public enum Role { Student } public interface IVector { } public struct Vector : IVector { } public struct SampleStruct { public void SampleMethod(string msg) => Console.WriteLine(msg); }
Boxing występuje w następujących przypadkach:
- Konwersja typu wartościowego na typ System.Object.
int value = 15; object box = value;
- Konwersja typu wartościowego na typ System.ValueType
decimal value = 200.0m; ValueType box = value;
- Konwersja typu wyliczeniowego na typ System.Enum.
Enum box = Role.Student;
- Konwersja typu wartościowego na typ dynamic.
short value = 14; dynamic box = value;
- Konwersja typu wartościowego na typ interfejsu.
IVector box = new Vector();
- Omawiane powyżej przykłady boxingu, występują także w momencie zwracania wartości wyniku z metody, oraz w sytuacji przekazywania argumentu do metody.
public object GetObject() => 13.34f; public ValueType GetValue() => 23; public Enum GetRole() => Role.Student; public dynamic GetAge() => 18; public IVector GetVector() => new Vector();
public void SetObject(object obj) { } public void SetValue(ValueType value) { } public void SetRole(Enum role) { } public void SetAge(dynamic age) { } public void SetVector(IVector vector) { }
SetObject(13.34f); SetValue(23); SetRole(Role.Student); SetAge(21); SetVector(new Vector());
- W sytuacji przekazywania implementacji interfejsu IVector, możemy lekko zmodyfikować metodę w celu uniknięcia boxingu (kod poniżej).
public void SetVector<T>(T vector) where T: IVector { }
- Dodawanie elementu do kolekcji typu List<object>.
var list = new List<object>(); list.Add(23); list.Add('d');
- Dodawanie elementu do nie generycznych kolekcji np. ArrayList, HashTable, SortedList etc.
var arrayList = new ArrayList { "65" }; var hashTable = new Hashtable { { "key", "value" } }; var sortedList = new SortedList { { "key", "value" } };
- Sprawdzenie czy argument metody nie jest null.
public bool IsNull<T>(T obj) => obj == null; public string GetMessage<T>(T obj) => obj?.ToString();
- Wywołanie nieprzesłoniętych wirtualnych metod na typach wartościowych.
var sample = new SampleStruct(); var hash = sample.GetHashCode(); var s = sample.ToString();
W powyższym przypadku w celu uniknięcia boxingu wymagana jest implementacja interfejsu IEquatable<T> i przesłonięcie metody ToString().
- Wywołanie metody klasy bazowej w strukturze.
public struct SampleStruct { public override string ToString() { return base.ToString(); } }
W powyższym kodzie został rozwiązany wcześniejszy problem dla wywołanie nieprzesłoniętej metody wirtualnej. Jednak w ciele metody ToString() wywołujemy metodę z klasy bazowej, co powoduje w tym przypadku boxing.
- Wywołanie metody GetType() na typie wartościowym.
var sample = new SampleStruct(); var type = sample.GetType();
- Powiązanie zadeklarowanego delegata z metodą typu wartościowego.
var sample = new SampleStruct(); Action<string> box = sample.SampleMethod;
- Konkatenacja stringa z użyciem typu wartościowego
char nextChar = 'z'; string s = "xy" + nextChar; string sFormat = String.Format("xy{0}", nextChar); string sInter = $"xy{nextChar}"; string sConcat = string.Concat("xy", nextChar);
W celu zrozumienia wystąpienia boxingu przy konkatenacji stringa podejrzyjmy sobie definicje operatora +. Jako drugi parametr przyjmowany jest wartość typu object.
string string.operator +(string left, object right)
Podejrzyjmy IL
Jeśli nie lubimy korzystać z pluginów, które podkreślają nam miejsca występowania ukrytej alokacji w Visual Studio możemy zawsze przeanalizować kod IL. Tutaj zaprezentuję dodatkowy przypadek boxingu, którego nie wymieniłem wcześniej w rozdziale Praktyka. W celu kompilacji instrukcji C# do kodu pośredniego IL wykorzystam program LINQPad.
W sytuacji użycia dopasowania wzorca (pattern matching) z stałą w instrukcji z is dochodzi do boxingu. Nie będę omawiał po kolei każdej instrukcji IL to, co w tej chwili nas interesuje to wystąpienie instrukcji box (Convert a boxable value to its boxed form) oznaczającej wystąpienie boxingu.
Podsumowanie
Dzisiejszy wpis sięga do postaw, o których klepiąc kolejne klasy typu CRUD zapominamy. Mam nadzieje, że udało mi się w prosty sposób zaprezentować na przykładach wystąpienie boxingu. Podsumowując boxing i unboxing są kosztownymi konwersjami głównie ze względu na alokacje dodatkowej pamięci podczas boxingu (GC będzie musiał po wszystkim posprzątać). Jeśli znasz przypadek wystąpienia boxingu, o którym nie wspomniałem to zapraszam do podzielenia się w komentarzu pod tym wpisem.