我正在开发一个 C# 增量生成器,充当通用上下文中托管回调和非托管回调之间的包装器。该包装器生成 interface
,其功能与
delegate
的工作方式相同,具有支持最多 16 个带或不带返回类型的泛型类型参数的
Invoke
方法(命名类似于
System.Action
和
System.Func
) ).我希望能够将
ref
限定参数添加到
Invoke
方法中,但出于与
Action
和
Func
相同的原因,根本没有办法生成
by-value
、
ref
的每个排列、
in
和
out
适用于 16 个不同的参数,即使使用源生成器也是如此。 (我正在详细阐述最终目标以避免 XY 问题。) 考虑替代方法,我想到了使用
ref struct
和
ref
字段来表示任何一个可能的
ref
“类别”。我可以使用普通字段来存储“
by-value
”参数(意味着不是
ref
限定;不是必然a
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)
。 (编辑:更正了构造函数详细信息) 此代理类型将允许我的
interface
定义
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<T>
的构造函数(a ref struct
)ParamProxy<T>
ref
限定参数存储在 ref
字段中ParamProxy<T>
Invoke
参数传递给scoped
Invoke
ref
字段的值“转发”到适当的 ref
、in
或 out
的
delegate
参数在
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`
}
抱歉帖子太长。我试图彻底描述这个场景。我的早期测试显示了预期的结果。我担心无意中泄漏内存或损坏堆栈。预先感谢您的任何反馈!
scoped
关键字的更多信息时找到了答案:
低级结构改进 - 更改输出参数的行为。 虽然我仍然相当确定在预期
用例中,out
参数对象不会超出范围,但
可以想象有人可能会滥用它,这就是为什么我不得不这样做仅在
Unsafe.AsRef
构造函数中使用 out
。更重要的是(根据上面的链接),这种用法在语言中是明确禁止的(对于out
参数)。我现在可能得到了正确的结果,但没有什么可以保证运行时或语言中未来的实现更改不会导致此问题。
ref
和
in
构造函数是有效的,特别是因为我没有设置构造函数参数
scoped
。如果参数 是
scoped
,那么将它们存储在 ref
字段中将违反它们的“转义范围”。out
构造函数应该被完全删除,但是用户可以通过声明和
default
初始化一个局部变量,然后通过ref
传递它来达到相同的效果。就源生成器而言, ref
是 out
参数的有效参数,因此这仍然有效。var getIntValueFromNative = /* ...get interface instance... */;
int value = default;
getIntValueFromNative.Invoke(new(ref value)); // `value` is `out` parameter in `handler`
我仍然欢迎对此的任何其他反馈,但我会将其标记为已接受的答案。