【问题标题】:Is it possible to access backing fields behind auto-implemented properties?是否可以访问自动实现属性后面的支持字段?
【发布时间】:2012-02-07 16:17:45
【问题描述】:

我知道我可以对属性使用详细语法:

private string _postalCode;

public string PostalCode
{
    get { return _postalCode; }
    set { _postalCode = value; }
}

或者我可以使用自动实现的属性。

public string PostalCode { get; set; }

我能否以某种方式访问​​自动实现属性背后的支持字段? (在本例中为 _postalCode)。


编辑:我的问题不是关于设计,而是关于,比方说,这样做的理论能力。

【问题讨论】:

  • 很好奇!!你为什么要那个?
  • 对实现进行编程是危险的。
  • 您可以通过私有反射访问它,但这是个坏主意。
  • 您在下面的答案中获得了官方参考。但是让我们说你被允许访问_postalCode,后来他们把它改成了__postalCode。呃!!!运行代码中断。
  • @Andrey:您可以使用private 修饰符来限制对其中一个访问器的访问(通常,set 在这种情况下是私有的)。但是由于您已经选择了“更懒惰”的自动实现版本,我认为没有可能的理由想要访问支持字段。

标签: c#


【解决方案1】:

我获取汽车属性后备字段FieldInfo的方法:

public static FieldInfo? GetAutoPropertyBackingField(this PropertyInfo pi, bool strictCheckIsAutoProperty = false)
{
    if (strictCheckIsAutoProperty && !StrictCheckIsAutoProperty(pi)) return null;

    var gts = pi.DeclaringType?.GetGenericArguments();
    var accessor = pi.GetGetMethod(true);
    var msilBytes = accessor?.GetMethodBody()?.GetILAsByteArray();
    var rtk = null != msilBytes
        ? accessor!.IsStatic
            ? GetAutoPropertyBakingFieldMetadataTokenInGetMethodOfStatic  (msilBytes)
            : GetAutoPropertyBakingFieldMetadataTokenInGetMethodOfInstance(msilBytes)
        : -1;

    accessor = pi.GetSetMethod(true);
    msilBytes = accessor?.GetMethodBody()?.GetILAsByteArray();
    if (null != msilBytes)
    {
        var wtk = accessor!.IsStatic
            ? GetAutoPropertyBakingFieldMetadataTokenInSetMethodOfStatic  (msilBytes)
            : GetAutoPropertyBakingFieldMetadataTokenInSetMethodOfInstance(msilBytes);

        if (-1 != wtk)
        {
            if (wtk == rtk)
            {
                var wfi = pi.Module.ResolveField(wtk, gts, null);
                if (!strictCheckIsAutoProperty || null == wfi || StrictCheckIsAutoPropertyBackingField(pi, wfi)) return wfi;
            }
            return null;
        }
    }

    if (-1 == rtk) return null;

    var rfi = pi.Module.ResolveField(rtk, gts, null);
    return !strictCheckIsAutoProperty || null == rfi || StrictCheckIsAutoPropertyBackingField(pi, rfi) ? rfi : null;
}

private static bool StrictCheckIsAutoProperty            (PropertyInfo pi)               => null != pi.GetCustomAttribute<CompilerGeneratedAttribute>();
private static bool StrictCheckIsAutoPropertyBackingField(PropertyInfo pi, FieldInfo fi) => fi.Name == "<" + pi.Name + ">k__BackingField";

