Обобщение и использование интерфейсов без упаковки экземпляров значений - PullRequest
0 голосов
/ 29 ноября 2018

Как я понимаю, дженерики - это элегантное решение для решения проблем с дополнительными процедурами упаковки / распаковки, которые происходят в общих коллекциях, подобных List.Но я не могу понять, как обобщения могут решить проблемы с использованием интерфейсов в обобщенной функции.Другими словами, если я хочу передать экземпляр значения, который реализует интерфейс универсального метода, будет ли выполняться бокс?Как компилятор относится к таким случаям?

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

Вот почему я решил проанализировать код IL простой программы, чтобы увидеть, используются ли какие-либо операции бокса в общей функции:

public class main_class
{
    public interface INum<a> { a add(a other); }
    public struct MyInt : INum<MyInt>
    {
        public MyInt(int _my_int) { Num = _my_int; }
        public MyInt add(MyInt other) => new MyInt(Num + other.Num);
        public int Num { get; }
    }

    public static a add<a>(a lhs, a rhs) where a : INum<a> => lhs.add(rhs);

    public static void Main()
    {
        Console.WriteLine(add(new MyInt(1), new MyInt(2)).Num);
    }
}
** * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *} * * * * * * * * * * * * *} *;}.....Но я был очень удивлен.Вот код IL кода Main:
IL_0000: ldc.i4.1
IL_0001: newobj instance void main_class/MyInt::.ctor(int32)
IL_0006: ldc.i4.2
IL_0007: newobj instance void main_class/MyInt::.ctor(int32)
IL_000c: call !!0 main_class::'add'<valuetype main_class/MyInt>(!!0, !!0)
IL_0011: stloc.0

Такой список не содержит инструкций box.Кажется, что newobj не создает экземпляр значения в куче, для значений он создает их в стеке.Вот описание из документации:

(стандарт ECMA-335 (Common Language Infrastructure) III.4.21) Типы значений обычно не создаются с использованием newobj.Они обычно размещаются либо как аргументы, либо как локальные переменные, используя newarr (для основанных на нуле, одномерных массивов), или как поля объектов.После выделения они инициализируются с помощью initobj.Однако инструкция newobj может использоваться для создания нового экземпляра типа значения в стеке, который затем может быть передан в качестве аргумента, сохранен в локальном и т. Д.

Итак, я решилпроверить функцию add.Это очень интересно, потому что оно также не содержит инструкций коробки:

.method public hidebysig static 
!!a 'add'<(class main_class/INum`1<!!a>) a> (
    !!a lhs,
    !!a rhs
) cil managed 
{
    // Method begins at RVA 0x2050
    // Code size 15 (0xf)
    .maxstack 8

    IL_0000: ldarga.s lhs
    IL_0002: ldarg.1
    IL_0003: constrained. !!a
    IL_0009: callvirt instance !0 class main_class/INum`1<!!a>::'add'(!0)
    IL_000e: ret
} // end of method main_class::'add'

Что не так с моими предположениями?Могут ли дженерики вызывать виртуальные методы значений без упаковки?

1 Ответ

0 голосов
/ 29 ноября 2018

Как я понимаю, дженерики - это элегантное решение для решения проблем с дополнительными процедурами упаковки / распаковки, которые происходят в общих коллекциях, таких как List<T>.

Устранение бокса было задним числомсценарий для дженериков, да.Но, как указывает Дэмиен в комментарии, более общей функцией было включение более лаконичного, более безопасного для типов кода.

, если я хочу передать экземпляр значения, который реализует интерфейс универсального метода,будет ли выполняться бокс?

Иногда да.Но поскольку бокс стоит дорого, CLR ищет способы его избежать.

Я думал, что add(new MyInt(1), new MyInt(2)) будет использовать операции бокса, потому что метод add generic использует интерфейс INum<a>

Я понимаю, почему вы сделали этот вывод, но это неправильно.То, как тело метода, который вы назвали , использует , информация не имеет значения.Вопрос в том, что такое сигнатура метода, который вы вызываете?Вывод типа C # определяет, что вы звоните add<MyInt>, и, следовательно, подпись эквивалентна звонку:

public static MyInt add(MyInt lhs, MyInt rhs)

Теперь вы справедливо указываете на наличие ограничения.Компилятор C # проверяет соблюдение ограничения, которым оно является. Это не меняет соглашение о вызовах метода .Метод занимает два MyInt с, и вы передали ему два MyInt с, и они являются типами значений, поэтому они передаются по значению.

Кажется, что newobj не создаетэкземпляр значения в куче, для значений он создает их в стеке.

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

add делаеттакже не содержит инструкций на коробке

Да, вы их просто не видите.Он содержит ограниченную callvirt, которая является условным блоком.

ограниченная callvirt имеет семантику:

  • должна быть ссылка на получателя в стеке.Там есть: ldarga помещает адрес получателя в стек.Если получатель является ссылочным типом, адрес переменной, содержащей ссылку, будет в стеке.Если это тип значения, то адрес переменной, которая содержит тип значения, будет в стеке.(Опять же, это стек виртуальной машины, о котором мы рассуждаем здесь.)

  • аргументы должны быть в стеке.Они есть;аргументом INum<MyInt>.add является MyInt, и опять же, который передается по значению, и значение находится в стеке из ldarg.

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

  • если получатель является типом значения и тип значения реализует метод, который вы вызываете, то метод типа значения вызывается как обычно: то есть без упаковки значения. Это ваш случай в , поэтому мы избегаем бокса.

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

Что не так с моими предположениями?

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

Могут ли дженерики вызывать виртуальные методы значений без упаковки?

Да.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...