代码改变世界

[集成IronPython] 使CLR对象对动态语言更友好(三)—— 使用ExtensionTypeAttribute

2008-10-06 10:08 by Colin Han, ... 阅读, ... 评论, 收藏, 编辑

动态类型语言(以下简称:"动态语言"),在10年前就已流行起来。JavaScript更是成为了WEB前台开发的事实标准。但它们进入普通开发人员的视野也就在近几年。随着Web2.0和敏捷开发方法论的兴起,动态语言的灵活高效的特性成为了它被更多项目选择和使用的理由。一些大型网站已开始使 用动态语言来实现,其中,国内比较优秀的作品有“豆瓣”。微软更是不甘落后,建立了DLR(动态语言运行时)来吸引动态语言爱好者在其上实现动态语言。IronPython就是其主要成员之一。

在本系列文章中,我们将逐步实现一个自定义控件,实现类似IDE的Immediate窗口的功能。用户可以在其中输入和运行IronPython代码。 【返回目录


前面的两节中, 我们在一个普通对象和一个Collection上实现了几个SpecialName方法。从而使得我们的业务对象能够支持一些动态语言的特性。比如:支持动态添加删除属性和支持切片。但是:

  • 有些时候,我们不希望在非动态语言环境中看到这些对象。
  • 有些时候,我们不希望我们的业务对象依赖于Microsoft.Script.dll这个奇怪的Assembly
  • 有些时候,我们可能根据我们的业务需要,让一个系统对象(比如:System.Collections.Generic.Dictionary<TKey, TValue>)能够能够支持一些特别的操作。
  • 有些时候,我们的用户就是很变态 (也许你还有一些变态的要求,不防在留言中提出。我们一起讨论,看有没有合适的解)

我们先来看一个微软的例子

Asp.NET Futures是微软新近发布的一个组件包,其中包含了ASP.NET对DLR的支持部分。基于这个组件包,你可以使用IronPython或Managed JScript开发ASP.NET应用。

Python语言秉承的信条是“简洁的优于复杂的// TODO:”,因此,为了简化开发人员的工作,ASP.NET Futures中也包含了一些DLR友好的对象。比如下面的两段代码。

// C# Code
string myValue = Request.QueryString["MyValue"];

# Python Code
myVar = Request.MyValue

很显然,Python代码比起C#代码简单了不少。(如果你实际使用一下IronPython,你会发现Python语言确实使得代码更加精炼,开发更加快速,只是我现在还不确定,调试的难度有多高)。

其实,上面的功能实现起来很简单。只需要在HttpRequest类型上添加两个方法就可以实现。如下:

 1namespace System.Web.UI
 2{
 3  public class HttpRequest
 4  {
 5    // 其它实现逻辑
 6    [SpecialName]
 7    public IList<SymbolId> GetMemberNames()
 8    {
 9      return this.
Params.AllKeys;
10    }

11    [SpecialName]
12    public object GetBoundMember(string name)
13    {
14      return this.
Params[name];
15    }

16  }

17}

注:上面的代码仅为了表达意思,其中有明显的语法错误,实际的代码要比上面的稍微复杂一点。

上面的代码中,GetMemberNames方法告诉DLR系统通过这个方法获得该对象上的所有属性(你可以认为方法是一个Delegate类型属性)而不是通过反射。下面的GetBoundMember方法实现具体的Get属性或方法实现。通过SpecialNameAttribute以标识这些方法可以被DLR使用。

但是,上面的实现使得HttpRequest对象多出了两个公共接口。对于没有深入理解过DLR的实现的开发人员来说,这两个接口非常的费解。怎样让C#开发人员和IronPython语言开发人员都很舒服呢?

我们来看看微软的实现

首先,微软在ASP.NET Future中关于动态语言支持的代码主要在Microsoft.Web.Scripting.dll中。因此我们从这个Assembly开始着手。使用Reflector打开这个文件,我们可以看到Assembly中添加了这个Attribute。