private static int GetAutoPropertyBakingFieldMetadataTokenInGetMethodOfStatic  (byte[] msilBytes) => 6 == msilBytes.Length &&                                                 0x7E == msilBytes[0] && 0x2A == msilBytes[5] ? BitConverter.ToInt32(msilBytes, 1) : -1;
private static int GetAutoPropertyBakingFieldMetadataTokenInSetMethodOfStatic  (byte[] msilBytes) => 7 == msilBytes.Length &&                         0x02 == msilBytes[0] && 0x80 == msilBytes[1] && 0x2A == msilBytes[6] ? BitConverter.ToInt32(msilBytes, 2) : -1;
private static int GetAutoPropertyBakingFieldMetadataTokenInGetMethodOfInstance(byte[] msilBytes) => 7 == msilBytes.Length && 0x02 == msilBytes[0]                         && 0x7B == msilBytes[1] && 0x2A == msilBytes[6] ? BitConverter.ToInt32(msilBytes, 2) : -1;
private static int GetAutoPropertyBakingFieldMetadataTokenInSetMethodOfInstance(byte[] msilBytes) => 8 == msilBytes.Length && 0x02 == msilBytes[0] && 0x03 == msilBytes[1] && 0x7D == msilBytes[2] && 0x2A == msilBytes[7] ? BitConverter.ToInt32(msilBytes, 3) : -1;

最后 6 个单行方法的代码看起来可能有点乱,因为浏览器使用的是非固定宽度的字体。

代码进行了非常严格和准确的检查以查找(自动)属性的支持字段。如果您不应用严格检查,它还可以找出与自动属性的实现相同的手写简单普通属性的支持字段。

2 种严格检查方法适用于 M$ dotnetfx 运行时。而且这些规则很可能在未来不会改变,因为它目前运行良好,并且在可预见的未来。

关键代码使用编译器生成的 MSIL 字节在最后 4 个方法中查找 auto 属性的支持字段。它们适用于 M$ 的 dotnetfx4x 和 dotnet5,可能还适用于所有 M$ 的 dotnetfx 运行时。

如果您将它与 mono 或其他框架一起使用,您可以使用 dnSpy 或其他类似工具查看编译器发出的自动属性的属性、支持字段的名称和 setter/getter 的 IL 字节,然后修改 6 个单行方法以适应它们。当然,您可以添加一些其他严格的检查,以确保代码在您的程序运行的 fx 上正常工作。

