Где С# хранит виртуальную таблицу структуры при десортировке с использованием [StructLayout(LayoutKind.Sequential)]

У меня есть устройство, которое передает двоичные данные. Для интерпретации данных я определил struct, соответствующий формату данных. struct имеет атрибут StuctLayoutAttribute с LayoutKind.Sequential. Это работает, как и ожидалось, например:

[StructLayout(LayoutKind.Sequential)]
struct DemoPlain
{
     public int x;
     public int y;
}

Marshal.OffsetOf<DemoPlain>("x");    // yields 0, as expected
Marshal.OffsetOf<DemoPlain>("y");    // yields 4, as expected
Marshal.SizeOf<DemoPlain>();         // yields 8, as expected

Теперь я хочу рассматривать одну структуру как другую структуру, поэтому я поэкспериментировал со структурой, реализующей интерфейс:

interface IDemo
{
    int Product();
}


[StructLayout(LayoutKind.Sequential)]
struct DemoWithInterface: IDemo
{
     public int x;
     public int y;
     public int Product() => x * y;
}

Marshal.OffsetOf<DemoWithInterface>("x").Dump();    // yields 0
Marshal.OffsetOf<DemoWithInterface>("y").Dump();    // yields 4
Marshal.SizeOf<DemoWithInterface>().Dump();         // yields 8

К моему удивлению, смещения и размер DemoWithInterface остаются такими же, как DemoPlain, и преобразование тех же двоичных данных с устройства либо в массив DemoPlain, либо в массив DemoWithInterface работает. Как это возможно?

Реализации C++ часто используют виртуальную таблицу (см. Где в памяти хранится виртуальная таблица?) наболеть виртуальными методами. Я считаю, что в C# методы, опубликованные в интерфейсе, и методы, объявленные virtual, аналогичны виртуальным методам в C++ и что для поиска правильного метода требуется что-то похожее на виртуальную таблицу. Это правильно или С# делает это совершенно по-другому? Если правильно, где хранится структура, подобная vtable? Если отличается, как реализован C# в отношении наследования интерфейсов и виртуальных методов?


person Kasper van den Berg    schedule 06.04.2018    source источник
comment
похожи на виртуальные методы в C++ - нет, не могут быть. Структуры не поддерживают наследование и, следовательно, не поддерживают виртуальное/переопределение. И даже на классе интерфейс это нечто другое и не подразумевает виртуальный.   -  person Henk Holterman    schedule 06.04.2018
comment
Поскольку структуры не могут быть унаследованы, нет необходимости в указателе vtable в самой структуре, даже если она реализует интерфейс. Допустим, у вас есть var s = new DemoWithInterface(); s.Product(). Здесь не нужна виртуальная диспетчеризация, можно вызвать только один конкретный метод. А вот если структура упакована - тогда другое дело и такая запись есть, но коробочная структура - это не то же самое, что неупакованная. Так вот: IDemo s = new DemoWithInterface(); s.Product() нужна виртуальная диспетчеризация, но структура упакована и представлена ​​по-другому (не только простые поля).   -  person Evk    schedule 06.04.2018


Ответы (2)


В основном "не относится". Структуры в C#, как уже обсуждалось, не поддерживают наследование, поэтому v-таблица не требуется.

Макет поля это макет поля. Это просто: где фактические поля. Реализация интерфейсов вообще не меняет поля и не требует никаких изменений в макете. Вот почему размер и макет не затронуты.

Существуют существуют некоторые виртуальные методы, которые структуры могут (и обычно должны) переопределять — ToString() и т. д. Так что вы можете с полным основанием спросить: "Итак, как это работает?" - и ответ: дым и зеркала. Также известен как вызов с ограничениями. Это откладывает вопрос «виртуальный вызов против статического вызова» до JIT. JIT имеет полное представление о том, переопределен метод или нет, и может генерировать соответствующие коды операций — либо блок и виртуальный вызов (блок — это объект, поэтому имеет v-таблицу), либо прямой статический вызов.

Может возникнуть соблазн подумать, что это должен делать компилятор, а не JIT, но часто структура находится во внешней сборке, и было бы катастрофой, если бы компилятор выдал статический вызов, потому что он мог видеть переопределенный ToString() и т. д., а затем кто-то обновляет библиотеку, не перестраивая приложение, и получает версию, которая не переопределяет (MissingMethodException), поэтому ограниченный вызов более надежен. И делать то же самое даже для типов в сборке просто проще и проще в поддержке.

Этот ограниченный вызов также происходит для универсальных (<T>) методов, поскольку T может быть struct. Напомним, что JIT выполняется по T для T с типизированным значением в универсальном методе, поэтому он может применять эту логику для каждого типа и запекать в фактических известных местоположениях статических вызовов. И если вы используете что-то вроде .ToString(), а ваша T является структурой, которая не переопределяет это: вместо этого она будет создавать бокс и виртуальный вызов.

Обратите внимание, что после того, как вы присвоите структуру переменной interface, например:

DemoWithInterface foo = default;
IDemo bar = foo;
var i = bar.Product();

вы «упаковали» его, и теперь все виртуальные вызовы на коробке. box содержит полную v-таблицу. Вот почему универсальные методы с ограничениями универсального типа часто предпочтительнее:

DemoWithInterface foo = default;
DoSomething(foo);

void DoSomething<T>(T obj) where T : IDemo
{
    //...
    int i = obj.Product();
    //...
}

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

person Marc Gravell    schedule 11.04.2018

Поведение маршалинга по умолчанию | Документы Microsoft и особенно раздел Типы значений, используемые в вызове платформы дает ответ:

При маршалинге в неуправляемый код эти форматированные типы маршалируются как структуры в стиле C.

а также

Когда форматированный тип маршалируется как структура, доступны только поля внутри типа. Если у типа есть методы, свойства или события, они недоступны из неуправляемого кода.

Таким образом, (виртуальные) методы С# struct удаляются, и передается только простая C-структура. В случае OP устройство отправляет байты, которые содержат простую C-структуру, Marshal.PtrToStructure<T>(IntPtr) преобразует байты в C#-структуру и, в случае DemoWithInterface, прикрепляет метод Product и vtable (или другие метаданные), чтобы структура реализовывала IDemo.

person Kasper van den Berg    schedule 06.04.2018
comment
Как указано в комментариях, C# structs не может иметь виртуальные методы, поэтому интересно, где они находятся, немного неправильно. - person Damien_The_Unbeliever; 06.04.2018
comment
Это не очень точно, типы структур имеют виртуальные методы (ToString, Equals, GetHashCode), и их переопределение нормально. Никак не меняет результат этого кода. Это нехороший Q + A, моделирование плоского мира путем попытки сопоставить систему типов .NET с C ++ бесполезно и ничего не делает, кроме укоренения плохой ментальной модели. Если вы хотите, чтобы кто-то написал книгу, подумайте о назначении вознаграждения за этот вопрос, иначе всем будет лучше без этого Q + A. - person Hans Passant; 07.04.2018
comment
Набор вознаграждений @HansPassant, я надеюсь узнать больше; Я, вероятно, также должен улучшить вопрос. В настоящее время я понятия не имею, как его улучшить, я подумаю, как переформулировать вопрос. - person Kasper van den Berg; 10.04.2018