Występowanie boksu w C #

85

Próbuję zebrać wszystkie sytuacje, w których boks występuje w C #:

  • Konwersja typu wartości na System.Objecttyp:

    struct S { }
    object box = new S();
    
  • Konwersja typu wartości na System.ValueTypetyp:

    struct S { }
    System.ValueType box = new S();
    
  • Konwersja wartości typu wyliczenia na System.Enumtyp:

    enum E { A }
    System.Enum box = E.A;
    
  • Konwersja typu wartości na odniesienie do interfejsu:

    interface I { }
    struct S : I { }
    I box = new S();
    
  • Używanie typów wartości w konkatenacji ciągów C #: Using value types in C # string concatenation:

    char c = F();
    string s1 = "char value will box" + c;
    

    uwaga: stałe chartypu są łączone w czasie kompilacji

    Uwaga: od wersji 6.0 C # kompilator optymalizuje konkatenacji udziałem bool, char, IntPtr, UIntPtrrodzaje

  • Tworzenie delegata z metody wystąpienia typu wartości:

    struct S { public void M() {} }
    Action box = new S().M;
    
  • Wywołanie niezastępowanych metod wirtualnych na typach wartości:

    enum E { A }
    E.A.GetHashCode();
    
  • Korzystanie ze stałych wzorców języka C # 7.0 w is wyrażeniem:

    int x = …;
    if (x is 42) { … } // boxes both 'x' and '42'!
    
  • Boks w C # konwersjach typów krotek:

    (int, byte) _tuple;
    
    public (object, object) M() {
      return _tuple; // 2x boxing
    }
    
  • Opcjonalne parametry objecttypu z wartościami domyślnymi typu wartości:

    void M([Optional, DefaultParameterValue(42)] object o);
    M(); // boxing at call-site
    
  • Sprawdzanie wartości nieograniczonego typu ogólnego dla null:

    bool M<T>(T t) => t != null;
    string M<T>(T t) => t?.ToString(); // ?. checks for null
    M(42);
    

    Uwaga: może to zostać zoptymalizowane przez JIT w niektórych środowiskach .NET

  • Wartość testowania typu typu nieograniczonego lub structogólnego z operatorami is/ as:

    bool M<T>(T t) => t is int;
    int? M<T>(T t) => t as int?;
    IEquatable<T> M<T>(T t) => t as IEquatable<T>;
    M(42);
    

    uwaga: może to zostać zoptymalizowane przez JIT w niektórych środowiskach .NET

Czy są jakieś sytuacje bokserskie, może ukryte, o których wiesz?

kontrola przepływu
źródło
2
Miałem do czynienia z tym jakiś czas temu i uznałem to za całkiem interesujące: Wykrywanie (nie) boksu za pomocą FxCop
George Duckett.
Bardzo fajne sposoby konwersji, które pokazałeś. Właściwie nie znałem drugiego, a może nigdy tego nie próbowałem :) Dzięki
Zenwalker
12
powinno to być pytanie społeczności wiki
Sly
2
A co z typami dopuszczającymi wartość null? private int? nullableInteger
allansson
1
@allansson, typy dopuszczające wartość
null

Odpowiedzi:

42

To świetne pytanie!

Boks pojawia się dokładnie z jednego powodu: kiedy potrzebujemy odniesienia do typu wartości . Wszystko, co podałeś, podlega tej zasadzie.

Na przykład, ponieważ obiekt jest typem referencyjnym, rzutowanie typu wartości na obiekt wymaga odwołania do typu wartości, co powoduje zapakowanie.

Jeśli chcesz wymienić wszystkie możliwe scenariusze, powinieneś również uwzględnić pochodne, takie jak zwracanie typu wartości z metody, która zwraca obiekt lub typ interfejsu, ponieważ powoduje to automatyczne rzutowanie typu wartości na obiekt / interfejs.