【讨论】:

    【解决方案2】:

    我不了解你,但我在其他公司的项目中编写过代码,现在我想知道我是怎么做到的!因此,在网络上搜索答案通常会更快,它把我带到了这里。

    但是,我的理由不同。我是单元测试,不在乎纯粹主义者要说什么,但作为单元测试设置的一部分,我试图为给定对象调用某个状态。但是这种状态应该在内部得到控制。我不希望其他开发人员不小心弄乱了状态,这可能会对系统产生深远的影响。所以一定要私设!然而,你如何在不调用(希望)永远不会发生的行为的情况下对类似的东西进行单元测试?在这种情况下,我相信在单元测试中使用反射很有用。

    另一种方法是暴露我们不想暴露的东西,这样我们就可以对它们进行单元测试!是的,我在现实生活环境中看到过这一点,光是想想还是让我摇头。

    所以,我希望下面的代码可能有用。

    这里有两种方法只是为了分离关注点,实际上,也有助于提高可读性。反射对于大多数开发人员来说都是令人头晕目眩的东西,根据我的经验,他们要么回避它,要么像瘟疫一样避免它!

    private string _getBackingFieldName(string propertyName)
    {
        return string.Format("<{0}>k__BackingField", propertyName);
    }
    
    private FieldInfo _getBackingField(object obj, string propertyName)
    {
        return obj.GetType().GetField(_getBackingFieldName(propertyName), BindingFlags.Instance | BindingFlags.NonPublic);
    }
    

    我不知道您使用的代码约定是什么,但就个人而言,我喜欢帮助方法是私有的并且以小写字母开头。我看的时候觉得不够明显,所以我也喜欢前面的下划线。

    讨论了支持字段及其自动命名。出于单元测试的目的,您很快就会知道它是否已更改!它对你的真实代码也不会是灾难性的,只是测试。所以我们可以对名字的命名做出简单的假设——正如我上面所说的。你可以不同意,这很好。

    更难的助手_getBackingField 返回其中一种反射类型,FieldInfo。我在这里也做了一个假设,你所追求的支持字段来自一个实例对象,而不是静态对象。如果您愿意,您可以将其分解为要传递的参数,但对于可能想要功能但不理解的普通开发人员来说,水域肯定会更加混乱。

    FieldInfos 的方便之处在于,它们可以在与FieldInfo 匹配的对象上设置字段。最好用一个例子来解释:

    var field = _getBackingField(myObjectToChange, "State");
    field.SetValue(myObjectToChange, ObjectState.Active);
    

    在这种情况下,该字段是一个名为ObjectState 的枚举类型。名称已更改以保护无辜者!因此,在第二行中,您可以看到通过访问之前返回的FieldInfo,我可以调用SetValue 方法,您可能认为该方法应该已经与您的对象相关,但事实并非如此!这就是反射的本质——FieldInfo 将字段与它的来源分开,因此您必须告诉它要使用哪个实例 (myObjectToChange),因此,您希望它具有的值,在这种情况下,@ 987654333@.

    长话短说,面向对象的编程将阻止我们做诸如访问私有字段之类的讨厌的事情,更糟糕的是,当代码的开发者不打算改变它们时。哪个好!这就是 C# 如此有价值并受到开发人员喜爱的原因之一。

    然而,微软给了我们反射,通过它,我们使用了一把强大的武器。它可能很丑,而且很慢,但同时,它暴露了 MSIL(MicroSoft 中间语言)内部工作的最深处——简称 IL——并且使我们几乎可以打破书中的每一条规则,这是一个很好的例子。

    【讨论】:

    • 伙计,您是单元测试实现而不是行为,即进行白盒测试。单元测试应该可以帮助你,例如当您想更改实现而不是行为时。一条建议是将您的测试对象视为黑匣子。使用它的方法和不同的模拟来获得你想要的状态。只是我的两分钱。
    • +1!按预期工作,也在 get-only 属性上。但是您应该添加一些信息以更改struct 变量中的支持字段:SetValue() 接受一个对象,因此它将结构变量装箱并更改副本上的字段:stackoverflow.com/a/27226969。因此,您需要自己将变量装箱,然后将编辑后的装箱变量分配回原始变量。在我的情况下,当我必须在调试期间编辑结构时,创建新结构并将其分配给变量确实更容易。
    【解决方案3】:

    更新:https://github.com/jbevain/mono.reflection 带有一个支持字段解析器方法,该方法适用于 C#、VB.NET 和 F# 生成的自动属性。 NuGet 包位于https://www.nuget.org/packages/Mono.Reflection/

    ORIGINAL:我最终得到了这种仅适用于 C# 自动属性的相当灵活的方法。正如其他答案所表明的那样,如果编译器实现使用&lt;PropertyName&gt;k__BackingField 以外的支持字段命名方案,这不是可移植的并且将不起作用。据我所知,目前所有 C# 编译器的实现都使用这种命名方案。 VB.NET 和 F# 编译器使用了另一种不适用于此代码的命名方案。

    private static FieldInfo GetBackingField(PropertyInfo pi) {
        if (!pi.CanRead || !pi.GetGetMethod(nonPublic:true).IsDefined(typeof(CompilerGeneratedAttribute), inherit:true))
            return null;
        var backingField = pi.DeclaringType.GetField($"<{pi.Name}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic);
        if (backingField == null)
            return null;
        if (!backingField.IsDefined(typeof(CompilerGeneratedAttribute), inherit:true))
            return null;
        return backingField;
    }
    

    【讨论】:

      【解决方案4】:

      不,没有。如果您想访问支持字段,请不要使用自动属性并自行滚动。

      来自Auto-Implemented Properties 的文档:

      当您如下例所示声明属性时,编译器会创建一个私有的匿名支持字段,该字段只能通过属性的 get 和 set 访问器访问。

      【讨论】:

      • 有官方参考吗?
      【解决方案5】:

      至少在 Visual Studio 2010 中,如果您明确声明需要非公共实例字段,则可以使用反射获取类中的私有字段列表:

      FieldInfo[] myInfo = ClassWithPostalCode.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
      

      然后您可以遍历 FieldInfo 数组。在这种情况下,您会看到支持字段的名称可能是

      k__BackingField

      我注意到所有自动属性似乎都遵循尖括号中的属性名称后跟“k__BackingField”的模式,但请记住,这不是官方的,并且可以在未来的 .Net 版本中更改。就此而言,我不完全确定它在过去的版本中没有什么不同。

      一旦知道了字段名称,就可以通过这种方式获取其值:

      object oValue = obj.GetType().InvokeMember(fi.Name
          , BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance
          , null, obj, null);
      

      【讨论】:

        【解决方案6】:

        Auto-implemented properties 是具有支持字段的手动实现属性的“懒惰”版本。由于它们不允许任何额外的逻辑,你唯一能用它们做的就是读取或写入“隐藏”的私有字段。如果您希望您的私有方法具有此特权,您仍然可以使用private 修饰符来限制访问 访问器之一(通常,set 在这种情况下是私有的) .

        如果您想访问其他类的私有字段(例如 BCL 中的类),您可以使用 Reflection 来执行此操作(如 these examples 中所述),但这将是一个讨厌的hack,没有人会保证框架源代码中的一个字母更改不会在将来破坏您的代码。

        但是由于您已经选择了自动实现,我认为没有可能的理由想要访问支持字段。直接访问字段或通过自动实现的属性访问器访问字段没有区别,也没有好处。例如,您可以争辩说您可以使用 Interlocked 以原子方式修改字段(您不能对属性执行此操作),但是当其他人都可以通过没有原子性的属性。

        更不用说编译器很可能会内联每个调用,所以还有no performance difference

        【讨论】:

        • 这是在VB中很常见的事情,当您想在自动实现的属性所属的类的构造函数中设置支持字段值时。我同意没有正当理由要在构造函数之外设置它。我很震惊(以及因此导致我来到这里的搜索)发现你不能在 C# 中做到这一点。但我现在真的很困惑,因为你说“你唯一能用它们做的就是读取或写入‘隐藏’的私有字段”。
        • @Yann:我不使用 VB,所以我不确定我是否理解你,但 C# 中有一个 readonly 修饰符,它允许你只在构造函数中设置私有字段。 if 只能应用于字段,不能应用于属性,因此如果您使用自动实现的属性,则无法使用它。
        【解决方案7】:

        这直接来自MSDN

        在 C# 3.0 及更高版本中,自动实现的属性使 当不需要额外的逻辑时,属性声明更简洁 在属性访问器中。它们还使客户端代码能够创建 对象。当您声明一个属性时,如下所示 例如,编译器创建一个私有的匿名支持字段, 只能通过属性的 get 和 set 访问器访问。

        所以不,你不能。

        【讨论】:

        • 结论So no you can't 不正确。它没有说任何关于不可访问的内容。例如,私有字段仍然可以使用反射访问。
        • “所以不,你不能。”根本不是真的。
        【解决方案8】:

        请参阅this document 的以下摘录:

        自动实现(auto-implemented)的属性使这种模式自动化。更具体地说,允许非抽象属性声明具有分号访问器主体。两个访问器都必须存在并且都必须有分号主体,但它们可以有不同的可访问性修饰符。当这样指定一个属性时,将自动为该属性生成一个支持字段,并且将实现访问器以读取和写入该支持字段。支持字段的名称是编译器生成的,用户无法访问。

        因此,无法访问字段。使用您的第一种方法。 自动实现的属性专门用于您不需要访问支持字段的情况。

        【讨论】:

        • 你的回答非常准确简洁,比别人帮了我很多!
        猜你喜欢
        • 2019-04-03
        • 1970-01-01
        • 1970-01-01
        • 2015-03-28
        • 1970-01-01
        • 2010-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多