Задача состоит в том, чтобы написать метод, который инициализирует объявленную переменную подобно использованию параметра out, но используя небезопасный контекст. Небезопасный код ниже работает в C++, но он печатает 0 на С#. Может ли кто-нибудь помочь?
UPD. Код работает в режиме Release.
static void InitBox(out Box box)
{
box = new Box(100);
}
unsafe static void InitBoxUnsafe(Box** box)
{
Box b = new Box(500);
(*box) = &b;
}
static void Main(string[] args)
{
//// The task is to write a code that does the same
// Box box;
// InitBox(out box);
// box.Print();
//// but using unsafe context
//// I wrote this code, but it not working (output is 0), can anyone tell why?
unsafe
{
Box* pBox;
Box** ppBox = &pBox;
InitBoxUnsafe(ppBox);
pBox->Print();
}
}
struct Box
{
private readonly int num;
public Box(int number)
{
num = numberl
}
public void Print()
{
Console.WriteLine(num);
}
}
Код работает только в C++, потому что вы только пытаетесь его с помощью очень простого приложения.
Вы захватываете переменную в стеке, которая будет перезаписана, как только вы войдете в другой стек стека, вызовите много смешных и трудных для отслеживания ошибок.
Безопасная версия С# работает, потому что она фактически не захватывает адрес локальной переменной, а копирует ее значение в рамку выше.
Теперь вы должны это сделать, если явно выделить неуправляемую память для структуры. Конечно, если вы надеетесь сделать это по соображениям производительности, это, скорее всего, сделает вашу работу намного хуже, и она обязательно даст вам массу проблем, связанных с работой с менее и менее безопасным кодом :)
Возможно, также будет возможно просто скопировать значение, а не распространять адрес из небезопасного метода, но я бы не стал спорить с этим.
Итак, главный вопрос: что случилось с out
и ref
? Почему вы пытаетесь использовать unsafe
код для выполнения чего-либо, что безопасный код может делать легко и чисто?
РЕДАКТИРОВАТЬ:
При работе с небезопасным кодом, возможно, стоит взглянуть на реально выпущенный код сборки. Это не дает вам никаких гарантий, поскольку как компилятор С#, так и компилятор JIT могут создавать разные результаты на разных компьютерах (или, возможно, даже на одном и том же компьютере в разное время), но это может дать вам некоторое представление. В этом случае код в сборке может быть упрощен:
Главный:
Box *pBox;
line). Итак, в [ebp - 8] у нас есть pBox
.pBox
в InitBoxUnsafe
, в моем случае через ecx
(так что ecx
теперь имеет значение ebp - 8
).InitBoxUnsafe (вид очевидный, но все же интересный - это не указано):
Box
в верхней части стека (в основном сводится к mov [esp], 0
, mov [esp], 500
- распределение типов значений в стеке очень простое).esp
в eax
- поэтому eax
теперь имеет адрес нашего нового экземпляра Box
.eax
в [ecx]
- поэтому наша переменная pBox
в Main
области теперь имеет адрес локальной переменной в области InitBoxUnsafe
. На этом этапе указатель остается в силе, и поле все еще находится в стеке.Назад к главной:
InitBoxUnsafe
стек InitBoxUnsafe
, esp
теперь возвращается туда, где он был до вызова, - и теперь pBox
имеет адрес выше текущего указателя стека. Значение все равно может быть там, но указатель теперь является незаконным. Конечно, мы находимся в unsafe
коде, поэтому нет никакого шанса нанести нам запястье...Print
выполняется как обычно. Это включает в себя добавление нескольких значений в стек, а самое главное - обратный адрес. Однако это перезаписывает значение *pBox
, поскольку оно указывает на ту же точку в стеке. Поэтому, когда Console.WriteLine
фактически вызывается, он будет распечатывать обратный адрес, а не 500
. Я предполагаю, что на 64-битном уровне это обычно означает 0
, потому что эта часть обратного адреса будет обычно равна нулю, а на 32-битной - обычно будет хлам.Print
inline. Это означает, что стек фактически не перезаписывается до тех пор, пока не вызовет сам Console.WriteLine
, а значение *pBox
будет захвачено задолго до того, как будет выполнен вызов. Тем не менее, почти все, что нажимает на стек между InitBoxUnsafe
и pBox->Print()
также уничтожит значение. Фактически достаточно просто вызывать pBox->Print()
дважды - первый будет печатать 500
, а второй будет печатать адрес потока Console
, например. Или, если вы вызываете только метод, который не имеет локальных переменных или аргументов, он может распечатать обратный адрес.Как вы можете видеть, код действительно не сильно отличается от того, что делает C++ с эквивалентным кодом. Различия между выводами двух (или действительно, запуска этих двух на разных компьютерах или их компиляции в разных компиляторах) связаны с тем, что то, что вы делаете, является незаконным и неопределенным - поведение, на которое вы не должны рассчитывать, Когда-либо.
Теперь, если вы посмотрите на свой второй вариант, где вы передали адрес Main
локальной переменной box
вместо указателя на указатель, весь указатель mumbo-jumbo почти полностью пропущен - теперь InitBoxUnsafe
просто делает mov [&box], 500
напрямую - совершенно законная операция. Фактически, в режиме деблокирования без отладчика, InitBoxUnsafe
теперь можно смело встраивать, полностью избавляясь от вызова - все это теперь в основном компилируется в Box box = (Box)500;
, Вызов " Print
на box
теперь также полностью безопасен, потому что значение теперь находится в полном объеме (прямо в [ebp - 8]
, а не в [[ebp - 8]]
, так сказать).
Ну, серфинг-код под Reflector дал это приятное решение, которое работает в Debug, а также Release
static unsafe void InitBoxUnsafe(Box* box)
{
*(box) = new Box(500);
}
static unsafe void Main(string[] args)
{
Box box;
InitBoxUnsafe(&box);
box.Print(); // (&box)->Print(); // in Reflector
}
out
- совершенно четко определенные и хорошо понимали C # понятие , которое не требует unsafe
. Это плохой код. Вы должны просто использовать out Box box
в сигнатуре метода, box = new Box(500);
в теле и InitBoxUnsafe(out box);
у звонящего. Это затем передавая указатели / ссылки. Просто: безопасно.
out
использованием unsafe
. Просто сырой код без какого-либо смысла. В любом случае спасибо за ваши ценные комментарии.
out
вместоunsafe
, вам не нужноunsafe
здесь вообще