[2019 年编辑: 由于这篇文章一直是我的最爱之一,因此我苦乐参半地注意到,在我自己的项目中,我在这里展示的方法已完全被新的、完全不同,更时尚的技术,我在this answer] 上详细介绍。
使用 C# 7.0 中的新“ref return”功能可以使创建和使用运行时动态生成的 get/set 访问器的过程更加简单,并且在语法上更透明。您现在可以使用一个方法返回一个托管指针 类型对字段的引用,本质上是一个访问器,它(反过来)启用方便的临时 get a̲n̲d̲ set 访问。下面,我提供了一个辅助实用函数,它简化了为任何类中的任意(即私有)实例字段生成 ByRef getter 函数。
➜ 对于“只是代码”,请跳到下面的注释。
作为一个运行示例,假设我们要访问一个私有实例字段m_iPrivate,一个在类OfInterestClass 中定义的int:
public class OfInterestClass
{
private int m_iPrivate;
};
接下来假设我们有一个静态字段“reference-getter”函数,它接受OfInterestClass 实例并使用新的通过引用返回所需的字段值C# 7 “ref return” 能力(下面,我将提供代码以在运行时生成此类函数,通过DynamicMethod):
public static ref int __refget_m_iPrivate(this OfInterestClass obj)
{
/// ...
}
这样一个函数(比方说“ref-getter”)是我们对私有字段进行完全读/写访问所需的全部。在以下示例中,请特别注意 setter 调用操作以及使用(即)++ 和 += 运算符的演示,因为将这些运算符直接应用于 方法调用如果您不熟悉 C#7, 可能看起来有点不寻常。
void MyFunction(OfInterestClass oic)
{
int the_value = oic.__refget_m_iPrivate(); // 'get'
oic.__refget_m_iPrivate() = the_value + 100; // 'set'
/// or simply...
oic.__refget_m_iPrivate() += 100; // <-- yes, you can
oic.__refget_m_iPrivate()++; // <-- this too, no problem
ref int prv = ref oic.__refget_m_iPrivate(); // via "ref-local" in C#7
prv++;
foo(ref prv); // all of these directly affect…
prv = 999; // …field m_iPrivate 'in-situ'
}
重点是,这些示例中显示的每个操作都会操作 m_iPrivate in situ(即,直接在其包含实例 oic 中),以便 任何和所有 em> 更改立即在那里公开可见。重要的是要意识到这意味着prv 尽管是int 类型和本地声明的,但其行为与典型的“本地”变量不同。这对于并发代码尤其重要;不仅更改可见 b̲e̲f̲o̲r̲e̲ MyFunction 已退出,而且现在使用 C# 7,调用者可以保留 引用返回托管指针(作为ref local),从而在任意长时间内继续修改目标a̲f̲t̲e̲r̲wards(尽管必须保持在获取引用堆栈的下方框架,即)。
当然,在这里和其他地方使用托管指针的一个主要且明显的优势是它继续保持有效(同样,在其堆栈帧的生命周期内),即使oic 本身就是一个引用类型在GC 堆中分配的实例——可能在垃圾回收期间被移动。这是与原生指针的巨大差异。
如上所示,ref-getter 是一个static extension method,可以在任何地方声明和/或使用。但是,如果您能够创建自己的派生自OfInterestClass 的类(也就是说,如果OfInterestClass 不是sealed),您可以做得更好。在派生类中,您可以公开 C# 语法以使用基类的私有字段,就好像它是派生类的公共字段一样。为此,只需向您的类添加一个 C# 只读 ref return 属性,它将静态 ref-getter 方法绑定到当前实例 this:
public ref int m_iPrivate => ref __refget_m_iPrivate(this);
这里,属性为public,因此任何人都可以访问该字段(通过对我们派生类的引用)。我们基本上已经公开发布了基类的私有字段。现在,在派生类(或其他地方,视情况而定)中,您可以执行以下任何或所有操作:
int v = m_iPrivate; // get the value
m_iPrivate = 1234; // set the value
m_iPrivate++; // increment it
ref int pi = ref m_iPrivate; // reference as C# 7 ref local
v = Interlocked.Exchange(ref m_iPrivate, 9999); // even do in-situ atomic operations on it!
如您所见,因为 属性 和前面的 方法 一样,也有一个 by reference 返回值,所以它的行为几乎完全一样就像一个字段一样。
现在了解详细信息。你如何创建我上面展示的静态 ref-getter 函数?使用DynamicMethod,这应该是微不足道的。例如,下面是传统(按值)静态 getter 函数的 IL 代码:
// static int get_iPrivate(OfInterestClass oic) => oic.m_iPrivate;
IL_0000: ldarg.0
IL_0001: ldfld Int32 m_iPrivate/OfInterestClass
IL_0006: ret
这是我们想要的 IL 代码(ref-return):
// static ref int refget_iPrivate(OfInterestClass oic) => ref oic.m_iPrivate;
IL_0000: ldarg.0
IL_0001: ldfld̲a Int32 m_iPrivate/OfInterestClass
IL_0006: ret
与按值获取器的唯一区别是我们使用ldflda(加载字段地址)操作码而不是ldfld(加载字段)。所以如果你熟练使用DynamicMethod 应该没问题吧?
错了!...
不幸的是,DynamicMethod 不允许 by-ref 返回值!
如果您尝试调用 DynamicMethod 构造函数并指定 ByRef 类型作为返回值...
var dm = new DynamicMethod(
"", // method name
typeof(int).MakeByRefType(), // by-ref return type <-- ERROR
new[] { typeof(OfInterestClass) }, // argument type(s)
typeof(OfInterestClass), // owner type
true); // private access
...函数抛出 NotSupportedException 并显示以下消息:
返回类型包含一些无效类型(即 null,ByRef)
显然,这个函数没有得到 C#7 上的备忘录和 ref-return。幸运的是,我找到了一个简单的解决方法,让它工作。如果您将非引用类型作为临时“虚拟”传递给构造函数,但随后立即在新创建的 DynamicMethod 实例上使用反射将其 m_returnType 私有字段更改为 ByRef-type 键入您真正想要的 (sic.),然后一切似乎都正常工作。
为了加快速度,我将切入已完成的通用方法,该方法通过为 U 类型的私有实例字段创建/返回静态 ref-getter 函数(具有提供的名称)来自动化整个过程,并且在 T 类中定义。
如果您只想要完整的工作代码,请从此处复制到末尾
首先我们必须定义一个代表 ref-getter 的委托,因为不能声明使用 ByRef 的 Func<T,TResult> 委托。幸运的是,旧的 delegate 语法确实可以做到这一点(phew!)。
public delegate ref U RefGetter<T, U>(T obj);
将委托以及以下静态函数放在一个集中的实用程序类中,在整个项目中都可以访问它们。这是最终的 ref-getter 创建函数,可用于为任何类中的所谓实例字段创建静态 ref-getter。
public static RefGetter<T, U> create_refgetter<T, U>(String s_field)
{
const BindingFlags bf = BindingFlags.NonPublic |
BindingFlags.Instance |
BindingFlags.DeclaredOnly;
var fi = typeof(T).GetField(s_field, bf);
if (fi == null)
throw new MissingFieldException(typeof(T).Name, s_field);
var s_name = "__refget_" + typeof(T).Name + "_fi_" + fi.Name;
// workaround for using ref-return with DynamicMethod:
// a.) initialize with dummy return value
var dm = new DynamicMethod(s_name, typeof(U), new[] { typeof(T) }, typeof(T), true);
// b.) replace with desired 'ByRef' return value
dm.GetType().GetField("m_returnType", bf).SetValue(dm, typeof(U).MakeByRefType());
var il = dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldflda, fi);
il.Emit(OpCodes.Ret);
return (RefGetter<T, U>)dm.CreateDelegate(typeof(RefGetter<T, U>));
}
现在回到本文开头,我们可以轻松地提供启动一切的__refget_m_iPrivate 函数。我们将使用静态 ref-getter 创建函数在运行时创建函数体并将其存储在静态委托类型字段(具有相同签名)中,而不是直接用 C# 编写的静态函数。在实例属性(如上所示,并在下面重复)或其他地方调用它的语法与编译器能够编写函数相同。
最后,要缓存动态创建的 ref-getter 委托,请将以下行放在您选择的任何 static 类中。将OfInterestClass 替换为基类的类型,int 替换为私有字段的字段类型,并更改字符串参数以匹配私有字段的名称。如果您无法创建自己的从OfInterestClass 派生的类(或不想这样做),那么您就完成了;只需将此字段设置为public,您就可以像调用函数一样调用它,传递任何OfInterestClass 实例以获取一个引用,该引用可让您读取、写入或监视其int-valued private 字段“m_iPrivate。 "
// Static delegate instance of ref-getter method, statically initialized.
// Requires an 'OfInterestClass' instance argument to be provided by caller.
static RefGetter<OfInterestClass, int> __refget_m_iPrivate =
create_refgetter<OfInterestClass, int>("m_iPrivate");
或者,如果您想以更简洁或更自然的语法发布隐藏字段,您可以定义自己的(非静态)代理类,该代理类包含一个实例,或者或许更好(如果可能的话),派生自——字段隐藏类OfInterestClass. 而不是部署之前在static 类中全局显示的代码行,而是将其放在您的代理类中,然后还添加以下行:
// optional: ref-getter as an instance property (no 'this' argument required)
public ref int m_iPrivate => ref __refget_m_iPrivate(this);