Бокс в C #

85

Я пытаюсь собрать все ситуации, в которых происходит бокс на C #:

  • Преобразование типа значения в System.Objectтип:

    struct S { }
    object box = new S();
    
  • Преобразование типа значения в System.ValueTypeтип:

    struct S { }
    System.ValueType box = new S();
    
  • Преобразование значения типа перечисления в System.Enumтип:

    enum E { A }
    System.Enum box = E.A;
    
  • Преобразование типа значения в ссылку на интерфейс:

    interface I { }
    struct S : I { }
    I box = new S();
    
  • Использование типов значений в конкатенации строк C #:

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

    примечание: константы charтипа объединяются во время компиляции

    примечание: начиная с версии 6.0 C # компилятор оптимизирует конкатенация с участием bool, char, IntPtr, UIntPtrтипы

  • Создание делегата из метода экземпляра типа значения:

    struct S { public void M() {} }
    Action box = new S().M;
    
  • Вызов непереопределенных виртуальных методов для типов значений:

    enum E { A }
    E.A.GetHashCode();
    
  • Использование шаблонов констант C # 7.0 в isвыражении:

    int x = …;
    if (x is 42) { … } // boxes both 'x' and '42'!
    
  • Бокс в преобразованиях типов кортежей C #:

    (int, byte) _tuple;
    
    public (object, object) M() {
      return _tuple; // 2x boxing
    }
    
  • Необязательные параметры objectтипа со значениями по умолчанию для типа значения:

    void M([Optional, DefaultParameterValue(42)] object o);
    M(); // boxing at call-site
    
  • Проверка значения неограниченного универсального типа для null:

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

    примечание: это может быть оптимизировано JIT в некоторых средах выполнения .NET

  • Значение structтипового тестирования неограниченного или общего типа с операторами 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);
    

    примечание: это может быть оптимизировано JIT в некоторых средах выполнения .NET

Есть ли еще какие-нибудь боксерские ситуации, может быть, скрытые, о которых вы знаете?

поток управления
источник
2
Некоторое время назад я имел дело с этим и нашел это довольно интересным: Обнаружение (не) бокса с помощью FxCop
Джордж Дакетт
Вы показали очень хорошие способы преобразования. Фактически, я не знал второй или, возможно, никогда не пробовал :) Спасибо
Zenwalker
12
это должен быть вопрос сообщества вики
Sly
2
А как насчет типов, допускающих значение NULL? private int? nullableInteger
allansson 03
1
@allansson, типы, допускающие значение NULL, - это просто типы значений
controlflow

Ответы:

42

Отличный вопрос!

Упаковка происходит ровно по одной причине: когда нам нужна ссылка на тип значения . Все, что вы перечислили, подпадает под это правило.

Например, поскольку объект является ссылочным типом, приведение типа значения к объекту требует ссылки на тип значения, что вызывает упаковку.

Если вы хотите перечислить все возможные сценарии, вы также должны включить производные, такие как возврат типа значения из метода, который возвращает объект или тип интерфейса, потому что это автоматически приводит тип значения к объекту / интерфейсу.

Кстати, случай конкатенации строк, который вы проницательно определили, также происходит от приведения к объекту. Оператор + переводится компилятором в вызов метода Concat строки, который принимает объект для передаваемого вами типа значения, поэтому происходит приведение к объекту и, следовательно, упаковка.

На протяжении многих лет я всегда советовал разработчикам помнить единственную причину бокса (я указал выше) вместо того, чтобы запоминать каждый отдельный случай, потому что список длинный и трудный для запоминания. Это также способствует пониманию того, какой код IL генерирует компилятор для нашего кода C # (например, + в строке вызывает вызов String.Concat). Когда вы сомневаетесь в том, что генерирует компилятор, и если происходит бокс, вы можете использовать дизассемблер IL (ILDASM.exe). Обычно вам следует искать код операции коробки (есть только один случай, когда упаковка может произойти, даже если IL не включает код операции коробки, подробнее ниже).

Но я согласен, что некоторые случаи бокса менее очевидны. Вы перечислили один из них: вызов непереопределенного метода значимого типа. Фактически, это менее очевидно по другой причине: когда вы проверяете код IL, вы видите не код операции блока, а код операции ограничения, поэтому даже в IL не очевидно, что происходит упаковка! Я не буду вдаваться в подробности, почему, чтобы этот ответ не стал еще длиннее ...

Другой случай менее очевидной упаковки - это вызов метода базового класса из структуры. Пример:

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

Здесь ToString переопределяется, поэтому вызов ToString для MyValType не генерирует бокс. Однако реализация вызывает базовый ToString, и это вызывает бокс (проверьте IL!).