[assembly: ExtensionType(typeof(HttpRequest), typeof(RequestParamsMembersInjector))]

注:注意,这个Attribute是添加在Assembly上,而不是扩展的任何一方的类型上。因为加载一个Assembly时遍历其中的类型是一件低性能并危险的事情。

呵呵,从标题中,大家应该已经知道“ExtensionTypeAttribute”是本节内容的重点。就是他实现了为已有CLR对象添加DLR支持功能的。

我们顺藤摸瓜,找到RequestParamsMembersInjector这个类型的实现,如下:

1 public static class RequestParamsMembersInjector
2 {
3     // Methods
4     [OperatorMethod] // 注1
5     public static object GetBoundMember(HttpRequest request, string name)
6     {
7         return request.Params[name];
8     }
9 }

注1:这里也许是以前版本的定义。在Bata3中没有找到这个Attribute,取而代之的是SpecialNameAttribute。

通过这个类,实现了为HttpRequest类型添加了一个GetBoundMember方法。看起来很像.NET 3.5提供的扩展方法。只是this关键字被SpecialNameAttribute代替了。

最后,给出一个我们自己的实现

这一节,我们为DLConsole添加了一个LoadAssembly方法,用来加载Extension。我们在测试工程中添加了一个SimplaXmlExtension类型实现访问XmlElement的子节点的功能。你可以从这里下载可运行的源代码。扩展的代码如下:

扩展的实现代码

你可以在程序中输入并运行如下的Python代码来测试这个扩展的工作。

print Foo.bar1
print Foo.bar2

总结

在前几篇文章的留言中,RednaxelaFX提到真正的让一个CLR对象支持动态语言,还是需要实现IDynamicObject接口。下去研究了一下,这个接口确实提供了更加强大的能力。如下:

namespace Microsoft.Scripting.Runtime {
    
public interface IDynamicObject {
        RuleBuilder
<T> GetRule<T>(DynamicAction action, CodeContext context, object[] args) where T : class;
    }

}

如此简单的接口,可见其功能强大吧。领会这个接口还需要对DLR有更深入的了解。下面的这个系列的文章可以帮助你更好的理解DLR的一些实现机理。但是,如果你如我一般对编译原理一窍不通。深入这一层次确实是一件危险的事情。

LINQ与DLR的Expression tree(1):简介LINQ与Expression tree
LINQ与DLR的Expression tree(2):简介DLR
LINQ与DLR的Expression tree(3):LINQ与DLR及另外两个库的AST对比
LINQ与DLR的Expression tree(4):创建静态类型的LINQ表达式树节点
LINQ与DLR的Expression tree(5):用lambda表达式表示常见控制结构

相信我们本节提到的方法,就是微软为了减少我们这些菜鸟的学习成本而提出的一个快速解决方案。基本上,我们平时能够想到的大多数功能都可以通过这个扩展注入到原有对象上了。

但是,这里还有一个小问题需要注意:

如果扩展对象和被扩展对象不再同一个Assembly中,你必须先将扩展所在的Assembly加载进来(使用ScripDomainManager.LoadAssembly方法(c#)或clr.AddReference方法(Py)),然后操作被扩展对象才能够使用扩展所添加的功能。因为,DLR使用的是JIT的技术,也就是说他在第一次操作一个类型的对象时,会将对这个对象的操作Cache下来。下一次他就会直接使用Cache下来的操作。而不是每一次都解释执行代码。

至此,我们基本可以完成大多数我们期望的动态语言友好性扩展(关于具体都可以编写哪些SpecialName方法,我会专门编写一章来讨论)。DLConsole也基本完成了。在下一节,我们将考虑让DLConsole支持其他的动态语言。每周一节,敬请关注feedsky

大家可以从这里下载可运行的源代码。

免责声明:本系列文章,完全是我个人研读 IronPython源代 码后找到的实现方案,并未详细的研究过IronPython的相关官方文档。因此,并不保证符合微软DLR和IronPython的设计思路,亦不能保证 在DLR和IronPython 2.0正式发布后能够继续使用。