有状态反射






3.50/5 (12投票s)
2005年1月9日
6分钟阅读

53253

301
描述了一系列以多态、实例特定方式处理反射成员的类。
引言
本文介绍了一系列类,它们以一种有状态的方式对属性进行反射,保留了反射成员与执行反射的实例之间的链接。它使得反射的属性和实例可以作为一个单元来处理,从而更容易地在应用程序中传递。
简而言之,它是一系列类,它们存储 MemberInfo
(或等效项)以及被反射的实例。
这在数据绑定中尤其重要(正如您将在我后续备受期待但仍未完成的关于 ASP.NET 中的双向数据绑定的文章中看到的),但也有其他用途。
背景
.NET 中的反射效果很好,但您反射的是类,而不是对象。您获得一个代表类上的方法/字段/属性等的 MemberInfo
的句柄,但您仍然需要拥有一个单独的类实例引用才能使用该句柄(除非它是静态成员)。
如果您的反射和对反射成员的使用发生在相隔不远的几行代码内,那没问题。但是,如果您想在一个模块中定位合适的成员,然后将该成员的使用推迟到其他地方怎么办?这种责任分离是一种更面向对象的做法,但 .NET 中的反射迫使您同时传递实例和成员句柄来完成这项工作。这只是件麻烦事。
我正在解决的问题在于递归反射操作——通常涉及“遍历”如下的点路径
"myObj.subObj.Value"
来检索 Value
的值。您可能会设置一个循环来执行此操作,并且它可能看起来像这样
public object RecursiveReflectGetGet(object obj, string path){
string thisPart;
string nextPart;
Type type;
do{
type = obj.GetType();
// Take the first part of the path off the path string
string[] parts =path.Split(new char[]{'.'},2);
thisPart =parts[0];
nextPart =(parts.Length>1) ? parts[1] : "";
MemberInfo[] members =type.GetMember(thisPart);
if (members.Length!=1)
throw new
ApplicationException("Not handling this for this example");
switch(members[0].MemberType){
case MemberTypes.Property:{
PropertyInfo prop = (PropertyInfo)members[0];
// Indexed properties not handled either
obj = prop.GetValue(obj, new object[0]);
}break;
case MemberTypes.Field:{
FieldInfo field = (FieldInfo)members[0];
obj = field.GetValue(obj);
}break;
default:
throw new NotImplementedException();
}
// Advance the path string to the next dot-part (if present)
path = nextPart;
}while(path!="");
return obj;
}
循环的每个周期都会从 path
中移除第一个点部分,在 obj
上找到相关的 MemberInfo
,获取其值,并将其赋回给 obj
。路径的其余部分也会被重新赋值给 path
。循环结束时,我们会再次循环处理层级结构中的下一个级别,或者 path
已为空,此时我们就完成了。
现在想象一下,myObj.subObj.Value
是一个 string
。我们可以成功检索它,但由于它是一个值类型,我们对其所做的任何更改都不会应用到“原始”对象,该对象隐藏在对象层次结构中。如果我们想更新原始对象,我们将不得不再次遍历点路径,但在最后执行 SetValue
操作。为了简单起见,我们将重用我们现有的一些代码
public void RecursiveReflectSet(object obj, string path, object value){
int i = path.LastIndexOf(".");
if (i!=-1){
// Just re-use RecursiveReflectGet to get the penultimate
// part of the dot-path (if it's a complex expression)
string thisPart = path.Substring(0,i);
path = path.Substring(i+1);
obj = RecursiveReflectGet(obj, thisPart);
}
MemberInfo[] members = obj.GetType().GetMember(path);
if (members.Length!=1)
throw new ApplicationException("Not handling this for this example");
switch(members[0].MemberType){
case MemberTypes.Property:{
PropertyInfo prop = (PropertyInfo)members[0];
// Indexed properties not handled
prop.SetValue(obj, value, new object[0]);
}break;
case MemberTypes.Field:{
FieldInfo field = (FieldInfo)members[0];
field.SetValue(obj, value);
}break;
default:
throw new NotImplementedException();
}
}
}
看起来浪费了很多周期,对吧?我们一路向下找到 get
Value
,然后我们又要一路向下 set
它。更糟糕的是,如果 Value
包含任何元数据(属性),我们就丢失了它们。我们完全丢失了获取 Value
的上下文。这不仅仅是值类型的问题——如果 Value
是引用类型,但我们想更改引用而不是仅仅使用它,那么我们将不得不做同样的工作。
这还没有提到类型转换。Value
的类型是什么?如果我们先执行 RecursiveReflectGet
,然后再执行 RecursiveReflectSet
,我们就知道 Value
的内容,因此也知道它的 Type
,因为我们有了它的引用。但如果我们只想直接执行 RecursiveReflectSet
,我们就必须先执行一个 get
*仅仅为了获取类型*,然后执行一个 set
。而且这一切都假设 Value
不是 null
。唯一真正知道 Value
类型的地方是 RecursiveReflectionSet
的最后一部分,所以我们可以在那里进行类型转换。但这方向错了:我们应该*首先*在调用者中进行类型转换,因为客户端可能有特定的类型转换规则要遵循,这取决于它最初如何处理 Value
,而在这里是不可能的(您可以通过委托来解决这个问题,但这会很混乱)。
元数据也是如此。假设我们已经用各种属性标记了 Value
,这些属性允许客户端发现其值的有效范围,例如(虚构示例)MaxLengthAttribute(40)
、DefaultValue("")
或 TypeConverter
。我们也丢失了所有这些。
那个 switch{}
语句也带有“重构以实现多态”的字样。
如果我们向客户端返回最终的 obj
和 MemberInfo
,所有这些问题都会消失。然后,客户端可以 get
值,对其进行操作,然后 set
值,而无需重新解析点路径,并且了解 Type
、任何自定义属性,这很有效。但这很麻烦:我们需要返回两样东西(我不喜欢输出参数),现在客户端必须为每个 MemberInfo
子类型 switch{}
,这使得代码更难处理。
输入 IPropertyInstance
。
解决方案:IPropertyInstance
不出所料,IPropertyInstance
将反射的“属性”与属性所属的实例结合起来。我说“属性”,但该接口旨在涵盖任何类属性行为,并且有类涵盖 PropertyInfo
和 FieldInfo
以及 PropertyDescriptor
和索引属性。它只是一个通用的接口,让您可以 get
和 set
某个东西,并且因为它被很好地打包好了,您可以随意传递它们,而无需向客户端公开您一直在反射什么的内部细节。
public interface IMemberInstance
{
/// <summary>
/// Retrieve the object instance that this IMemberInstance manipulates
/// </summary>
object Instance{ get; }
/// <summary>
/// Retrieves the name of the instance member
/// </summary>
string Name { get; }
/// <summary>
/// Gets/sets the value of <c>Name</c> on <c>Instance</c>
/// </summary>
object Value { get;set; }
/// <summary>
/// Retrieves the type of the member
/// </summary>
Type Type { get; }
/// <summary>
/// Retrieves an appropriate TypeConverter
/// for the property, or null if none retrieved
/// </summary>
TypeConverter Converter { get; }
}
代码中包含各种 IPropertyInstance
的实现,包括 PropertyInfoInstance
(用于操作属性)、FieldInfoInstance
(用于字段)和 IndexedPropertyInfoInstance
(用于索引属性)。其中大多数都包含一个构造函数,如果您已经拥有相关的 PropertyInfo
/ PropertyDescriptor
等的句柄,就可以使用该构造函数。还有一个静态 GetInstance(object instance, string memberName)
方法,它将从一个 object
和一个(string
)成员名称创建一个 IMemberInstance
。这为您节省了自己进行反射的麻烦,此外,抽象类 MemberInfoInstance
上的静态方法将返回 FieldInfoInstance
、PropertyInfoInstance
或 IndexedPropertyInfoInstance
,自动确定成员的类型。
(还有一些,包括 PropertyDescriptorInstance
,我将在 2WayDataBinding 文章中介绍)。
使用代码
通常,只需使用其中一个 static
方法来获取您的 IMemberInstance
,然后只 get
/set
Value
即可。
IMemberInstance member =MemberInfoInstance.GetInstance(obj, "AccountBalance");
decimal value =(decimal)member.Value;
// Do some calculations that update value
member.Value =value;
当然,强制转换可能存在问题,所以也许
IMemberInstance info =MemberInfoInstance.GetInstance(obj, "AccountBalance");
if (info.HasConverter && info.Converter.CanConvertFrom(typeof(decimal))){
decimal typedValue =(decimal)info.Converter.ConvertFrom(dataMember.Value);
// do calculations etc...
info.Value =info.Converter.ConvertTo(typedValue, info.Type);
}
现在这些例子相当简单。我已将其用作我 TwoWayDataBinder
类的核心,但您如何使用它取决于您。显然,它只有在成员的使用由运行时评估的表达式确定时才真正有用,例如在数据绑定或 RAD 工具中。
顺便说一句,这种类型转换是 Visual Studio 设计器用来将您在属性网格中输入的字符串转换为正在设计中的控件上的类型化值 *正是*的机制。通常,它设置为与字符串相互转换,因此您可以在正在设计对象的一个字段 AccountType
中输入“Mortgage
”,类型转换代码将——比如说——将其转换为 AccountType.Mortgage
enum
(甚至是“Mortgage
”类的实例)。然而,没有理由将其仅限于设计器——它只是一个用于指定将对象转换为其他类型对象规则的通用机制(与 IConvertable
不同,后者允许对象转换为内置值类型,如 Int32
和 DateTime
)。
如果您没有安装 NUnit,则在生成项目之前必须删除对 NUnit 的项目引用。如果您已安装 NUnit 并想运行单元测试,请在 DEBUG 生成中添加 TEST 编译指令。
竞赛
列出您可以想到使用此技术的潜在用途。奖品是沾沾自喜。
历史
- 05/09/01 - 文章初版。