为什么返回带有方法的指针会使测试在调试模式下失败

本文关键字:测试 调试 模式 失败 返回 有方法 指针 为什么 | 更新日期: 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