关于 c#:有没有办法创建一个委托来获取和设置 FieldInfo 的值?

Is there a way to create a delegate to get and set values for a FieldInfo?

对于属性有 GetGetMethodGetSetMethod 以便我可以这样做:

1
2
Getter = (Func<S, T>)Delegate.CreateDelegate(typeof(Func<S, T>),
                                             propertyInfo.GetGetMethod());

1
2
Setter = (Action<S, T>)Delegate.CreateDelegate(typeof(Action<S, T>),
                                               propertyInfo.GetSetMethod());

但是我该如何处理 FieldInfos 呢?

我不是在寻找 GetValueSetValue 的代表(这意味着我每次都会调用反射)

1
2
Getter = s => (T)fieldInfo.GetValue(s);
Setter = (s, t) => (T)fieldInfo.SetValue(s, t);

但是如果这里有 CreateDelegate 方法呢?我的意思是,既然赋值返回一个值,我可以把赋值当作一种方法吗?如果是这样,是否有 MethodInfo 句柄?换句话说,我如何将正确的 MethodInfo 设置和从成员字段获取值传递给 CreateDelegate 方法,以便我得到一个可以直接读取和写入字段的委托?

1
2
Getter = (Func<S, T>)Delegate.CreateDelegate(typeof(Func<S, T>), fieldInfo.??);
Setter = (Action<S, T>)Delegate.CreateDelegate(typeof(Action<S, T>), fieldInfo.??);

我可以构建表达式并编译它,但我正在寻找更简单的东西。最后如果问的问题没有答案我不介意走表达路线,如下图:

1
2
3
4
5
6
7
8
var instExp = Expression.Parameter(typeof(S));
var fieldExp = Expression.Field(instExp, fieldInfo);
Getter = Expression.Lambda<Func<S, T>>(fieldExp, instExp).Compile();
if (!fieldInfo.IsInitOnly)
{
    var valueExp = Expression.Parameter(typeof(T));
    Setter = Expression.Lambda<Action<S, T>>(Expression.Assign(fieldExp, valueExp), instExp, valueExp).Compile();
}

或者我是在追求不存在(因为我还没有看到类似的东西)?


正如 Peter Ritchie 所建议的,您可以在运行时编译自己的代码。一旦您第一次调用委托,该方法就会被编译。所以第一次调用会很慢,但是任何后续调用都将尽可能快地在没有非托管指针/联合的情况下进入 .NET。除了第一次调用外,delegate 的速度比 FieldInfo 直接快 500 倍左右。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class DemoProgram
{
    class Target
    {
        private int value;
    }

    static void Main(string[] args)
    {
        FieldInfo valueField = typeof(Target).GetFields(BindingFlags.NonPublic| BindingFlags.Instance).First();
        var getValue = CreateGetter<Target, int>(valueField);
        var setValue = CreateSetter<Target, int>(valueField);

        Target target = new Target();

        setValue(target, 42);
        Console.WriteLine(getValue(target));
    }

    static Func<S, T> CreateGetter<S, T>(FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName +".get_" + field.Name;
        DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(T), new Type[1] { typeof(S) }, true);
        ILGenerator gen = setterMethod.GetILGenerator();
        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldsfld, field);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, field);
        }
        gen.Emit(OpCodes.Ret);
        return (Func<S, T>)setterMethod.CreateDelegate(typeof(Func<S, T>));
    }

    static Action<S, T> CreateSetter<S,T>(FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName+".set_"+field.Name;
        DynamicMethod setterMethod = new DynamicMethod(methodName, null, new Type[2]{typeof(S),typeof(T)},true);
        ILGenerator gen = setterMethod.GetILGenerator();
        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Stsfld, field);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(OpCodes.Stfld, field);
        }
        gen.Emit(OpCodes.Ret);
        return (Action<S, T>)setterMethod.CreateDelegate(typeof(Action<S, T>));
    }
}

请记住,结构是按值传递的。这意味着如果 Action<S, T> 作为第一个参数按值传递,则不能使用 Action<S, T> 更改结构的成员。


[2019 年编辑:由于这篇文章一直是我的最爱之一,因此我在我自己的项目中展示的方法已被一种更新、完全不同且更时尚的技术完全取代,这令人苦恼。 ,我在这个答案中详细说明].

