65.9K
CodeProject 正在变化。 阅读更多。
Home

LinFu 框架介绍,第一部分 - LinFu.DynamicProxy:一个轻量级的代理生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (79投票s)

2007年10月15日

LGPL3

14分钟阅读

viewsIcon

391455

downloadIcon

3461

一个快速的动态代理库,支持 .NET 泛型

引言

LinFu 框架是我一直以来想在 .NET 中实现的一些功能的集合,但遗憾的是,现有的实现并不完全符合我的需求。所以,和所有好奇的程序员一样,我开始自己编写库,这就是结果。我可以喋喋不休地谈论它,但这张图片应该能让你大致了解 LinFu 的作用以及这一系列文章的走向。

Screenshot - LinFu_small.png

注意: LinFu 这个词(发音:LINN-FOO)实际上是一个首字母缩写,意思是“Language INdependent Features Underneath [.NET]”(语言无关的底层功能[.NET])。四年前,它从简单的动态代理和 Reflection.Emit 的研究开始,逐渐演变成一个不改变各自语法来扩展现有 .NET 语言的个人项目。整个框架是我多年个人研究的结晶,经过一番斟酌,**我决定在 LGPL 许可下开源这个项目**。这意味着只要遵守许可条款,每个人都可以免费使用该库,无需支付版税。如果您要在自己的商业项目中使用它,我非常乐意收到一些建设性的商业反馈(一些专业的推荐信也会有帮助)。

如果您认为应该添加任何功能(或您想自己添加任何功能),请将您的修改发送给我,以便我查看并将其合并到我目前的 codebase 中。总之,让我们回到文章…

语言无关?

LinFu 在最基本的形式上,只是一个编译成 IL 的托管 DLL 库。由于任何用 IL 编写的内容都可以被任何 .NET 语言使用,因此我在后续文章中将展示的 LinFu 的所有功能都可以被任何 .NET 语言使用。基于这个原理,LinFu 可以扩展 VB.NET 和 C# 等语言,而不会影响它们各自的语法。它支持以下功能:

动态代理

LinFu 有自己的代理生成器,甚至比 CastleProject 的 Dynamic Proxy 和 Castle DynamicProxy2 等更知名的实现还要早。这个版本的 LinFu DynamicProxy 自 VS2005 发布以来就支持泛型,并且根据我目前的基准测试(本文将讨论),它的性能比 Castle 的实现平均快 200% 以上。简而言之,LinFu 的 Dynamic Proxy 既精简又高效,速度极快。

鸭子类型和后期绑定

像 Ruby 和 Python 这样的动态语言(以及在一定程度上,VB.NET)拥有在运行时调用方法的能力,而无需在编译时确切知道将使用哪些方法。此外,它们还能进一步实现“推断”,即根据运行时传递给方法的参数来推断应该调用哪个方法(也称为“鸭子类型”)。如果 C# (2.0) 能原生支持这一点就好了,但不幸的是,这并不完全可能……除非您使用 LinFuLinFu 会对 C# 语法进行一些令人费解的操作来实现这一点,稍后在系列文章中,我将向您展示它是如何工作的。

Ruby 风格的 Mixins

动态语言能够无缝地动态修改其类,以便每个类实际上可以向每个类实现添加和删除方法(甚至添加其他类和接口实现)。信不信由你,我找到了一种在 C# 和 VB.NET 中实现相同类型的动态 Mixins 的方法,并且仍然能够遵循每种语言的强类型规则!(好吧,可能 VB.NET 不行,因为它有时可以是弱类型,但这里不赘述)。使用 LinFu,您可以轻松地在运行时逐步构建对象的实现,就像逐步构建 SQL 字符串一样。想在运行时动态实现一个接口,但没有在编译时实现它的类?没问题。LinFu 可以逐步缝合方法,在运行时形成一个连贯的接口实现。

带 Lambda 参数的委托(又称“柯里化”)

虽然 C# 3.0 和即将发布的 VB.NET 版本支持定义 Lambda“表达式”,但我一直想要的一个功能是能够获取任何委托并将其一个或多个参数硬绑定(或封闭)到特定的任意值。换句话说,此功能允许您通过在触发事件时硬编码传递给委托的某些(或所有)参数来“模拟”委托调用。换句话说,LinFu 可以非常轻松地模拟事件。

通用事件处理

编写使用 MVC 模式(或 MVP 模式)的应用程序时,最大的问题之一是找到一种统一的方式让控制器处理从表示层/视图触发的事件,而无需在编译时“预先”知道这些事件的签名。LinFu 实际上可以附加到运行时在任何对象上触发的任何事件,而不管该事件可能使用的委托签名是什么。