Nawiasem mówiąc, przypadek konkatenacji ciągów, który trafnie zidentyfikowałeś, również pochodzi z rzutowania na obiekt. Operator + jest tłumaczony przez kompilator na wywołanie metody ciągowej Concat, która akceptuje obiekt dla przekazanego typu wartości, więc następuje rzutowanie na obiekt i tym samym tworzenie pudełek.

Przez lata zawsze doradzałem programistom, aby pamiętali jeden powód boksu (wskazałem powyżej) zamiast zapamiętywać każdy przypadek, ponieważ lista jest długa i trudna do zapamiętania. Ułatwia to również zrozumienie, jaki kod IL generuje kompilator dla naszego kodu C # (na przykład + on string daje wywołanie String.Concat). Jeśli masz wątpliwości, co generuje kompilator i jeśli wystąpi boxing, możesz użyć IL Disassembler (ILDASM.exe). Zwykle powinieneś szukać kodu operacyjnego pudełka (jest tylko jeden przypadek, w którym może wystąpić boks, nawet jeśli IL nie zawiera kodu operacyjnego pudełka, więcej szczegółów poniżej).

Ale zgadzam się, że niektóre wydarzenia bokserskie są mniej oczywiste. Wymieniłeś jeden z nich: wywołanie niezastępowanej metody typu wartości. W rzeczywistości jest to mniej oczywiste z innego powodu: kiedy sprawdzasz kod IL, nie widzisz kodu operacyjnego pudełka, ale kod ograniczenia, więc nawet w IL nie jest oczywiste, że ma miejsce boks! Nie będę wchodził w szczegóły, dlaczego nie mam dopuścić do tego, aby ta odpowiedź stała się jeszcze dłuższa ...

Innym przypadkiem mniej oczywistego boksu jest wywołanie metody klasy bazowej ze struktury. Przykład:

struct MyValType
{
    public override string ToString()
    {
        return base.ToString();
    }
}

Tutaj ToString jest zastępowane, więc wywołanie ToString w MyValType nie spowoduje wygenerowania boksu. Jednak implementacja wywołuje podstawowy ToString, co powoduje boksowanie (sprawdź IL!).

Nawiasem mówiąc, te dwa nieoczywiste scenariusze bokserskie również wywodzą się z jednej zasady powyżej. Gdy metoda jest wywoływana w klasie bazowej typu wartości, musi istnieć coś, do czego słowo kluczowe this może się odwoływać. Ponieważ klasa bazowa typu wartości jest (zawsze) typem referencyjnym, metoda this słowo kluczowe musi odnosić się do typu referencyjnego, dlatego potrzebujemy odwołania do typu wartości, a więc boksowanie występuje z powodu pojedynczej reguły.

Oto bezpośredni link do sekcji mojego kursu online .NET, który szczegółowo omawia boks: http://motti.me/mq

Jeśli interesują Cię tylko bardziej zaawansowane scenariusze bokserskie, tutaj znajduje się bezpośredni link (chociaż powyższy link również Cię tam przeniesie, gdy omówi bardziej podstawowe rzeczy): http://motti.me/mu

Mam nadzieję, że to pomoże!

Motti

