使用反射在基类构造函数中设置派生类的值

3
我有两个类,如下所示:

public abstract class MyBase
{
     protected MyBase(){
         Initialize();
     }

     protected IDictionary<string,string> _data;

     private void Initialize() {
         // Use Reflection to get all properties
         // of the derived class (e.g., call new MyDerived() then
         // I want to know the names "Hello" and "ID" here
         var data = GetDataFromBackend(propertyNamesFromDerived);
         _data = data;
     }
}

public class MyConcrete : MyBase
{
     public MyConcrete(){
         // Possibly use Reflection here
         Hello = _data["Hello"];
         ID = new Guid(data["ID"]);
     }

     public string Hello {get;set;}
     public Guid ID {get; set;}
}

正如您所见,我希望我的基类构造函数知道我正在实例化的派生类的属性。
现在,这似乎是一个巨大的代码气味,让我更详细地解释一下我的意图,也许有更好的方法。
我有一个后端系统,存储键/值对,本质上是一个 Dictionary<string,string>。我想以一种抽象的方式处理此后端系统,使人们可以创建类,其属性是关键字进入后端系统。当他们构建此对象时,它将自动从该系统加载数据并将所有变量初始化为该数据。
换句话说,我只是重新发明了序列化,除了我不控制后端系统,而是使其非常轻松地工作。我不希望调用者在构造对象后调用Initialize(),因为在100%的情况下,您必须在构造后初始化它。
我不想将初始化代码移动到派生类中,除了字符串转业务对象转换之外。
我必须使用工厂吗?还是在基础构造函数中查看派生类的属性名称被认为是安全的?(不关心它们的值和它们是否已初始化,只需要名称)。
还是提供字典和具体业务对象之间的外观有更好的方法?
编辑:这是 .net 3.5,因此没有 System.Dynamic,这将使其平凡无奇 :(
编辑2:在查看答案并仔细思考后,我想我的问题现在归结为:从基础构造函数调用 GetType().GetProperties() 以获取属性的名称及其是否使用某些特定属性是安全的吗?
6个回答

5

等一下,让我们停一下,好好地做这件事。 MyBase 不应该负责这个。

因此,您可以编写一个类来管理从后端获取数据,并在该类上编写一个方法,例如:

T Get<T>() where T : new()

你需要让 Get 负责从后端读取字典,并使用反射来填充一个 T 实例。因此,你可以这样说:

var concrete = foo.Get<MyConcrete>();

这并不困难,也是正确的方法。
顺便提一下,Get 的代码看起来大致是这样的。
T t = new T();
var properties = typeof(T).GetProperties();
foreach(var property in properties) {
    property.SetValue(t, dictionary[property.Name], null);
}
return t;

其中dictionary是您加载的键/值对。事实证明,有更优化的方法来做到这一点,但除非它成为瓶颈,否则不必担心。


谢谢。在查看了这个内容之后,我澄清了我的问题,我真正感兴趣的部分是 GetType().GetProperties() 是否安全,只是为了获取名称和属性 - 不再关心在基类中设置值。 - Michael Stum
在语言和运行时的保证范围内。目前正在尝试了解CLR在构造函数运行时对对象状态做出的保证。 - Michael Stum
@Michael Stum:我再次强调,您绝不能在基类的构造函数中或任何继承基类的链的任何位置进行此操作。 当您的冰箱有异味时,您会清洗它,而不是盲目地伸手拿出发霉的三明治。 - jason
是的,这正是我正在寻找替代方法的原因。目前,每个派生类都是一个独立的对象,调用服务并获取数据,这是很多重复。基类的想法是为我执行该服务调用。我可以将属性操作移动到派生类中,因此没有“从基类修改派生”的小问题。但是,基类需要知道派生属性的名称(+是否存在属性),其余部分可以仅在其内部完成。 - Michael Stum
我正在寻找的“保证”是CLR能够确保在基础构造函数中调用GetType().GetProperties()并获取所有属性,因此不会丢失任何属性,也不会因此而出现异常。就像@SLaks在下面所说的那样。因此,派生类只需要关心字典到属性的转换,这意味着我大大减少了当前代码中的管道/重复。 - Michael Stum
显示剩余3条评论

3
更好的方法是直接让类使用字典:
public string Hello {
    get { return (string)base.data["Hello"]; }
    set { base.data["Hello"] = value; }
}

您可以在getter中调用TryGetValue,这样如果键不存在,就可以返回默认值。(最好在基类中使用单独的方法来实现此操作)
您可以创建代码片段,以使属性更易于创建。
如果您不想使用此方法,可以调用GetType().GetProperties()获取类中属性的PropertyInfo对象,然后调用SetValue(this, value)。这将会很慢;您可以使用表达式树、CreateDelegate或IL生成等各种技巧来加速它。

谢谢。查看了这个之后,我澄清了我的问题,我真正感兴趣的部分是 GetType().GetProperties() 是否安全,只是为了获取名称和属性 - 不再关心在基类中设置值。 - Michael Stum
1
@Michael:是的,它是安全的。与C++不同,对象已经完全构造完成;唯一尚未发生的是派生类构造函数中的用户代码。您可以调用GetType()甚至将其转换为其派生类型。 - SLaks

1

那岂不意味着我必须将Initialize变为抽象方法,并将管道移动到Concrete类中?我想避免这种情况。 - Michael Stum
不,例如您可以添加一个抽象方法listProperties,在Initialize内部调用它以查询所需的数据。 - marc

0

我不确定这是否是你真正应该做的,但这就是你要求的(将其放在Initialize中,你将获得派生属性名称列表):

        var derivedProps = this.GetType().GetProperties();
        var propNames = new List<string>(derivedProps.Select(x => x.Name));

通过使用derivedProps中的PropertyInfo,您可以设置属性。


0
你考虑过使用 ExpandoObject 吗?它可以让你动态地添加属性并检查它们(例如在序列化时)。

非常好的观点。忘了说:我在使用 .net 3.5,所以没有动态特性 :( - Michael Stum

0

在基类构造函数中,你实际上不能安全地对这些属性进行任何操作,因为某个派生构造函数可能会重置它们。最好的方法是进行两阶段加载(例如显式调用Initialize)。


谢谢。在查看了这个内容之后,我澄清了我的问题,我真正感兴趣的部分是 GetType().GetProperties() 是否安全,只是为了获取名称和属性 - 不再关心在基类中设置值。 - Michael Stum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接