原文: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 所表示的对象模型:
< 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 中可以这样写:
结果会输出 "baz".import clr clr.AddReference("TestExtensions.dll") from TestExtensions import ExtClass foo = ExtClass.Load("test.xml") print foo.bar