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

通过含unsafe引用转义的ref struct转发ref参数是否安全?

问题:使用ref struct封装ref/in/out参数的安全性疑问

我正在开发一个C#增量生成器,作为泛型上下文下托管与非托管回调之间的包装器。这个工具会生成功能与delegate一致的接口,其Invoke方法支持最多16个带或不带返回值的泛型参数(命名逻辑和System.Action、System.Func一致)。

我希望给Invoke方法添加ref限定参数,但和Action/Func的情况一样,哪怕用源生成器,也没法为16个参数生成by-value、ref、in、out的所有排列组合(特意说明最终目标避免XY问题)。

考虑替代方案后,我想到用带ref字段的ref struct来表示任意一种ref分类:用普通字段存储by-value参数(指非ref限定,不一定是ValueType),用readonly ref readonly字段存储ref、in或out参数,具体实现代码如下:

public enum RefCategory
{
    None = 0,
    Ref,
    InRef,
    OutRef
}

public readonly ref struct ParamProxy<T>
{
    [MaybeNull]
    private readonly T obj;
    private readonly ref readonly T _ref;

    public readonly RefCategory RefCategory;

    public static implicit operator ParamProxy<T>(T obj) => new(obj);
    [return: MaybeNull]
    public static implicit operator T(ParamProxy<T> proxy) => proxy.Value;

    public ParamProxy() : this(default!) { }

    public ParamProxy(T obj)
    {
        this.obj = obj;
        _ref = ref Unsafe.NullRef<T>();
        RefCategory = RefCategory.None;
    }

    public ParamProxy(ref T @ref)
    {
        Unsafe.SkipInit(out obj);
        _ref = ref @ref;
        RefCategory = RefCategory.Ref;
    }

    public ParamProxy(in T inRef, object? _ = null)
    {
        Unsafe.SkipInit(out obj);
        _ref = ref inRef;
        RefCategory = RefCategory.InRef;
    }

    public ParamProxy(out T outRef, int _ = 0)
    {
        Unsafe.SkipInit(out obj);
        Unsafe.SkipInit(out outRef);
        _ref = ref Unsafe.AsRef(in outRef);
        RefCategory = RefCategory.OutRef;
    }

    private readonly ref T GetRef(RefCategory category)
    {
        switch (category)
        {
            case RefCategory.None:
                throw new InvalidOperationException("Parameter is not a by-ref parameter");
            case RefCategory.Ref:
                if (RefCategory != RefCategory.Ref)
                {
                    throw new InvalidOperationException("Parameter is not a `ref` parameter");
                }
                break;
            case RefCategory.InRef:
                if ((RefCategory != RefCategory.InRef) && (RefCategory != RefCategory.Ref))
                {
                    throw new InvalidOperationException("Parameter is not an `in` or `ref` parameter");
                }
                break;
            case RefCategory.OutRef:
                if ((RefCategory != RefCategory.OutRef) && (RefCategory != RefCategory.Ref))
                {
                    throw new InvalidOperationException("Parameter is not an `out` or `ref` parameter");
                }
                break;
            default:
                throw new UnreachableException();
        }
        return ref Unsafe.AsRef(in _ref);
    }

    public readonly ref readonly T InRef
    {
        get => ref GetRef(RefCategory.InRef);
    }

    public readonly ref T OutRef
    {
        get => ref GetRef(RefCategory.OutRef);
    }

    public readonly ref T Ref
    {
        get => ref GetRef(RefCategory.Ref);
    }

    [MaybeNull]
    public readonly T Value
    {
        get => RefCategory switch
        {
            RefCategory.None => obj,
            _ => Unsafe.IsNullRef(in _ref) ? default : _ref
        };
    }
}

这个实现借助System.Runtime.CompilerServices.Unsafe,根据构造函数的不同避免初始化obj和/或_ref字段。in和out构造函数带有哑元参数,因为C#不允许仅通过ref分类重载方法/构造函数,但通过默认哑元参数,编译器能明确区分new(ref x)、new(in x)和new(out x)。

用这个代理类型,我的接口可以这样定义Invoke方法:

public ParamProxy<TResult> Invoke(scoped ParamProxy<T1> t1, scoped ParamProxy<T2> t2, scoped ParamProxy<T3> t3);

我的源生成器已经能分析类型信息(T1、T2、T3、TResult等),也能在编译时检查Invoke调用,若使用错误的ref分类就发出诊断提示,可用性不是当前关注点。

我想问的是:这种实现是否存在危险?特别是out参数需要用Unsafe.AsRef来避免“更窄的逃逸范围”错误的部分。

我认为在我的特殊使用场景下,这个实现是可靠安全的,理由如下:

  • ref、in或out参数被传入ParamProxy(ref struct)的构造函数
  • ParamProxy将ref限定参数存储在ref字段中
  • ParamProxy对象作为scoped参数传入Invoke方法
  • Invoke方法将ref字段的值转发给delegate对应的ref、in或out参数
  • delegate在Invoke返回前立即被调用

用户代码调用Invoke的示例:

var getIntValueFromNative = /* ...get interface instance... */;
getIntValueFromNative.Invoke(new(out int value));

源生成器会生成如下Invoke实现:

public void Invoke(scoped ParamProxy<int> param)
{
    handler(out param.OutRef); // `handler` is a `delegate`
}

早期测试结果符合预期,但我担心会无意中造成内存泄漏或栈损坏,希望得到反馈!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 02:39:51