C#中将ref值赋值给out参数的行为是什么?泛型内存读取相关疑问
代码当前的实际运行逻辑
你现在的实现一定会发生值拷贝,不会返回指向原span内存的引用,原因如下:
out T result是值类型的输出参数,本身在栈上分配存储空间,哪怕你使用了result = ref <表达式>的语法,这里的语义也只是「将ref指向的内存内容拷贝到result的存储空间中」,不会改变result本身的存储位置。- 你遇到的CS0177报错本质上是编译器的规则限制:
ref赋值操作不会被识别为对out参数的赋值操作,编译器无法确认你在所有代码路径都给out参数设置了值,所以必须先初始化result = default。
能不能实现返回引用避免拷贝?
可以实现,但是需要修改方法签名,C# 7.0及以上版本支持返回引用类型,你可以改成如下写法:
public unsafe ref T TryRead<T>(ulong address, out bool success) where T : unmanaged { Span<byte> buffer = ReadBytes(address, sizeof(T)); success = !buffer.IsEmpty; if (success) { return ref Unsafe.As<byte, T>(ref buffer.GetPinnableReference()); } return ref Unsafe.NullRef<T>(); }
调用方需要先判断success为true后再使用返回的引用,也可以用C# 11+提供的Nullable引用包装处理空场景。需要特别注意:返回的引用生命周期不能超过原span的生命周期,否则会出现野指针访问。
堆分配和栈分配span的行为差异
不管span是哪种来源,你当前的代码拿到的result都是独立的栈拷贝,和原内存无关,行为没有差异。如果改为返回引用的实现,两者差异如下:
- 当span指向堆上数组的切片时:span内部保存的是GC跟踪指针,GC移动堆数组时会自动更新span的内部指针,但是你通过
Unsafe.As拿到的引用属于非跟踪指针,GC不会主动更新这个引用的地址,如果引用持有期间对应的堆数组被GC移动,这个引用就会变成野指针。 - 当span是
stackalloc分配的栈内存时:对应的内存只在当前方法栈帧的生命周期内有效,方法返回后栈帧被释放,任何指向这块内存的引用都会立即失效,栈内存不受GC管理,不存在被移动的可能。
优化建议
- 如果你的
ReadBytes大部分场景返回的是堆上已有数组的切片,改用返回引用的实现可以完全消除值拷贝,对高频小读取场景的性能提升非常明显。 - 如果
ReadBytes是stackalloc读取远端数据的场景,本身就需要把远端数据拷贝到栈缓冲区,不管用返回值还是返回引用都至少有一次拷贝,你原来的写法不会有额外开销。
内容的提问来源于stack exchange,提问作者TheLeftExit
相关产品推荐
相关产品推荐

