为什么返回带有方法的指针会使测试在调试模式下失败
本文关键字:测试 调试 模式 失败 返回 有方法 指针 为什么 | 更新日期: 2023-09-27 18:33:23
当我在发布模式下启动以下测试时,它们都通过了,但在调试模式下它们都失败了。
[TestFixture]
public unsafe class WrapperTests
{
[Test]
public void should_correctly_set_the_size()
{
var wrapper = new Wrapper();
wrapper.q->size = 1;
Assert.AreEqual(1, wrapper.rep()->size); // Expected 1 But was: 0
}
[Test]
public void should_correctly_set_the_refcount()
{
var wrapper = new Wrapper();
Assert.AreEqual(1, wrapper.rep()->refcount); // Expected 1 But was:508011008
}
}
public unsafe class Wrapper
{
private Rep* q;
public Wrapper()
{
var rep = new Rep();
q = &rep;
q->refcount = 1;
}
public Rep* rep()
{
return q;
}
}
public unsafe struct Rep
{
public int refcount;
public int size;
public double* data;
}
但是,如果我删除 rep(( 方法并使 q 指针公开,则测试在调试和发布模式下都会通过。
[TestFixture]
public unsafe class WrapperTests
{
[Test]
public void should_correctly_set_the_size()
{
var wrapper = new Wrapper();
wrapper.q->size = 1;
Assert.AreEqual(1, wrapper.q->size);
}
[Test]
public void should_correctly_set_the_refcount()
{
var wrapper = new Wrapper();
Assert.AreEqual(1, wrapper.q->refcount);
}
}
public unsafe class Wrapper
{
public Rep* q;
public Wrapper()
{
var rep = new Rep();
q = &rep;
q->refcount = 1;
}
}
public unsafe struct Rep
{
public int refcount;
public int size;
public double* data;
}
我不明白什么会导致这种行为?
为什么当我使用方法返回 q 的值时测试失败?
Rep
是一个结构体,因此var rep = new Rep();
会将rep
数据存储在堆栈上(当前堆栈帧是构造函数调用(。
q = &rep;
将得到一个指向rep
的指针,因此q
指向堆栈上的数据。这是这里真正的问题,因为一旦构造函数退出,它使用的堆栈空间就被认为是自由和可重用的。
在调试模式下调用rep()
时,会创建更多堆栈帧。其中一个覆盖q
指针指向的地址处的数据。
在发布模式下,对 rep()
的调用由 JIT 内联,并且创建的堆栈帧更少。但是问题仍然存在,它只是隐藏在您的示例中,因为您没有进行足够的函数调用。
例如,此测试在发布模式下无法通过,只是因为调用Split
:
[Test]
public void should_correctly_set_the_refcount()
{
var wrapper = new Wrapper();
"abc,def".Split(',');
Assert.AreEqual(1, wrapper.rep()->refcount);
}
作为一般规则,您永远不应该让指针超过它们指向的数据。
若要解决此问题,可以分配一些非托管内存,如下所示:
public unsafe class Wrapper
{
public Rep* q;
public Wrapper()
{
q = (Rep*)Marshal.AllocHGlobal(sizeof(Rep));
q->refcount = 1;
q->size = 0;
q->data = null;
}
~Wrapper()
{
Marshal.FreeHGlobal((IntPtr)q);
}
public Rep* rep()
{
return q;
}
}
这通过了您的所有测试。
需要注意的几点:
- 有一个终结器可以释放内存
- 内存不会被 GC 移动,就像它被固定一样
-
AllocHGlobal
不会将分配的内存清零,因此如果需要,应手动清除结构字段,或者如果结构很大,则应使用 P/Invoke 调用ZeroMemory
。