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:

  • Heap Allocations Viewer (ReSharper)Heap Allocations Viewer
  • Clr Heap Allocation Analyzer (Visual Studio)Clr Heap Allocation Analyzer

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.

Using plugin

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.

Sample IL

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.