使用新的a€?ref returna€? C# 7.0 中的功能可以使创建和使用运行时动态生成的 get/set 访问器的过程更加简单且语法透明。不必使用 DynamicMethod 来发出单独的 getter 和 setter 函数来访问该字段,您现在可以拥有一个返回对该字段的托管指针类型引用的单一方法,即?-本质上是一个(反过来)启用的单一访问器方便,即席获得 aì2nì2dì2 套访问权限。下面,我提供了一个辅助实用程序函数,它简化了为任何类中的任意(即私有)实例字段生成 ByRef getter 函数。

一个??对于一个€?只是代码,a€?跳到下面的注释。

作为一个运行示例,假设我们要访问一个私有实例字段 m_iPrivate,一个在类 OfInterestClass 中定义的 int:

1
2
3
4
public class OfInterestClass
{
    private int m_iPrivate;
};

接下来假设我们有一个静态字段a€?reference-gettera€?采用 OfInterestClass 实例并使用新的 C# 7 a€?ref returna€? 通过引用返回所需字段值的函数能力(下面,我将提供代码以在运行时通过 DynamicMethod 生成此类函数):

1
2
3
4
public static ref int __refget_m_iPrivate(this OfInterestClass obj)
{
     /// ...
}

这样一个函数(a€?ref-getter,a€? let\\'s say)是我们对私有字段拥有完全读/写访问权限所需要的全部。在以下示例中,请特别注意调用 setter 的操作和使用 (ie) +++= 操作符的演示,因为将这些操作符直接应用于方法调用可能看起来有点不寻常如果你不熟悉 C#7。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 affecta€|
    prv = 999;                                       // a€|field m_iPrivate 'in-situ'
}

重点是,这些示例中显示的每个操作都在原位(即直接在其包含实例 oic 内)操作 m_iPrivate,以便任何和所有更改都立即公开可见。重要的是要意识到这意味着 prv 尽管是 int 类型并在本地声明的,但它的行为不像典型的 a€?locala€?多变的。这对于并发代码尤其重要;不仅更改可见 bì2eì2fì2oì2rì2eì2 MyFunction 已经退出,而且现在在 C# 7 中,调用者能够保留 ref 返回托管指针(作为 ref local),因此可以在任意长的时间内继续修改目标 aì2fì2tì2eì2rì2wards(尽管必须保留在获取引用的堆栈帧下方,即)。

当然,在此处和在 gen?-erala 中的其他地方使用托管指针的一个主要且明显的优势是它继续保持有效(再次,在其堆栈帧的生命周期内),即使 oica"本身是在 GC 堆中分配的引用类型实例",可能会在垃圾收集期间移动。这是与原生指针的巨大差异。

如上所示,ref-getter 是一个 static 扩展方法,可以在任何地方声明和/或使用。但是,如果您能够创建自己的从 OfInterestClass 派生的类(也就是说,如果 OfInterestClass 不是密封的),您可以做得更好。在派生类中,您可以公开 C# 语法以使用基类的私有字段,就好像它是派生类的公共字段一样。为此,只需向您的类添加一个 C# 只读 ref 返回属性,该属性将静态 ref-getter 方法绑定到当前实例 this:

1
public ref int m_iPrivate => ref __refget_m_iPrivate(this);

这里,属性是public,所以任何人都可以访问该字段(通过对我们派生类的引用)。我们基本上已经公开发布了基类的私有字段。现在,在派生类(或其他地方,视情况而定)中,您可以执行以下任何或所有操作:

1
2
3
4
5
6
7
8
9
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 代码:

1
2
3
4
// 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):

1
2
3
4
// static ref int refget_iPrivate(OfInterestClass oic) => ref oic.m_iPrivate;
IL_0000: ldarg.0    
IL_0001: ldfldì2a Int32 m_iPrivate/OfInterestClass
IL_0006: ret

与按值获取器的唯一区别是我们使用 ldflda(加载字段地址)操作码而不是 ldfld(加载字段)。所以如果你熟练使用 DynamicMethod 应该没问题,对吧?

Wrong!... unfortunately DynamicMethod does not allow a by-ref return value!

如果您尝试调用指定 ByRef 类型作为返回值的 DynamicMethod 构造函数...

1
2
3
4
5
6
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 并带有以下消息:

The return Type contains some invalid type (i.e. null, ByRef)

