是否可以访问自动实现属性后面的后备字段
本文关键字:字段 属性 实现 访问 是否 | 更新日期: 2023-09-27 18:20:10
我知道我可以使用属性的详细语法:
private string _postalCode;
public string PostalCode
{
get { return _postalCode; }
set { _postalCode = value; }
}
或者我可以使用自动实现的属性。
public string PostalCode { get; set; }
我可以以某种方式访问自动实现属性后面的后台字段吗?(在本例中为_postalCode)。
编辑:我的问题不是设计,而是理论能力。
我不知道你的情况,但我在其他公司的项目中写过代码,现在我想知道我是如何做到的!因此,在网上搜索答案通常会更快,这让我来到了这里。
然而,我的理由不同。我在进行单元测试,不在乎纯粹主义者怎么说,但作为单元测试设置的一部分,我试图调用给定对象的特定状态。但这种状态应该受到内部控制。我不希望其他开发人员意外地扰乱状态,这可能会对系统产生深远影响。所以它一定是私人设置的!然而,如何在不调用(希望)永远不会发生的行为的情况下对类似的事情进行单元测试?在这种情况下,我相信在单元测试中使用反射是有用的。
另一种选择是暴露我们不想暴露的东西,这样我们就可以对它们进行单元测试!是的,我在现实生活中看到过这种情况,一想到它仍然让我摇头。
所以,我希望下面的代码可能有用。
这里有两种方法,实际上只是为了分离关注点,还有助于提高可读性。对于大多数开发人员来说,反射是一件令人头疼的事情,根据我的经验,他们要么回避它,要么像躲避瘟疫一样躲避它!
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);
}
我不知道你使用什么代码约定,但就我个人而言,我喜欢helper方法是私有的,并且以小写字母开头。我在阅读时发现这一点不够明显,所以我也喜欢前面的下划线。
讨论了后台字段及其自动命名。为了进行单元测试,您很快就会知道它是否发生了变化!这对你的真实代码也不会是灾难性的,只是测试。因此,我们可以对名称的命名做出简单的假设——正如我在上面所做的那样。你可能不同意,这没关系。
更困难的辅助对象_getBackingField
返回这些反射类型之一FieldInfo
。我在这里也做了一个假设,你想要的支持字段来自一个实例对象,而不是静态的。如果你愿意的话,你可以把它分解成一些论点来传递,但对于那些想要功能但不想要理解的普通开发人员来说,情况肯定会更糟。
FieldInfo
的便利之处在于,它们可以在与FieldInfo
匹配的对象上设置字段。这可以用一个例子来更好地解释:
var field = _getBackingField(myObjectToChange, "State");
field.SetValue(myObjectToChange, ObjectState.Active);
在这种情况下,字段是一种称为ObjectState
的枚举类型。为了保护无辜者,已经改名了!因此,在第二行中,您可以看到,通过访问之前返回的FieldInfo
,我可以调用SetValue
方法,您可能认为它应该已经与您的对象相关,但没有!这就是反射的本质——FieldInfo
将字段与它的来源分开,因此您必须告诉它使用哪个实例(myObjectToChange
),从而告诉它您希望它具有的值,在本例中为ObjectState.Active
。
因此,长话短说,面向对象编程将防止我们做诸如访问私有字段之类的恶劣事情,更糟糕的是,在代码开发人员不打算的时候更改它们。这很好!这也是C#如此有价值,并受到开发人员喜爱的原因之一。
然而,微软给了我们反思,通过它,我们挥舞着强大的武器。它可能很丑陋,也很慢,但同时,它揭示了MSIL(MicroSoft Intermediate Language,简称IL)内部工作的最深处,并使我们能够几乎打破书中的每一条规则,这就是一个很好的例子。
更新:https://github.com/jbevain/mono.reflection附带了一个后台字段解析程序方法,该方法可用于C#、VB.NET和F#生成的自动属性。NuGet包位于https://www.nuget.org/packages/Mono.Reflection/
原创:我最终得到了这个相当灵活的方法,只适用于C#自动属性。正如其他答案所表明的那样,这是不可移植的,并且如果编译器实现使用<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;
}
至少在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);
请参阅本文档的以下摘录:
自动实现(自动实现)的属性使此模式自动化。更具体地说,允许非抽象属性声明具有分号访问器体。两个访问器都必须存在,并且都必须具有分号主体,但它们可以具有不同的可访问性修饰符。当这样指定一个属性时,将自动为该属性生成一个后备字段,并且将实现访问器来读取和写入该后备字段。支持字段的名称是编译器生成的,用户无法访问。
因此,无法访问字段。利用你的第一种方法。自动实现的属性专门用于不需要访问后台字段的情况。
自动实现的属性是带有后台字段的手动实现属性的"懒惰"版本。由于它们不允许任何额外的逻辑,所以你唯一能做的就是读取或写入"隐藏"的私有字段。如果您希望您的私有方法具有此权限,您仍然可以使用private
修饰符来将访问限制为其中一个访问者(在这种情况下,set
通常是私有的)。
如果你想访问其他类的私有字段(比如BCL中的类),你可以使用Reflection来访问(如这些例子中所解释的),但这将是一个讨厌的黑客,没有人能保证框架源代码中的一个字母的更改不会在未来破坏你的代码。
但是,由于您已经选择了自动实现,我认为没有任何可能的理由想要访问backing字段。直接访问字段或通过自动实现的属性访问器访问字段没有任何区别,也没有任何好处。例如,您可以争辩说,您可以使用Interlocked
来原子性地修改字段(这是您不能对属性执行的),但当其他人都可以通过没有原子性的属性访问字段时,它不会强制执行任何"线程安全"。
更不用说编译器很可能会内联每个调用,所以性能也并没有差异。
这直接来自MSDN:
在C#3.0及更高版本中,自动实现的属性使当不需要额外的逻辑时,属性声明更简洁在属性访问器中。它们还允许客户端代码创建对象。当您声明如下所示的属性时例如,编译器创建一个私有的匿名支持字段只能通过属性的get和set访问器进行访问
所以不,你不能。
没有。如果你想访问backing字段,那么不要使用auto属性并滚动自己的属性。
来自自动实现属性的文档:
当您如以下示例所示声明一个属性时,编译器会创建一个私有的匿名支持字段,该字段只能通过该属性的get和set访问器访问。
我获取auto属性后台字段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个单行方法的代码看起来可能有点混乱,因为浏览器使用了非固定宽度的字体。
该代码进行了非常严格和准确的检查,以查找(auto)属性的支持字段。如果不应用严格检查,它还可以找到手写简单纯属性的支持字段,该字段与auto属性的实现相同。
这两种严格的检查方法适用于M$dotnetfx运行时。这些规则在未来很可能不会改变,因为到目前为止,在可预测的未来,它运行良好。
密钥代码使用编译器生成的MSIL字节在最后4个方法中查找auto属性的backing字段。它们适用于M$的dotnetfx4x和dotnet5,也许还适用于M$dotnetfx的所有运行时。
如果您将其与mono或其他框架一起使用,您可以使用dnSpy或其他类似工具查看编译器发出的auto属性的属性、支持字段的名称和setter/getter的IL字节,然后修改6个单行方法以适应它们。当然,你可以添加一些其他严格的检查,以确保代码在你的程序运行的fx上正确工作。