是否可以访问自动实现属性背后的支持字段?

时间:2012-01-11 09:40:16

标签: c#

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

private string _postalCode;

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

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

public string PostalCode { get; set; }

我可以以某种方式访问​​自动实现属性后面的支持字段吗? (在此示例中,_ postalCode )。


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

8 个答案:

答案 0 :(得分:43)

我不了解你,但我在其他公司的项目中编写代码,现在我想知道我是怎么做的!因此,通常可以更快地进行网络搜索以获得答案,并将它带到了这里。

但是,我的理由不同。我是单元测试,并不关心纯粹主义者必须说什么,但作为单元测试设置的一部分,我试图为给定对象调用某个状态。但是这个状态应该在内部控制。我不希望其他开发人员意外地搞乱状态,这可能对系统产生深远的影响。所以必须私下设置!然而,如何在不调用(希望)永远不会发生的行为的情况下对这样的事情进行单元测试?在这种情况下,我相信使用反射和单元测试很有用。

另一种方法是暴露我们不想暴露的东西,所以我们可以对它们进行单元测试!是的,我在现实生活中看到过这种情况,只是想着它仍然让我摇头。

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

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

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。我在这里也做了一个假设,你所追求的支持字段是来自一个实例的对象,而不是静态的。如果你愿意的话,你可以将其分解为可以传入的参数,但对于那些可能需要功能而不是理解的普通开发人员来说,水域肯定会更加混乱。

关于FieldInfo的方便之处在于,他们可以在与FieldInfo匹配的对象上设置字段。用一个例子可以更好地解释这一点:

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

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

因此,简而言之,面向对象编程将阻止我们执行诸如访问私有字段之类的令人讨厌的事情,更糟糕的是,当代码的开发人员不打算更改它们时。哪个好!这是C#非常有价值并且受到开发人员喜欢的原因之一。

然而,微软给了我们反思,通过它,我们运用了一个强大的武器。它可能很丑,而且非常慢,但与此同时,它暴露了MSIL(MicroSoft中间语言)-IL的内部工作的最深处 - 并且使我们能够完全打破书中的每个规则,这是一个很好的例子。

答案 1 :(得分:6)

这直接来自MSDN

  

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

所以不,你不能。

答案 2 :(得分:6)

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

ORIGINAL:我最终只为C#auto-properties提供了这种相当灵活的方法。正如其他答案所表明的那样,如果编译器实现使用除<PropertyName>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;
}

答案 3 :(得分:5)

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

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

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

  

&LT;邮编&GT; k__BackingField

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

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

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

答案 4 :(得分:3)

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

来自Auto-Implemented Properties的文档:

  

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

答案 5 :(得分:3)

请参阅以下摘录自this document

  

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

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

答案 6 :(得分:1)

Auto-implemented properties是手动实现的属性的“懒惰”版本,带有支持字段。由于它们不允许任何其他逻辑,因此您可以使用它们执行的唯一操作是读取或写入“隐藏”私有字段。您仍然可以使用private修饰符限制访问到其中一个访问者(在这种情况下,set通常是私有访问者),如果您需要私有方法拥有这项特权。

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

但是既然你已经选择了自动实现,我认为没有理由想要访问支持字段。直接访问该字段或通过自动实现的属性访问器访问该字段没有区别,也没有任何好处。例如,您可以争辩说可以使用Interlocked以原子方式修改字段(您无法对属性执行此操作),但是当其他人都可以访问该字段时,它不会强制执行任何“线程安全”通过没有原子性的财产。

更不用说编译器很可能内联每个调用,因此也有no performance difference

答案 7 :(得分:0)

我获取汽车属性支持字段的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 或其他类似工具查看编译器发出的 auto 属性的属性、支持字段的名称和 setter/getter 的 IL 字节,然后修改 6 个单行方法以适应它们。当然,您还可以添加一些其他严格检查,以确保代码在运行程序的 fx 上正常工作。