是否可以访问自动实现属性后面的后台字段?

我知道我可以对属性使用冗长的语法:

private string _postalCode;


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

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

public string PostalCode { get; set; }

我可以以某种方式访问自动实现属性后面的后备字段吗? (在本例中是 _ 邮政编码)。


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

27804 次浏览

不,没有。如果您想访问后备字段,那么不要使用 auto 属性,而是滚动您自己的属性。

来自 自动实现的属性的文档:

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

请参阅 这份文件的以下节选:

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

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

这句话直接来自 MSDN:

在 C # 3.0及更高版本中,自动实现的属性 当不需要额外的逻辑时,property 声明更简洁 它们还允许客户端代码创建 声明属性时,如下所示 例如,< strong > 编译器创建一个私有的匿名后备字段,该字段 只能通过属性的 get 和 set 访问器访问

所以你不能。

自动实现的属性 是带有后台字段的手动实现属性的“惰性”版本。因为它们不允许任何其他逻辑,所以您只能使用它们来读写“隐藏”私有字段。如果您希望您的私有方法具有这种特权,那么您仍然可以将 private修饰符用于 限制访问,以用于其中一个访问器(通常,在这种情况下,set将是私有的)。

如果你想访问一些其他类的私有字段(比如来自 BCL 的类) ,你可以使用 反射来做到这一点(如 这些例子所解释的) ,但它将是一个 恶意黑客,没有人会保证在框架源代码中的一个字母的变化不会破坏你的代码。

但是因为您已经选择了自动实现,所以我认为没有可能的理由需要访问后备字段。直接访问字段或通过自动实现的属性访问器访问字段没有区别,也没有好处。例如,您可以认为您可以使用 Interlocked原子地修改字段(您不能对属性这样做) ,但是当其他所有人都可以通过该属性无原子性地访问该字段时,它不会强制执行任何“线程安全”。

更不用说编译器很可能每次调用都内联,所以也有 没有性能差异

至少在 VisualStudio2010中,如果显式声明希望使用非公共实例字段,则可以使用反射获取类中的私有字段列表:

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

然后可以循环通过 FieldInfo 数组。在这种情况下,您将看到后台字段的名称可能是

< 邮政编码 > k _ _ BackingField

我已经注意到,所有自动属性似乎都遵循“ k _ _ BackingField”之后的尖括号中的属性名模式,但是请记住,这不是官方的,并且在以后的版本中可能会更改。网。就此而言,我不能完全确定它在过去的版本中是否有所不同。

一旦你知道了字段名,你可以这样得到它的值:

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

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

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

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

因此,我希望下面的代码可能有用。

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

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 # 如此有价值并受到开发人员喜爱的原因之一。

然而,微软给了我们反射,并通过它,我们挥舞着一个强大的武器。它可能很丑陋,速度也很慢,但同时,它暴露了微软(MicroSoft)中间语言(简称 IL)内部工作的最深层次,使我们能够几乎打破书中的每一条规则,这就是一个很好的例子。

更新: https://github.com/jbevain/mono.reflection提供了一个后台字段解析器方法,该方法可以处理由 C # 、 VB.NET 和 F # 生成的自动属性。NuGet 软件包在 https://www.nuget.org/packages/Mono.Reflection/

最初: 我最终使用了这个相当灵活的方法,只针对 C # auto-properties。正如其他答案所表明的那样,这是不可移植的,如果编译器实现使用 <PropertyName>k__BackingField以外的备份字段命名方案,那么这种方法就不会起作用。据我所知,目前所有 C # 编译器的实现都使用这种命名方案。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;
}

我获取汽车资产后台字段的 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)属性的后台字段进行了非常严格和准确的检查。它还可以找到一个手写的简单普通属性的支持字段,如果你不应用严格的检查,这个属性与自动属性的实现是相同的。

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

密钥代码使用编译器生成的 MSIL 字节在最后4个方法中查找 auto 属性的备份字段。他们为 M $的 dotnetfx4x 和 dotnet5工作,也许还有 M $的所有 dotnetfx 运行时。

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