实现反射的过程中遇到的问题及解决方法(关于自定义类对象数组的深层拷贝)
在最近做的工程里,有一个需求是实现一个静态方法public static object CopyOjbect(object sourceObject),这个方法利用C#.net的反射机制,可以比较“通用地”返回几种自定义类对象的深层拷贝。在实现这个静态方法的过程中,曾经遇到了一点小问题,困扰了一整天的时间。现在把解决过程写出来,希望这点经验能给同样遇到这个问题的C#程序员一点小小的帮助。如果您有其他的解决方案,请不吝赐教。
这个方法的结构非常简单:首先利用sourceObject.GetType()方法,得到sourceObject的类型objectType,然后调用静态方法Activator.CreateInstance(objectType)得到该类型的一个新的实例newObject,这个返回实例用一个object引用来提领。接下来,使用objectType.GetProperties()得到该类型所有的公共属性(get-set属性)。对于其中的每一个属性,如果其类型是C#语言内置类型,如int, string, double等,就将sourceObject对象中这个属性的值直接赋给新对象newObject的相同属性。由于不知道这个object的实际类型,因此无法将这个ojbect引用显式cast成为实际类型,从而也就不能直接调用这个公共属性的set方法,而需要通过InvokerMember()方法来调用。InvokerMember的功能很强大,主要参数有需要调用的成员方法名,该成员方法所属的对象引用,以及一个包括目标成员方法参数列表的object[]。如果该属性不是一个C#语言内置类型,而是一个自定义类对象的引用,则递归copyObject()方法自身,得到这个自定义对象的一个新实例,并将该实例的引用赋值给这个属性。如果该属性是一个数组,则通过for循环来执行上述过程。
在处理类型为自定义类数组的属性时,问题出现了:
来看下面这两个类,首先是自定义类型MyClass:
public class MyClass{
//members;
}
和一个自定义类MyContainer:
public class MyContainer{
private MyClass[] myClassArray;
public MyClass[] MyClassArray
{
get{
return myClassArray;
}
set{
myClassArray = value;
}
}
private MyClass myClassObject;
public MyClass MyClassObject
{
get{
return myClassObject;
}
set{
myClassObject = value;
}
}
//other members;
}
注意,自定义类MyContainer有两个公共属性,一个是MyClass类对象的引用,另一个是MyClass类对象引用的数组。
现在有一个MyContainer类型的对象sourceObject,希望通过调用CopyObject(sourceObject)来得到一个新的MyContainer对象newObject。起初,处理自定义类对象数组属性的代码是这么实现的:
for(PropertyInfo 属性p in objectType.GetProperties())
{
//处理其他类型的属性;
else if (属性p.type.EndsWith("[]")) //这个属性是一个自定义对象的数组
{
object[] oArray = new object[((ojbect[])(属性p.value)).Length]; //生成数组
for(int i=0; i<((ojbect[])(属性p.value)).Length; i++)
{
oArray[i] = CopyObject(((ojbect[])(属性p.value))[i]); //递归调用CopyObject方法,得到数组中自定义对象的实例,并将该实例的引用存放到新数组属性里面;
}
//接下来通过InvokeMember调用这个数组属性的set方法
objectType.InvokeMember(属性p.name, //调用的方法名。运行时,这个参数的值是MyClassArray,但此时未知
BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty,
null,
newObject, //运行该方法的对象
new object[]{oArray}); //方法参数列表
}
}
这段代码是想要调用MyContainer中公共属性MyClassArray的set方法,把新生成的数组引用赋值给这个公共属性,达到深层拷贝数组属性的目的。编译正常通过,但运行时,InvokeMember抛出了这样一个异常:System.MissingMethodException: Member not found。
没有这个成员?不对啊?MyClassArray属性明明就在那里,为什么会说没有这个成员呢?对于和他相同类型MyClass的属性MyClassObject,用InvokeMember调用MyClassObject的set方法的时候完全正常,为什么当属性变成MyClass类型的数组时,就没有这个成员了呢?
在网上查找这个异常信息代表的含义,没有找到相关的解释。难道真的是没有这个成员方法?那么MyClassArray的set方法,到底叫什么名字呢?用GetMethod()方法反射一下MyContainer类,得到的方法名字是set_MyClassArray,可能问题出在这里,原来是我调用的方法名不对。但对于MyClassObject属性,它的set方法叫做set_MyClassObject,为什么我用MyClassObject方法名调用的时候就没错呢?先不管这些了。把InvokeMemeber调用改成这样:
objectType.InvokeMember("set_" + 属性p.name, ......);
编译通过,运行时却又抛异常了:System.MissingMethodException: Attempt to access a missing member.
尝试调用一个不存在的方法。看来set_MyClassArray这个名字也不对。那到底是怎么回事呢?
这时候,我注意到,两次异常信息的message不一样。一个是Member not found,另一个是Attempt to access a missing member. 说明这两次异常是不一样的。那么它们分别代表什么意思呢?网上还是没有找到。我自己来试试看:
objectType.InvokeMember("1234" + 属性p.name, ......); //随便给他个名字
运行时的异常是Attempt to access a missing member. 看来这个异常真的是找不到方法,鄙视一下GetMethod的误导。现在问题又回来了,为什么MyClassArray找不到成员方法?
又想了好久,终于想到了一种可能性:MyClassArray方法是有的,但是没有以object[]类型为参数的重载方法!MyClassArray的set方法签名实际上是set_MyClassObject(MyClass[] value),但是没有set_MyClassObject(object[] value)这个签名,所以也就member not found了。
为了证明这个猜测,把程序改成:
else if (属性p.type.EndsWith("[]")) //这个属性是一个自定义对象的数组
{
MyClass[] oArray = new MyClass[((ojbect[])(属性p.value)).Length]; //先不管通用性了,把类型名写死看看对不对
for(int i=0; i<((ojbect[])(属性p.value)).Length; i++)
{
oArray[i] = (MyClass)CopyObject(((ojbect[])(属性p.value))[i]); //这里必须要用一个显式的cast,否则编译器不能通过
}
objectType.InvokeMember(属性p.name,
BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.SetProperty,
null,
newObject,
new object[]{oArray});
}
运行正常,看来我刚才的猜测是正确的。这里我犯了一个经验错误:对于set_MyClassObject(MyClass value)这个签名,因为所有的Class都继承自object,所以用CopyObject()返回的object引用来调用是没有问题的,调用的时候.net对这个object引用进行了一次强制cast,运行正常。想当然的,我就以为可以用一个object[]数组引用来提领一个MyClass[]对象。这个是不行的,它们不是继承关系。
现在的问题是如何解决通用性问题。编程的时候,我不能确定传进来的是一个什么对象数组,所以没办法用new来产生数组引用;同理,我也不能在CopyObject返回的时候使用未知类型的显式cast。该怎么办呢?
虽然MyClass[]和object[]不是继承关系,但MyClass[]是一个数组。数组是一个对象,既然是对象,那就和object有继承关系。能不能用Activator.CreateInstance()来创建一个数组,并用一个返回的object来引用这个数组呢?程序写成这样:
object oArray = Activator.CreateInstance(sourceObject.GetType().GetProperty(属性p.name).PropertyType);
编译时通过,运行时抛出异常:System.MissingMethodException: No parameterless constructor defined for this object.
这个对象没有不使用参数的构造器。我平时用new来产生数组引用的时候,比如stirng[] strs = new string[SIZE]; 拿来就用了,根本没想过new一个数组也是需要调用构造函数的。在MSDN上一查Activator.CreateInstance(),果然这个方法需要调用构造函数,而这个不需要参数列表的重载版本是调用默认的不需要参数的构造函数的。那么,一个数组对象的构造函数是什么呢?在new一个数组的时候,必须显式或隐式指定数组的大小,那么,数组对象是否有一个构造函数是以一个int为参数的呢?试试看:
object oArray = Activator.CreateInstance(sourceObject.GetType().GetProperty(属性p.name).PropertyType, new object[]{((object[])(属性p.val)).Length});
编译通过,运行正常。
这个是我在实现这个反射方法的时候遇到的一些困难,把他们写出来,希望能够给遇到同样问题的人些许帮助。