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

Posted

技术标签:

【中文标题】为啥返回带有方法的指针会使测试在调试模式下失败?【英文标题】:Why does returning a pointer with a method makes the test fail in debug mode?为什么返回带有方法的指针会使测试在调试模式下失败? 【发布时间】:2014-12-07 16:41:16 【问题描述】:

当我在发布模式下启动以下测试时,它们都通过了,但在调试模式下它们都失败了。

[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 的值时测试会失败?

【问题讨论】:

【参考方案1】:

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

【讨论】:

以上是关于为啥返回带有方法的指针会使测试在调试模式下失败?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 GHUnit 的异步测试中的错误断言会使应用程序崩溃,而不仅仅是测试失败?

为啥注入的任何 DLL 都会使主机进程崩溃?

为啥我的应用程序在发布模式下崩溃但在调试模式下不崩溃?

为啥 returnValueMap() 返回 NULL

为啥带有 some 方法的三元运算符会使此语句为假?

为啥在调试模式和运行模式下保留计数不同?