Между прочим, эти два неочевидных сценария бокса также вытекают из единственного правила, приведенного выше. Когда метод вызывается в базовом классе типа значения, должно быть что-то, на что должно ссылаться ключевое слово this . Поскольку базовый класс типа значения (всегда) является ссылочным типом, ключевое слово this должно относиться к ссылочному типу, поэтому нам нужна ссылка на тип значения, и поэтому упаковка происходит из-за единственного правила.

Вот прямая ссылка на раздел моего онлайн-курса .NET, в котором подробно обсуждается бокс: http://motti.me/mq

Если вас интересуют только более продвинутые сценарии бокса, вот прямая ссылка там (хотя ссылка выше тоже приведет вас туда, как только она обсудит более простые вещи): http://motti.me/mu

Надеюсь, это поможет!

Мотти

Мотти Шакед
источник
1
Если a ToString()вызывается для определенного типа значения, который не переопределяет его, будет ли тип значения помещен в коробку на сайте вызова или метод будет отправлен (без упаковки) на автоматически сгенерированное переопределение, которое ничего не делает, кроме цепочки (с бокс) к базовому методу?
supercat 07
@supercat Вызов любого метода, который вызывает baseтип значения, вызовет бокс. Это включает виртуальные методы, которые не переопределяются структурой, и Objectметоды, которые вообще не являются виртуальными (например, GetType()). См. Этот вопрос .
afak Gür
@ AfakGür: Я знаю, что конечным результатом будет бокс. Мне было интересно узнать, через какой именно механизм это происходит. Поскольку компилятор, генерирующий IL, может не знать, является ли тип типом значения или ссылкой (это может быть общий), он будет генерировать callvirt независимо. JITter будет знать, является ли тип типом значения и переопределяет ли он ToString, чтобы он мог сгенерировать код сайта вызова для упаковки; в качестве альтернативы, он может автоматически сгенерировать для каждой структуры, которая не отменяет ToStringметод public override void ToString() { return base.ToString(); }и ...
supercat
... чтобы бокс происходил внутри этого метода. Поскольку метод будет очень коротким, его можно будет встроить. Выполнение чего-либо с помощью последнего подхода позволит ToString()получить доступ к методу структуры через Reflection, как и любой другой, и использовать его для создания статического делегата, который принимает тип структуры в качестве refпараметра [такая вещь работает с ненаследуемыми методами структуры], но я просто попытался создать такого делегата, и это не сработало. Можно ли создать статический делегат для ToString()метода структуры , и если да, то каким должен быть тип параметра?
supercat
Ссылки не работают.
OfirD
5

Вызов не виртуального метода GetType () для типа значения:

struct S { };
S s = new S();
s.GetType();
Вячеслав Иванов
источник
2
GetTypeтребует бокса не только потому, что он не виртуальный, но и потому, что места хранения значений типа, в отличие от объектов кучи, не имеют «скрытого» поля, которое GetType()можно использовать для определения их типа.
supercat 07
@supercat Хммм. 1. Бокс, добавленный компилятором, и скрытое поле, используемое средой выполнения. Может быть, компилятор добавляет бокс, потому что он знает о времени выполнения… 2. Когда мы вызываем не виртуальный ToString (строка) для значения перечисления, он также требует бокса, и я не верю, что компилятор добавляет это, потому что он знает о деталях реализации Enum.ToString (строка) . Таким образом, я думаю, я могу сказать, что бокс всегда происходил при вызове невиртуального метода на «типе базового значения».
Вячеслав Иванов
Я не думал о Enumналичии каких-либо собственных невиртуальных методов, хотя ToString()метод для an Enumдолжен иметь доступ к информации о типе. Интересно, есть ли Object, ValueTypeили Enumкакие-либо невиртуальные методы, которые могли бы выполнять свою работу без информации о типе.
supercat
3

Упоминается в ответе Мотти, просто иллюстрируя образцы кода:

Участвующие параметры

public void Bla(object obj)
{

}

Bla(valueType)

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

}

Bla(valueType)

Но это безопасно:

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

}

Bla(valueType)

Тип возврата

public object Bla()
{
    return 1;
}

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

Проверка неограниченного T на нуль

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

Использование динамических

dynamic x = 42; (boxes)

Еще один

enumValue.HasFlag

науфал
источник
0
  • Использование неуниверсальных коллекций, System.Collectionsтаких как ArrayListили HashTable.

Конечно, это конкретные примеры вашего первого случая, но они могут быть скрыты. Удивительно, сколько кода я до сих пор встречаю, в котором они используются вместо List<T>и Dictionary<TKey,TValue>.

Джесси С. Слайсер
источник
0

Добавление любого значения типа значения в ArrayList вызывает бокс:

ArrayList items = ...
numbers.Add(1); // boxing to object
sll
источник