You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Unsafe Array指针指向错误:获取参数地址而非实际值

问题描述

我尝试实现一个自定义泛型Array<T>结构体,代码如下:

internal unsafe struct Array<T>
{
    public int Length;

    internal nuint* ptr;

    public Array(int capacity)
    {
        ptr = (nuint*)Marshal.AllocHGlobal(capacity * sizeof(nuint)); // allocates memory on heap
        Length = capacity;
    }

    public T this[int index]
    {
        get
        {
            if (index > Length || index < 0)
                throw new ArgumentOutOfRangeException(nameof(index));
            nuint PointerToT = ptr[index]; // gets pointer to type
            byte TFirstByte = *(byte*)PointerToT; // gets the first byte of the type
            return Unsafe.As<byte, T>(ref TFirstByte); // converts byte to type

        }
        set
        {
            if (index > Length || index < 0)
                throw new ArgumentOutOfRangeException(nameof(index));
            void* valueAsPtr = Unsafe.AsPointer(ref value); // gets pointer, but gets the wrong address
            ptr[index] = (nuint)valueAsPtr; // sets pointer at index
        }
    }
}

但在setter中获取值的内存地址时,得到的是参数value的地址,而非实际变量的地址。例如使用以下测试代码:

Array<int> myArray = new(1);
int v = 21;
myArray[0] = v;

int* ptr = &v;

ptr获取的地址与setter中得到的地址不一致,且执行Console.WriteLine(myArray[0])取值时,会尝试访问setter中已失效的value参数内存。请问能否获取值被解析为参数前的内存地址?


解决方案

首先明确:无法直接获取传入setter参数前的原始变量地址。原因在于C#的方法参数传递规则:当调用myArray[0] = v时,v的值会被复制到setter的value参数中(值类型是值拷贝,引用类型是引用拷贝),此时value是方法栈上的局部变量,原始变量v的地址在setter内部完全不可见。

你的实现思路存在根本性问题:当前的Array<T>本质是存储指向对象的指针,但对于值类型来说,这种做法不仅毫无意义,还会导致悬垂指针(setter执行完毕后value的栈内存被释放,后续get操作会访问无效内存);对于引用类型,手动管理指针会跳过GC,极易引发内存泄漏。

正确实现思路:直接存储T实例

修改后的代码直接在堆上分配存储T实例的内存,读写操作直接针对实例本身,彻底避免悬垂指针问题:

internal unsafe struct Array<T>
{
    public int Length;
    private T* _data;

    public Array(int capacity)
    {
        Length = capacity;
        // 分配能容纳capacity个T实例的内存
        _data = (T*)Marshal.AllocHGlobal(capacity * sizeof(T));
    }

    public T this[int index]
    {
        get
        {
            if (index < 0 || index >= Length)
                throw new ArgumentOutOfRangeException(nameof(index));
            return _data[index];
        }
        set
        {
            if (index < 0 || index >= Length)
                throw new ArgumentOutOfRangeException(nameof(index));
            _data[index] = value;
        }
    }

    // 必须手动调用释放内存,避免泄漏
    public void Dispose()
    {
        Marshal.FreeHGlobal((nint)_data);
        _data = null;
        Length = 0;
    }
}

这个实现的优势:

  • 对值类型直接拷贝值到堆内存,对引用类型存储对象引用,和.NET原生数组行为一致
  • 不存在悬垂指针问题,读写操作安全
  • 需注意调用Dispose方法手动释放堆内存,避免内存泄漏

特殊场景:存储原始变量指针(不推荐)

如果确实需要跟踪原始变量的实时修改,必须由调用者主动传入指针,而不是通过setter的参数获取:

// 在Array<T>中添加SetPointer方法
public void SetPointer(int index, T* valuePtr)
{
    if (index < 0 || index >= Length)
        throw new ArgumentOutOfRangeException(nameof(index));
    // 存储原始变量的指针,需自行保证变量生命周期
    ptr[index] = (nuint)valuePtr;
}

调用方式:

Array<int> myArray = new(1);
int v = 21;
fixed (int* vPtr = &v)
{
    myArray.SetPointer(0, vPtr);
}

这种方式风险极高,一旦原始变量v超出作用域被回收,存储的指针会变成悬垂指针,访问时会引发程序崩溃或内存错误,仅在特殊场景下谨慎使用。


内容的提问来源于stack exchange,提问作者TheSharkysNL

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 19:10:29