显然,这个函数没有得到 C#7 上的备忘录和 ref-return。幸运的是,我找到了一个简单的解决方法,让它工作。如果您将非 ref 类型作为临时"dummy"传递给构造函数,但随后立即在新创建的 DynamicMethod 实例上使用反射将其 m_returnType 私有字段更改为 ByRef 类型(原文如此。)你真正想要的,那么一切似乎都很好。

为了加快速度,我将切入已完成的通用方法,该方法通过为具有提供的名称的 U 类型的私有实例字段创建/返回静态 ref-getter 函数来自动化整个过程,并在类 T.

中定义

如果您只想要完整的工作代码,请从此处复制到末尾

首先我们必须定义一个代表 ref-getter 的委托,因为不能声明具有 ByRef 用法的 Func<T,TResult> 委托。幸运的是,旧的 delegate 语法确实可以做到这一点(呸!)。

1
public delegate ref U RefGetter<T, U>(T obj);

将委托以及以下静态函数放在一个集中的实用程序类中,在整个项目中都可以访问它们。这是最终的 ref-getter 创建函数,可用于为任何类中的所谓实例字段创建静态 ref-getter。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 值的 private 字段 "m_iPrivate ."

1
2
3
4
// 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 类中全局显示的代码行,而是将其放置在您的代理类中,然后还添加以下行:

1
2
// optional: ref-getter as an instance property (no 'this' argument required)
public ref int m_iPrivate => ref __refget_m_iPrivate(this);


字段访问不是通过方法(如 getter 和 setter)执行的——它是通过 IL 指令执行的——所以没有什么可以分配给委托的。您必须使用表达式路由来创建可以分配给委托的代码"块"(实际上是 IL)。


不,没有简单的方法可以创建一个委托来获取/设置一个字段。

您必须编写自己的代码来提供该功能。我建议在共享库中使用两个函数来提供此功能。

使用你的代码(在这个例子中我只展示了 get-delegate 的创建):

1
2
3
4
5
6
7
8
9
static public class FieldInfoExtensions
{
    static public Func<S, T> CreateGetFieldDelegate<S, T>(this FieldInfo fieldInfo)
    {
        var instExp = Expression.Parameter(typeof(S));
        var fieldExp = Expression.Field(instExp, fieldInfo);
        return Expression.Lambda<Func<S, T>>(fieldExp, instExp).Compile();
    }
}

这使得从 FieldInfo 创建一个 get-delegate 变得很容易(假设该字段是 int 类型):

1
Func<MyClass, int> getter = typeof(MyClass).GetField("MyField").CreateGetFieldDelegate<MyClass, int>();

或者如果我们稍微修改一下你的代码:

1
2
3
4
5
6
7
8
9
static public class TypeExtensions
{
    static public Func<S, T> CreateGetFieldDelegate<S, T>(this Type type, string fieldName)
    {
        var instExp = Expression.Parameter(type);
        var fieldExp = Expression.Field(instExp, fieldName);
        return Expression.Lambda<Func<S, T>>(fieldExp, instExp).Compile();
    }
}

这使它变得更加容易:

1
Func<MyClass, int> getter = typeof(MyClass).CreateGetFieldDelegate<MyClass, int>("MyField");

也可以使用 IL 创建这些委托,但如果有的话,代码会更复杂,性能也不高。


DynamicMethod 中的

ref return 限制似乎至少在 v4.0.30319 上消失了。

Glenn Slayden 使用来自微软文档的信息修改了这个答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using System.Reflection;
using System.Reflection.Emit;

namespace ConsoleApp {
    public class MyClass {
        private int privateInt = 6;
    }

    internal static class Program {
        private delegate ref TReturn OneParameter<TReturn, in TParameter0>(TParameter0 p0);

        private static void Main() {
            var myClass = new MyClass();

            var method = new DynamicMethod(
               "methodName",
                typeof(int).MakeByRefType(), // <- MakeByRefType() here
                new[] {typeof(MyClass)},
                typeof(MyClass),
                true); // skip visibility


            const BindingFlags bindings = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;

            var il = method.GetILGenerator();
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldflda, typeof(MyClass).GetField("privateInt", bindings));
            il.Emit(OpCodes.Ret);

            var getPrivateInt = (OneParameter<int, MyClass>) method.CreateDelegate(typeof(OneParameter<int, MyClass>));

