[翻译]修改 .NET 对象使其在 IronPython 中表现出动态性(属性注入)

原文:http://blogs.msdn.com/srivatsn/comments/8383517.aspx

修改 .NET 对象使其在 IronPython 中表现出动态性


假设你要和一个 .NET 的库进行互操作,但同时你又想让它表现的像动态语言中的对象那样,你想动态的给对象添加/删除方法或属性。在 python 中你可以这样写:

class x(object):
    pass

y = x()
y.z = 42
dir(y)

这样 dir(y) 就会包含 z. 如果 x 是 .NET 类而不是一个 python 类呢,能做到这一点吗?让我们尝试一个简单的 .NET 类:

public class TestExt
{
}

在 IronPython 中你可以这样:

import clr
clr.AddReference("TestExtensions.dll")
from TestExtensions import TestExt
y = TestExt()
y.z = 42


但这个代码会抛出 AttributeError,提示 'TestExt' 不存在属性 'z'。现在该怎么办呢?这正是 DLR 的扩展机制所要做的。有5个方法可以让 .NET 类来实现,这些方法可以对 binder 显示特殊含义,它们是:

  • GetCustomMember – 在常规的 .NET 查找前执行
  • GetBoundMember – 在常规的 .NET 查找后执行
  • SetMember – 在常规的 .NET 成员赋值前执行
  • SetMemberAfter – 在常规的 .NET 成员赋值后执行
  • DeleteMember – 在常规的 .NET 操作符访问之前执行(没有对应的 .NET 版本 —— 因此是唯一的一个)

.NET 类可以实现这些函数,并用 SpecialName 特性 来标注它们。现在绑定规则可以在常规 .NET 绑定的前后,调用 Getter/Setter. GetCustomMember/SetMember 首先被调用,并且,如果它返回一个值,则会被当作成员查找的返回值。这就可以覆盖任意可能存在的 .NET 成员。但如果你从该函数中返回 OperationFailed.Value,就会按常规方法继续进行查找。GetBoundMember/SetMemberAfter 在常规调用失败的时候会被调用到 —— ‘失败’表示不存在要绑定到的名字的成员。记住这些,我们来修改一下 .NET 类,向其中添加一些东西:

Dictionary<string, object> dict = new Dictionary<string, object>();
[SpecialName]
public object GetBoundMember(string name)
{
    if (dict.ContainsKey(name))
        return dict[name];
    else
        return OperationFailed.Value;
}

[SpecialName]
public void SetMemberAfter(string methodName, object o)
{
    dict.Add(methodName, o);
}

现在如果我尝试 y.z = 42, 代码会成功运行。同样我可以把 y.z 设定为一个函数,这样就可以调用 y.z() 了。换一种方法,我也可以重写 GetCustomMember 和 SetMember 方法来实现,例子一样有效,因为如果在 dict 中查找不到成员就会返回 OperationFailed.Value. 但这会带来一个负担,就是会影响到所有 .NET 成员的查找过程。

SetMember 函数可以返回 bool 而不是 void. 如果返回 bool 值,这个返回值会控制是否继续进行绑定查找。

那么,这个特性的作用是什么?我为什么要用它呢?假设我们要写一个类似下面 xml 所表示的对象模型:

<foo>
    
<bar>baz</bar>
</foo>


我想通过 foo.bar 访问这个 xml,并且取得的值应该是 baz. 要实现这个功能,只要给 .NET 的 XmlElement 类添加 GetBoundMember 方法,该方法去进行查找,并返回一个 XmlElement. 或者,还可以给 XmlElement 加一个扩展方法。但是扩展方法并不出现在反射的结果中,所以在 IronPython 中目前还没有这个支持。好在有一个避开的办法:可以用 ExtensionType 特性去修饰一个 assembly,该特性指出你要去扩展哪个类型,用哪个类来扩展。然后,你需要注册一次这个 assembly,以使这些方法被注入到合适的地方去。以后也许会修改为其他更好的实现方法,但目前而言,这个办法是可用的。下面就是你需要实现的代码:


[assembly: ExtensionType(typeof(System.Xml.XmlElement), typeof(TestExtensions.ExtClass.XmlElementExtension))]
namespace TestExtensions
{    
    public class ExtClass
    {
        static ExtClass()
        {
            Microsoft.Scripting.Runtime.RuntimeHelpers.RegisterAssembly(typeof(ExtClass).Assembly);
        }

        public static XmlElement Load(string fileName)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(fileName);
            return doc.DocumentElement;
        }
        public static class XmlElementExtension
        {
            [SpecialName]
            public static object GetCustomMember(object myObj, string name)
            {
                XmlElement xml = myObj as XmlElement;

                if (xml != null)
                {
                    for (XmlNode n = xml.FirstChild; n != null; n = n.NextSibling)
                    {
                        if (n is XmlElement && string.CompareOrdinal(n.Name, name) == 0)
                        {
                            if (n.HasChildNodes && n.FirstChild == n.LastChild && n.FirstChild is XmlText)
                            {
                                return n.InnerText;
                            }
                            else
                            {
                                return n;
                            }

                        }
                    }
                }
                return OperationFailed.Value;
            }
        }
    }
}

现在,在 IronPython 中可以这样写:

import clr
clr.AddReference("TestExtensions.dll")
from TestExtensions import ExtClass
foo = ExtClass.Load("test.xml")
print foo.bar
结果会输出 "baz".

Published Saturday, April 12, 2008 3:16 AM by srivatsn

posted on 2008-04-14 03:24  NeilChen  阅读(1281)  评论(0编辑  收藏  举报

导航