[2019年更新:由于本文一直是我最喜欢的文章之一,但令人遗憾的是,我在自己的项目中展示的方法已经完全被一个新的、完全不同且更加流畅的技术所取代,我在this answer中详细介绍了这种技术。]
使用 C# 7.0 中的新“
ref return”特性可以使创建和使用运行时动态生成的 get/set 访问器的过程更加简单和语法透明。现在,您不需要使用
DynamicMethod 来发出用于访问字段的单独的
getter 和
setter 函数,而是可以使用一个返回对该字段的托管指针类型引用的单个方法,基本上是一个单个访问器,它(反过来)使得方便、临时的
get和
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();
oic.__refget_m_iPrivate() = the_value + 100;
oic.__refget_m_iPrivate() += 100;
oic.__refget_m_iPrivate()++;
ref int prv = ref oic.__refget_m_iPrivate();
prv++;
foo(ref prv);
prv = 999;
}
在这些示例中所展示的每个操作都操纵 m_iPrivate
原位(即“直接”在其包含实例oic
中)以便于“任何和所有”的更改都能够立即公开显示出来。重要的是要认识到,这意味着尽管被声明为本地变量且具有int类型的prv
不像您典型的“本地”变量那样运行。对于并发代码而言,这一点尤其重要; 不仅更改可以在MyFunction
退出之前可见,而且现在随着C# 7,调用者有能力保留一个ref return 托管指针(作为ref local),从而可以继续修改目标任意长的时间a̲f̲t̲e̲r̲w̲a̲r̲d̲s̲(尽管必须保持在获取ref的堆栈帧之下)。
当然,在这里和其他一般情况下使用托管指针的一个显而易见的优点是,它会继续保持有效(再次强调,仅在其堆栈帧的生命周期内),即使
oic
本身是一个分配在
GC堆中的引用类型实例,在垃圾回收期间可能会被移动。这与原生指针相比是巨大的区别。
如上所述,ref-getter是一个static
扩展方法,可以在任何地方声明和/或使用。但是,如果您能够创建一个派生自OfInterestClass
的类(也就是说,如果OfInterestClass
不是密封类),那么您可以使其更加优美。在派生类中,您可以公开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!
你可以看到,因为像之前的方法一样,属性也具有按引用返回值,所以它的行为几乎与字段完全相同。
现在让我们来说细节。如何创建我上面展示的静态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
The only difference from the by-value getter is that we are using the
ldflda
(load field address) opcode instead of
ldfld
(load field). So if you're well-practiced with
DynamicMethod
it should be no problem, right?
"Wrong!"... unfortunately
DynamicMethod
does not allow a by-ref return value!
If you try to call the
DynamicMethod
constructor specifying a
ByRef
type as the return value...
var dm = new DynamicMethod(
"",
typeof(int).MakeByRefType(),
new[] { typeof(OfInterestClass) },
typeof(OfInterestClass),
true);
......该函数会抛出NotSupportedException
,并显示以下信息:
返回类型包含一些无效类型(例如null、ByRef)
显然,这个函数没有接收到C#7和ref-return的备忘录。幸运的是,我找到了一个简单的解决方法来使它正常工作。如果你将一个非ref类型作为临时“占位符”传递给构造函数,但随后立即使用反射在新创建的DynamicMethod
实例上更改其m_returnType
私有字段,使其成为您实际想要的ByRef类型类型(sic.),那么一切都似乎可以正常工作。
为了加快速度,我将剪切完成的通用方法,通过创建/返回静态ref-getter函数来自动化整个过程,以获取类型为U
、名称为提供的名称、定义在类T
中的私有实例字段。
如果你只想要完整的可工作代码,请从以下位置复制到末尾。
首先我们需要定义一个代表 ref-getter 的委托,因为无法声明使用 ByRef 的 Func<T,TResult>
委托。幸运的是,旧版的 delegate
语法可以做到这一点 (呼!)。
public delegate ref U RefGetter<T, U>(T obj);
将代理与以下静态函数一起放置在一个中央实用类中,以便可以在整个项目中访问它们。这是最终的ref-getter创建函数,可用于在任何类中创建名为“instance”字段的静态ref-getter。请保留HTML标签。
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;
var dm = new DynamicMethod(s_name, typeof(U), new[] { typeof(T) }, typeof(T), true);
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 值的 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);
Delegate.CreateDelegate
吗?你已经有一个带有 getter 的委托。只需调用getter(myInstanceOfT)
就会调用fieldInfo.GetValue
方法并返回值。 - Chris SinclairFunc<S, T> getter = s => (T)fieldInfo.GetValue(s);
是你所能做的,因为字段没有像属性一样的setter\getter方法。如果性能是关键,我建议使用Expression。 - Vyacheslav Volkov