            Console.WriteLine(typeof(string).Assembly.ImageRuntimeVersion);
            Console.WriteLine(getPrivateInt(myClass));
        }
    }
}

输出:

1
6

我不知道你是否会使用 Expression,那为什么要避免反射呢? Expression 的大多数操作都依赖于反射。

GetValueSetValue 本身就是 get methodset method,用于字段,但它们不用于任何特定字段。

字段与属性不同,它们是字段,没有理由为每个字段生成 get/set 方法。但是,类型可能会随着不同的字段而变化,因此 GetValueSetValue 被定义为 parameter/return valueobject 以表示差异。 GetValue 甚至是一个抽象方法,也就是说,对于覆盖它的每个类(仍然是反射),必须在相同的签名内。

如果你不输入它们,那么下面的代码应该可以:

1
2
3
4
public static void SomeMethod(FieldInfo fieldInfo) {
    var Getter=(Func<object, object>)fieldInfo.GetValue;
    var Setter=(Action<object, object>)fieldInfo.SetValue;
}

但如果你愿意,有一个受限的方式:

1
2
3
4
5
6
public static void SomeMethod<S, T>(FieldInfo fieldInfo)
    where S: class
    where T: class {
    var Getter=(Func<S, object>)fieldInfo.GetValue;
    var Setter=(Action<S, T>)fieldInfo.SetValue;
}

因为 Getter 仍然是 Func<S, object>,你可能想看看:

C# 中的协变和逆变,第三部分:Lippert 先生博客上的方法组转换方差。


只是为了添加更多的方法:D

1
2
3
4
5
 public static Func<T, TResult> CreatePropertyOrFieldReaderDelegate<T, TResult>(string field)
        {
            var input = Expression.Parameter(typeof(T));
            return Expression.Lambda<Func<T, TResult>>(Expression.PropertyOrField(input, field), input).Compile();
        }

这将创建一个返回值的方法..

测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
class Testing {
  public int Data = 2;
  public string PropData { get; } ="Default";
 }


  [Fact]
  public void CreateSingleFieldReader()
        {
            var a = ReflectionHelper.CreatePropertyOrFieldReaderDelegate<Testing, int>("Data");
            Assert.Equal(2, a(new Testing()));

        }

这是在处理对象时创建委托的另一种选择(不知道字段的特定类型)。虽然如果 field 是一个结构(因为装箱),它会更慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static class ReflectionUtility
{
    public static Func<object, object> CompileGetter(this FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName +".get_" + field.Name;
        DynamicMethod setterMethod = new DynamicMethod(methodName, typeof(object), new[] { typeof(object) }, true);
        ILGenerator gen = setterMethod.GetILGenerator();
        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldsfld, field);
            gen.Emit(field.FieldType.IsClass ? OpCodes.Castclass : OpCodes.Box, field.FieldType);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Castclass, field.DeclaringType);
            gen.Emit(OpCodes.Ldfld, field);
            gen.Emit(field.FieldType.IsClass ? OpCodes.Castclass : OpCodes.Box, field.FieldType);
        }
        gen.Emit(OpCodes.Ret);
        return (Func<object, object>)setterMethod.CreateDelegate(typeof(Func<object, object>));
    }

    public static Action<object, object> CompileSetter(this FieldInfo field)
    {
        string methodName = field.ReflectedType.FullName +".set_" + field.Name;
        DynamicMethod setterMethod = new DynamicMethod(methodName, null, new[] { typeof(object), typeof(object) }, true);
        ILGenerator gen = setterMethod.GetILGenerator();
        if (field.IsStatic)
        {
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(field.FieldType.IsClass ? OpCodes.Castclass : OpCodes.Unbox_Any, field.FieldType);
            gen.Emit(OpCodes.Stsfld, field);
        }
        else
        {
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Castclass, field.DeclaringType);
            gen.Emit(OpCodes.Ldarg_1);
            gen.Emit(field.FieldType.IsClass ? OpCodes.Castclass : OpCodes.Unbox_Any, field.FieldType);
            gen.Emit(OpCodes.Stfld, field);
        }
        gen.Emit(OpCodes.Ret);
        return (Action<object, object>)setterMethod.CreateDelegate(typeof(Action<object, object>));
    }
}