一个非常非常简单的 IoC 容器

想象一下,您有一个不依赖外部配置文件(如 XML 文件)来组装自身的控制反转(或依赖注入)容器。此外,假设您只需要通过几行代码和一个简单的 XCOPY 操作即可完成所有配置并启动。听起来太简单了吗?眼见为实,在第四部分,我将尽力让您相信。

契约式设计

在 .NET 中(甚至在 Java 中,如 JContract)创建 DbC 框架的尝试并不少(例如 eXtensible C#、Kevin McFarlane 的 DbC Framework 等),但很少有能够有效地模仿 Eiffel 语言的 DbC 机制,而不将其实现与特定语言绑定(如 XC# 的情况)或用大量的先决条件、后置条件和不变量检查来弄乱其域方法(如 McFarlane 的库)。LinFu 允许您在运行时透明地将先决条件、后置条件和不变量注入到您的代码中,无论您是否能够访问要注入库的源代码。想用用户定义的契约包装 System.Data.IDbConnection ?没问题,只需一行代码。想在运行时动态生成可验证的契约?LinFu 也可以做到。当与 Simple.IoC 容器结合使用时,它甚至可以在应用程序运行时注入契约,而无需重新编译整个应用程序。

如您所见,LinFu 可以做许多有趣的事情。通过发布这些文章,我希望能够通过使用我的库来帮助相当多的开发人员编写更好的代码。对我而言,回馈整个开发社区总是一件乐事,任何我能节省其他开发人员编写更多样板代码的时间都是值得的。那么,废话不多说,让我们开始吧!

背景

本文假设您熟悉动态代理的概念,对于那些想了解其工作原理的人,我将尽力在接下来的几节中解释,而不必深入 Reflection.Emit 的细节。但是,如果您是普通开发者,可以快速浏览接下来的几节来使用代码。

DynamicProxy

本文将向您展示如何使用 LinFuDynamicProxy 为您的方法代码添加“钩子”,以便您可以在运行时任意注入新代码。如果您想为应用程序添加额外功能(如日志记录或性能计时),但又不想让这些额外功能在代码库中变得杂乱,这将非常有用。

Using the Code

LinFuDynamicProxy 库允许您拦截对您对象实例中任何虚方法的调用,并在运行时用您自己的实现替换这些方法。例如:

public class Greeter
{
    public virtual void Greet()
    {
        Console.WriteLine("Hello, World!");
    }
}

在这个例子中,我们将包装 Greet() 方法,并将其问候语替换为“Hello, CodeProject!”。

选择您的风格

使用 LinFu,有两种拦截风格,如以下接口定义所示:

public interface IInterceptor
{
    object Intercept(InvocationInfo info);
}

public interface IInvokeWrapper
{
    void BeforeInvoke(InvocationInfo info);
    object DoInvoke(InvocationInfo info);
    void AfterInvoke(InvocationInfo info, object returnValue);
}

包装 Greeter 对象

根据您的需求,您需要实现这两个接口中的一个来创建您自己的拦截器。在这种情况下,我们将实现 IInvokeWrapper ,以便在调用 Greeter 类的 Greet() 方法之前和之后添加一些额外的行为:

public class GreetInterceptor : IInvokeWrapper
{
    private Greeter _target;
    public GreetInterceptor(Greeter target)
    {
        _target = target;
    }
    public void BeforeInvoke(InvocationInfo info)
    {
        Console.WriteLine("BeforeGreet() called");
    }

    public object DoInvoke(InvocationInfo info)
    {
        // Make our own greeting,
        // and ignore the old one
        Console.WriteLine("Hello, CodeProject!");

        object result = null;

        // Note: If you wanted to call the original
        // implementation, uncomment the following line:
        //result = info.TargetMethod.Invoke(_target, info.Arguments);
        return result;
    }

    public void AfterInvoke(InvocationInfo info, object returnValue)
    {
        Console.WriteLine("AfterGreet() called");
    }
}

您首先注意到的可能是,拦截器需要引用 greeter 对象的实际实例。这是因为 LinFu 的动态代理本身没有行为(或内在状态)。LinFu 生成的每个代理都会动态覆盖其父类的所有虚方法。每个覆盖的方法实现都会将方法调用委托给附加的 IInterceptor 对象。在许多方面,每个代理都像一个空壳。没有附加的拦截器,代理只会抛出 NotImplementedException

手动编写的代理

如果我手动编写代理,它会看起来像这样:

public class GreeterProxy : Greeter, IProxy
{
    private IInterceptor _interceptor;
    public override void Greet()
    {
        if(_interceptor == null)
        throw new NotImplementedException();

        // The following is pseudocode:
        InvocationInfo info = new InvocationInfo();

        // Note: The actual proxy would fill the info
        // object with the necessary method data
        // Pass the call to the interceptor
        _interceptor.Intercept(info);
    }

    public IInterceptor Interceptor
    {
        get 
        { 
            return _interceptor;  
        }
        set 
        { 
            _interceptor = value; 
        }
    }
}

您在这里可以看到,尽管代理可能继承了其原始基类的任何实现,但 LinFu 的动态代理仅将调用重定向到拦截器。当代理被调用以替代实际对象时,它会构造一个 InvocationInfo 对象,其中包含所有必要的反射详细信息。这样,拦截器就可以调用该方法的原始实现,或者用它自己的全新实现来替换它。

InvocationInfo 对象

InvocationInfo 类本身定义如下:

public class InvocationInfo
{
    // … Constructor omitted for brevity
    public object Target
    {
        get 
        { 
            return _proxy; 
        }
    }
    public MethodInfo TargetMethod
    {
        get 
        { 
            return _targetMethod; 
        }
    }
     public StackTrace StackTrace
     {
        get
        {
           return _trace;
        }
     }
     public MethodInfo CallingMethod
     {
        get
        {
           return (MethodInfo) _trace.GetFrame(0).GetMethod();
        }
     }
     public Type[] TypeArguments
     {
        get 
        { 
            return _typeArgs; 
        }
     }
     public object[] Arguments
     {
       get 
        { 
            return _args; 
        }
     }
  //
}

此类中的大多数属性都比较容易理解。Target 属性指向拦截了方法调用的代理实例,而 StackTrace 属性指向调用代理时调用堆栈的状态。

然而,这里似乎缺少的是对将为该代理提供实现的实际对象的任何引用(在本例中是 Greeter 对象)。这是故意的。就 LinFu 库而言,代理的唯一任务是将其实现委托给 IInterceptor 对象,该对象可能(或可能不)具有实际对象的实现作为其实现(即实际的 Greeter 对象)。从代理的角度来看,其他所有内容都是无关紧要的。

注意: 我将代理实例与代理实现(因此,与实际对象)分开,以便于同时管理多个代理。理论上,可以将一些代理池化并重复使用以节省内存,但这远远超出了本文的范围。我将留给读者来想出一个实现这一目标的方案。

整合

现在有了 GreetInterceptor 类,剩下的就是实例化代理并将其附加到 greeter

ProxyFactory factory = new ProxyFactory();
Greeter actualGreeter = new actualGreeter();
GreetInterceptor interceptor = new GreetInterceptor(actualGreeter);
Greeter greeter = factory.CreateProxy<Greeter>(interceptor);

一旦 greeter 代理对象被实例化并初始化了拦截器,剩下的就是运行它:

// Since the interceptor is in place, the original "Hello, World!" message
// will be replaced with "Hello, CodeProject!"
greeter.Greet();

调用 Greet() 方法后,代理会将调用重定向回 GreetInterceptor 实例,该实例将提供 Greet() 方法的替换实现。除了代理实例化本身之外,这个过程使得拦截方法调用相对透明。但是,如果您需要替换拦截器怎么办?

IProxy 接口

事实证明,LinFu 生成的每个代理都实现了 IProxy 接口。由于 IProxy 接口具有 Interceptor 属性,因此替换现有的 interceptor 就像执行以下操作一样简单:

IInterceptor otherInterceptor = new SomeOtherInterceptor();
IProxy proxy = (IProxy)greeter;
proxy.Interceptor = otherInterceptor;

// Call the new implementation
greeter.Greet();

关注点

LinFuDynamicProxy 最让我着迷的一点是它的原始速度。在我的 1.8GHz 双核笔记本上,生成单个代理只需要几毫秒。作为参考,我对 LinFu 的 Dynamic Proxy 和 CastleProject 的 DP2 进行了一个简单的基准测试。

基准测试设置

此基准测试已针对 LinFu 和 Castle DP2 库运行,代理类型缓存已关闭。这是为了防止任何结果因任一库的缓存命中而被歪曲。对于 Castle 的 DynamicProxy2,我不得不修改 BaseProxyGenerator.cs 的第 160 行来禁用缓存。

protected void AddToCache(CacheKey key, Type type)
{
    // NOTE: This has been disabled for benchmarking purposes
    //scope.RegisterInCache(key, type);
}

对于 LinFu,我通过在基准测试代码的 Program.cs 的第 104 行将代理缓存的引用设置为 null 来禁用代理缓存的使用。

…
ProxyFactory factory = new ProxyFactory();
// Disable the cache in order to prevent
// the factory from skewing the benchmark results
factory.Cache = null;
…

基准测试结果

Screenshot - LinFuGraph.png

此测试模拟了一种最坏情况场景,其中每个代理生成器(或工厂)有效地必须连续生成多达一千种唯一的代理类型。正如该基准测试的结果所示,当生成多达一百种唯一的代理类型时,这两个库的扩展性都很好。但是,一旦基准测试超过一百种类型的阈值,Castle 的性能就会明显下降。事实上,当它接近生成一千种唯一类型时,其速度比 LinFu 的实现慢五倍。除此之外,我认为基准测试本身就说明了问题。

但是,由于类型缓存默认在两个库中都已启用,因此多次调用生成相同代理类型将被缓存,并且这些调用(实际上)只会产生极少的开销。尽管如此,此基准测试显示了两个库之间的速度差异,并且如这些数字所示,LinFuDynamicProxy 比其 Castle 对等项快得多。

注意:如果您愿意进行自己的基准测试,我已经将基准测试代码包含在源代码中,以及我用于生成基准测试图表的 Excel 2007 电子表格。我还包含了一个修改后的 Castle.DynamicProxy2.dll 副本,其缓存已禁用,以防 CodeProject 上的其他人有兴趣添加更多基准测试。

限制

LinFu 的 Dynamic Proxy 生成器有几项无法做到的事情。它们是:

它不能继承自密封类型。

LFDP 本身无法覆盖密封类型。但是,当我讨论 LinFu.Reflection 时,我将向您展示如何绕过此限制。

它不能覆盖非虚成员。

这个很容易理解。最近,我一直在尝试使用 Mono.Cecil 库来使非虚方法变为虚方法,但到目前为止,我还没有找到足够健壮的东西可以包含在 LinFu 中。一旦找到可靠的方法,我将更新本文。

特别感谢

感谢 Jeff Brown 和 Julien Dagorn,LinFu.DynamicProxy 现在支持使用 refout 参数覆盖方法。谢谢你们的建议!

许可证

整个 LinFu 框架均在 LGPL 许可下提供,这意味着您可以在商业应用程序中免费使用库 DLL,无需支付版税,但如果您修改其源代码,则必须发布源代码,以便其他人也能使用。

下一篇文章预告

LinFuDynamicProxy 只是整个 LinFu 框架所能提供功能的初步部分。在下一篇文章中,我将向您展示如何结合使用 C# 3.0 的功能和 LinFu 在运行时动态生成接口实现。举个例子。假设在我的域模型中,我有以下接口定义:

public interface IPerson
{
    string Name 
    { 
        get; 
    }
    int Age 
    { 
        get; 
    }
}

public interface IDogOwner
{
    int NumberOfDogsOwned 
    { 
        get; 
    }
}

使用 LinFuDynamicObject 和 C# 3.0 的新功能(即匿名类型),我可以使用以下代码在运行时实现这些接口:

// Notice that this DynamicObject is actually wrapping a *bare* System.Object
// and that there is initially no underlying implementation

DynamicObject dynamicObject = new DynamicObject(new object())

// Implement IPerson
dynamicObject.MixWith(new { Name="Me", Age=18 });

// Implement IDogOwner
dynamicObject.MixWith(new { NumberOfDogsOwned=1 });

// If it looks like a person…
IPerson person = dynamicObject.CreateDuck<IPerson>();

// …then it must be a person!
// This will return "Me"
string name = person.Name;

// This will return '18'
int age = person.Age;

// …or an IDogOwner
IDogOwner dogOwner = dynamicObject.CreateDuck<IDogOwner>();

// This will return '1'
int dogsOwned = dogOwner.NumberOfDogsOwned;

现在,我将让您在脑海中消化最后一段代码。可以说,LinFu 允许开发人员在其习惯的静态类型语言环境中体验动态语言的特性。同时,我将去写下一篇文章。敬请期待!

历史

  • 2007年10月15日 -- 发布原始版本
  • 2007年10月26日 -- 更新文章和下载
  • 2007年11月5日 -- 更新所有下载
  • 2007年11月12日 -- 更新源码和二进制下载
© . All rights reserved.