Motti Shaked
źródło
1
Jeśli a ToString()jest wywoływana dla określonego typu wartości, który go nie przesłania, czy typ wartości zostanie umieszczony w miejscu wywołania, czy też metoda zostanie wysłana (bez ramki) do automatycznie wygenerowanego zastąpienia, które nie robi nic poza łańcuchem (z boks) do metody podstawowej?
supercat
@supercat Wywołanie dowolnej metody, która wywołuje basetyp wartości, spowoduje boksowanie. Obejmuje to metody wirtualne, które nie są zastępowane przez strukturę, i Objectmetody, które w ogóle nie są wirtualne (jak GetType()). Zobacz to pytanie .
Şafak Gür
@ ŞafakGür: Wiem, że efektem końcowym będzie boks. Zastanawiałem się nad dokładnym mechanizmem, za pośrednictwem którego to się dzieje. Ponieważ kompilator generujący IL może nie wiedzieć, czy typ jest typem wartości, czy referencją (może być ogólny), niezależnie od tego wygeneruje callvirt. JITter wiedziałby, czy typ jest typem wartości i czy zastępuje ToString, więc mógłby wygenerować kod wywołania witryny, aby wykonać opakowanie; alternatywnie może automatycznie generować się dla każdej struktury, która nie zastępuje ToStringmehtoda public override void ToString() { return base.ToString(); }i ...
supercat,
... mieć boksowanie w ramach tej metody. Ponieważ metoda byłaby bardzo krótka, można by ją wprowadzić. Robienie rzeczy z tym drugim podejściem pozwoliłoby na ToString()dostęp do metody struktury przez Reflection, tak jak każda inna i służy do tworzenia statycznego delegata, który przyjmuje typ struktury jako refparametr [taka rzecz działa z niedziedziczonymi metodami struktury], ale ja po prostu próbowałem stworzyć takiego delegata i nie udało się. Czy można utworzyć delegata statycznego dla ToString()metody struktury , a jeśli tak, to jaki powinien być typ parametru?
supercat
Linki są zepsute.
OfirD
5

Wywołanie niewirtualnej metody GetType () na typie wartości:

struct S { };
S s = new S();
s.GetType();
Wiaczesław Iwanow
źródło
2
GetTypewymaga boksu nie tylko dlatego, że nie jest wirtualny, ale dlatego, że lokalizacje przechowywania typu wartości, w przeciwieństwie do obiektów sterty, nie mają „ukrytego” pola, którego GetType()można użyć do identyfikacji ich typu.
supercat
@supercat Hmmm. 1. Boks dodany przez kompilator i ukryte pole używane przez środowisko wykonawcze. Może to być kompilator dodający opakowanie, ponieważ wie o czasie wykonywania… 2. Kiedy wywołujemy niewirtualne ToString (string) na wartości wyliczenia, wymaga to również pakowania i nie wierzę, że kompilator to dodaje, ponieważ wie o szczegółach implementacji Enum.ToString (string) . Tak więc myślę, że mogę powiedzieć, że boks zawsze występował, gdy wywołano metodę niewirtualną na „typ wartości bazowej”.
Wiaczesław Iwanow
Nie rozważałem Enumposiadania własnych metod niewirtualnych, chociaż ToString()metoda dla osoby Enumwymagałaby dostępu do informacji o typie. Zastanawiam się, czy Object, ValueTypeczy Enumma jakieś metody nie wirtualne, które mogą wykonywać swoje zadania bez informacji o typie.
supercat
3

Wspomniane w odpowiedzi Mottiego, ilustrujące tylko przykładami kodu:

Uwzględnione parametry

public void Bla(object obj)
{

}

Bla(valueType)

public void Bla(IBla i) //where IBla is interface
{

}

Bla(valueType)

Ale to jest bezpieczne:

public void Bla<T>(T obj) where T : IBla
{

}

Bla(valueType)

Typ zwrotu

public object Bla()
{
    return 1;
}

public IBla Bla() //IBla is an interface that 1 inherits
{
    return 1;
}

Sprawdzanie nieograniczonego T względem zera

public void Bla<T>(T obj)
{
    if (obj == null) //boxes.
}

Korzystanie z dynamiki

dynamic x = 42; (boxes)

Inny

enumValue.HasFlag

nawfal
źródło
0
  • Korzystanie z kolekcji nieogólnych w System.Collectionstakich jak ArrayListlub HashTable.

To prawda, że ​​są to określone przypadki twojego pierwszego przypadku, ale mogą to być ukryte problemy. To zdumiewające ilość kodu, z którym wciąż się dziś spotykam, w którym są używane zamiast List<T>i Dictionary<TKey,TValue>.

Jesse C. Slicer
źródło
0

Dodanie dowolnej wartości typu wartości do tablicy ArrayList powoduje opakowanie:

ArrayList items = ...
numbers.Add(1); // boxing to object
sll
źródło