自定义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
相关产品推荐
相关产品推荐

