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

流畅方法和类型构建器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

5.00/5 (73投票s)

2013年6月7日

Apache

24分钟阅读

viewsIcon

142346

downloadIcon

855

使用简单流畅的API在运行时创建方法和类型。

NuGet

感谢Mackenzie Zastrow,现在有了一个NuGet包。您可以在https://nuget.net.cn/packages/Pfz.TypeBuilding/[^]找到它。

示例

即使以下示例看起来像Basic代码,它实际上是用于生成动态方法的C#代码,该方法将读取文本文件并将所有非空行写入控制台。

string fileName = null;
string line = null;
StreamReader streamReader = null;

var method = 
  new FluentMethodBuilder(typeof(void)).
  AddParameter(() => fileName).
  AddLocal(() => streamReader).
  AddLocal(() => line).
  Body.
    Using(() => streamReader, () => new StreamReader(fileName)).
      Loop().
        Assign(() => line, () => streamReader.ReadLine()).
        If(() => line == null).
          Break().
        EndIf().
        If(() => line.Length > 0).
          Do(() => Console.WriteLine(line)).
        EndIf().
      EndLoop().
    EndUsing().
  EndBody().
  Compile<Action<string>>();

method(@"Test.txt");

背景

我喜欢动态代码,我说的不是允许我们以动态方式访问现有对象的dynamic关键字,我说的是在运行时生成的代码。这开启了许多可能性,特别是实现难以遵循或模式庞大的类,也为轻松实现面向方面编程提供了可能性。

可以说,我使用了以下解决方案来生成动态代码:

  • 在运行时生成一个单元,将其保存到磁盘,调用编译器完成工作,最后加载库(我在10年前在Delphi和C++ Builder中做过);
  • 在运行时生成一个单元,使用CodeDOM编译它(实际上,这是逆向使用CodeDOM,但它有效),最后加载库;
  • Reflection.Emit创建动态方法和模块,这些方法和模块可以直接加载,无需将文件写入磁盘(但使用起来要困难得多);
  • 并使用可编译的Linq表达式。

为了避免重复,我编写了一些解决方案,以便在运行时更容易生成新类型,无论是通过实现接口并调用代理对象来处理调用,还是通过使用DelegatedTypeBuilder,它允许创建新方法,其实现由委托提供。

然而,仍然存在许多问题:实现接口的解决方案只将接口声明的方法重定向到代理对象,禁止用户在运行时创建新属性,并且要求单个方法能够处理任何运行时生成的方法,以无类型方式使用参数。DelegatedTypeBuilder需要一个已经能够完成所有代码的委托,这对于小型模式来说可能没问题,但如果需要新字段,或者生成的代码可以用不同的方式编写,它会受到极大限制。

此外,还有Microsoft解决方案的固有局限性。LINQ表达式只编译为动态方法,而不是整个类型。Reflection.Emit动态模块无法调用其他库的私有成员,甚至无法调用创建它的库的私有成员,这迫使我在不应该公开的时候公开许多东西。因此,考虑到所有限制,当我想快速无约束地创建新的动态方法,或者创建需要处理每个属性2个或更多字段的新类型时,现有解决方案都无济于事。所以,在一个灵光乍现的时刻(或者可能是谵妄,因为我病了发烧),我决定创建一个新的、更易于使用的东西。

而这个新东西就是一个**流畅的**类型和方法构建器。

它是如何工作的 - 非常简短的解释

我不会详细解释该库的内部工作原理,但主要几点是:

  • LINQ表达式已经可编译,但C#编译器在解析表达式时不支持所有构造(例如赋值变量),所以我决定添加方法来使其更容易。
  • LINQ表达式也是不可变的,并且它们在构造函数中接收所有参数。但我通常想要(我认为大多数人也是)的是逐步构建一个方法,有效地添加语句,可能做一些测试并添加更多语句。这就是FluentMethodBuilder的工作方式。此外,使用流畅的API,如果您想一次性构建整个方法(就像我在第一个示例中那样),它看起来会更自然。
  • LINQ表达式是一个方法,而不是一个类型,但它们可以访问私有字段(LINQ中使用的局部变量成为私有字段)。动态模块可以创建整个类型,但它们不能访问私有字段。但是,可以编译一个LINQ表达式,创建委托,然后构建一个调用该委托的动态模块。

性能

如果您担心性能,那么请放心。生成的代码非常快。因为它直接在内存中编译表达式,所以避免了浪费时间生成文本文件、将其保存到磁盘,然后调用大型编译器(使用CodeDOM或类似解决方案通常会发生这种情况)。然后,如果您正在编译一个独立的方法,它将有一个虚拟调用被调用,或者,如果您正在编译一个完整的类型并通过接口访问它,您将有两个虚拟调用。如果您将AddMethodrespectVisibility参数设置为true,甚至可以将其减少为单个虚拟调用,但在这种情况下,生成的方法将无法访问其生成器的内部或私有成员。

使用代码

这个库的核心有三个类:

  • FluentMethodBuilder:这是您必须使用的类,无论您是想创建单个独立方法还是完整类型;
  • FluentTypeBuilder:如果您需要创建整个类型而不是单个独立方法,您应该从FluentTypeBuilder开始。添加到它的每个方法(您必须记住属性的get和set都是方法)都将使用FluentMethodBuilder
  • FluentExpression:这个类型不是必需的,但它使在运行时修改表达式变得更容易,因此,它会使您更容易操作。

那么,让我们分别看看它们

FluentMethodBuilder

如果您想创建一个不绑定到特定类型的方法,您应该直接实例化一个FluentMethodBuilder。您只需在其构造函数中提供结果方法的返回类型。

使用这样的方法构建器,您有以下可能性:

  • 添加参数;
  • 添加局部变量;
  • 添加外部变量;
  • 填充其主体;
  • 编译它。

添加参数

要添加参数,您应该将其在代码中声明为局部变量,并为其设置默认值。这仅是为了使所有将由流畅方法调用构建的不同表达式能够访问该参数,并且默认值只是一个验证要求,因为在构建表达式时我们不能让变量没有值(因为C#编译器会抱怨),我选择仅检查默认值以明确它没有接收您局部变量中声明的值。

然后,在调用AddParameter()时,您应该提供一个将访问该局部变量的表达式。因此,要声明一个名为name、类型为string的参数,您应该这样做:

string name = null;
method.AddParameter(() => name);

从那时起,您可以通过以下方式让您的动态方法使用该参数:

method.Body.Do(() => Console.WriteLine("The name is: " + name));

当您编译该方法时,它将被编译为一个接收类型为stringname参数的方法,并且所有使用您的局部name的访问实际上都将使用该参数。

添加局部变量

当我们自己编写代码时,我们通常可以两次访问同一成员,两次执行同一get方法,或者我们可以只访问一次,将其放入局部变量,然后多次访问局部变量。

嗯,在流畅方法中,您也可以添加局部变量。它们的工作方式与参数完全相同。您应该在构建器方法中声明一个局部变量,并将其设置为其默认值,仅为了使该变量可被用于构建方法的所有表达式访问,然后您调用AddLocal(),并带有一个直接访问该变量的表达式,这足以使该变量可供您的调用访问。

添加外部变量

这样做只是为了避免错误。当您执行类似以下操作时:

method.Body.Assign(() => variable, () => 5);

生成的表达式将自然地访问这样的“变量”,但它会与所有线程以及对生成方法的所有调用共享该变量。这通常不是您想要的,因此您可能需要调用AddLocal()

但是,如果您想共享这样的变量呢?这就是它自然的工作方式,但在我的最初测试中,我经常忘记将变量声明为变量。代码工作正常,只有当我创建其他实例时才会出现错误。因此,为了避免错误,我决定在您访问字段(表达式使用的局部变量就是它)而没有说明如何使用它时生成异常。

因此,如果您希望它被生成器代码和生成代码的所有调用共享,则应使用AddExternal()调用注册该变量。它与AddParameter()AddLocal()具有完全相同的语法,但它只会避免异常,因为这样的变量不需要编译为其他东西。

填充其主体

这就是奇迹真正发生的地方,也是这段代码与LINQ表达式分歧最大的地方,即使它大量使用了它们。

事实是:Body将成为LINQ的BlockExpression进行编译。这里最大的区别在于我们如何构建它。当使用普通的LINQ表达式时,我们被迫在外部部分之前创建内部部分。在小案例中这不是大问题,但表达式越复杂,编写它(以及以后阅读它)就越困难。

使用流畅的API,我们有Body,我们可以不断“添加”新的操作。如果您已经有一个表达式(或者您可以直接用C#编写),只需调用Do()方法,它将把该表达式纳入方法主体中。

作为流畅的API,您可以执行以下操作:

method.
  Do(() => Console.WriteLine("Action1")).
  Do(() => Console.WriteLine("Action2"));

或者您可以这样做:

method.Do(() => Console.WriteLine("Action1"));
method.Do(() => Console.WriteLine("Action2"));

因此,如果您在决定要添加到方法中的内容之前有一些条件,您可以一次做一件事。您不需要一步构建整个表达式。

此外,还有许多不同的方法可以使事情变得更容易,或者仅仅克服C#编译器表达式生成的自然限制。例如,C#编译器不允许我们这样做:

method.Do(() => value = 5);

它理解这个命令,但它说赋值表达式不支持(好吧,至少在C# 4中是这样,我不知道版本5甚至未来的版本)。

因此,为了解决这个问题,还有其他方法,比如Assign()。有了它,您可以这样做:

method.Assign(() => value, () => 5);

注意:我们这里使用了两个表达式,一个用于访问变量,一个用于获取值,然而这两个表达式将成为一个AssignExpression,并在编译阶段被整合到一个单一的块表达式中。

Assign()更有趣的是创建新块的方法,例如If()Try()Loop()While()For()

说到循环方法,我认为这是LINQ表达式中的一个bug。我不能直接添加breakcontinue。我应该给它们循环的“目标”到开头或结尾,如果我给错了目标,break可能会变成continuecontinue可能会变成break。所以,它们只是在做goto,不应该有不同的表达式。但我解决了这个问题,所以你可以调用没有参数的Break(),它会退出其包含的循环。

编译方法

要编译该方法,只需调用FluentMethodBuilderCompile()方法。

如果您没有为其提供委托类型,则会选择一个默认类型。如果您提供委托类型,它必须与方法兼容。验证部分实际上是由LINQ表达式完成的。

如果您在编译时知道委托类型,可以使用泛型Compile<T>()方法,以避免提供类型并对获得的结果进行相同类型的强制转换。

FluentTypeBuilder

FluentTypeBuilder没有流畅的API,但是每个添加的方法都将使用FluentMethodBuilder,因此最重要的部分将是流畅的。

创建FluentTypeBuilder时,您必须提供一个泛型参数。这是将用于生成类型的“基本”类型。如果您没有任何偏好,它应该是object

在构造函数中,您还可以传递类型将实现的所有接口。

然后,您可以执行以下操作:

  • 添加字段 - 这使用与向方法添加局部变量和参数相同的语法;
  • 添加方法;
  • 添加属性;
  • 添加事件;

在您添加了所有需要的代码之后,您可以:

  • 编译类型。这将返回一个Type实例,因此您需要使用反射;
  • 调用GetConstructorDelegate()。这将返回一个创建生成类型实例的委托,无需使用反射,这更快,是创建生成类型实例的理想方式。

事实上,关于FluentTypeBuilder没什么好解释的,因为重要的部分是方法生成(无论是真实方法还是使用get/set方法的属性),而那是FluentMethodBuilder的工作。

FluentExpression

FluentMethodBuilder有一个Body属性,您可以在其中使用流畅的API来添加新语句。问题是:例如,在执行If()时,您可能没有作为If()参数提供的完整表达式。

您可能希望在运行时组合不同的And()Or()条件,但组合LINQ表达式并不容易(事实上,使用Lambda表达式而不是其内部Body添加任何条件通常会在以后产生奇怪的错误,而不是立即的异常)。

因此,为了避免这些问题,您可以使用FluentExpression

有了它,您可以执行以下操作:

var expression = FluentExpression.Create(() => firstCondition);

if (mustCheckSecondCondition)
  expression.And(() => secondCondition);

if (useOrCondition)
  expression.
    Or(() => firstOrCondition).
    Or(() => secondOrCondition);

您可以很好地通过手动组合表达式来做任何您想做的事情,但我真的认为这更容易使用。

方法摘要

我已经简要介绍了FluentMethodBuilder能做什么。现在,我想列出您可以使用的构造:

方法 使用示例
Do
block.Do(() => Console.WriteLine("Test"));
返回
block.Return(() => 5);
Assign
block.Assign(() => fieldOrVariable, () => value);
If
block.
  If(() => x < 10).
    Do(() => Console.WriteLine("x is less than 10.")).
  Else().
    Do(() => Console.WriteLine("x is at least 10.")).
  EndIf();
Using
block.
  Using(() => variable, () => new DisposableObject()).
    Do(() => Console.WriteLine("Here we can use the disposable object.")).
  EndUsing();
循环
block.
  Loop().
    Do(() => Console.WriteLine("This is an infinite loop.")).
  EndLoop();
While
block.
  While(() => Application.IsRunning).
    Console.WriteLine("New messages will be written while the application is running.").
  EndWhile();
For
block.
  For(() => i, () => i<10, FluentExpression.Increment(() => i)).
    Do(() => Console.WriteLine(i)).
  EndFor();
注意:变量i必须已在方法中声明为局部变量。
Break and Continue
block.
  Loop().
    Assign(() => line, () => streamReader.ReadLine()).
    If(() => line == null).
      Break().
    EndIf().
    If(() => line.Length == 0).
      Continue().
    EndIf().
    Do(() => Console.WriteLine(line)).
  EndLoop();

注意:我修改了文章开头提供的代码,使用了Continue()方法。当然,您可以按照自己觉得更舒适的方式编写代码。

Try, Catches and Finally
block.
  Try().
    Do(() => MethodThatMayThrowExceptions()).
  Catch(() => ioException).
    Do(() => Console.WriteLine("An IOException was thrown.")).
  Catch(() => exception).
    Do(() => Console.WriteLine("An exception was thrown and it was not an IOException.")).
  Finally().
      Do(() => Console.WriteLine("Finalizing independently if expections were thrown or not.")).
  EndTry();

注意:此代码假定变量exceptionioException存在,并且它们分别具有ExceptionIOException类型。

呼叫
block.Do(() =>anotherFluentMethod.Call("argument 0", 57, ...));

注意:Call()方法存在于类型化的FluentMethodBuilderFluentVoidMethodBuilderFluentMethodBuilder<T>)中,它允许您调用位于您当前正在构建的同一类型上的另一个方法,因为不可能直接引用尚未构建的方法。此方法仅用于构建表达式,如果直接调用,它将不起作用。

Inline
block.Inline(anotherFluentMethod);

注意:此方法将把另一个FluentMethod的主体合并到此方法中。这不是对另一个方法的调用,它仅用于支持在某些期望表达式的情况下传递整个方法。建议在大多数情况下使用刚刚介绍的Call()方法。

注意:我在示例中使用的所有*块*语句都指的是一个包含块。默认的是FluentMethodBuilderBody,但当您执行Loop()时,例如,它会生成另一个*块*,就像If、Try、Catch等生成块一样。此外,在每个操作结束时,总是可以用**。**替换**;**,这样您就可以添加新的语句。

附加功能

这个库的核心是FluentTypeBuilder。有了它,在运行时创建新类型并添加您想要的任何方法、属性、事件和字段都非常容易。

但我应该说,我通常做的是在运行时使用特定模式实现接口,因此,为了避免总是做相同的寻找所有应该实现的属性、事件和方法的代码,我决定让它变得更容易。

如果您的目的是在运行时实现接口或抽象类,请参阅AbstractTypeImplementer类。您应该继承它来完成工作,它具有您应该重写的抽象方法,这些方法将被调用以实现方法、事件和属性,并且它们将使用属性和事件的正确类型,以便您可以轻松构建表达式。

例如,这是NotifyPropertyChangedImplementer的完整代码:

using System;
using System.ComponentModel;
using System.Reflection;
using System.Linq.Expressions;
namespace Pfz.TypeBuilding.AbstractTypeImplementers
{
  public sealed class NotifyPropertyChangedImplementer<TAbstract>:
    AbstractTypeImplementer<TAbstract>
  {
    private readonly Expression<Func<PropertyChangedEventHandler>> 
                     _eventHandlerFieldExpression;

    public NotifyPropertyChangedImplementer():
      base(typeof(INotifyPropertyChanged))
    {
    }
    public NotifyPropertyChangedImplementer(
      Expression<Func<PropertyChangedEventHandler>> eventHandlerFieldExpression):
      base(typeof(INotifyPropertyChanged))
    {
      _eventHandlerFieldExpression = eventHandlerFieldExpression;
    }

    private NotifyPropertyChangedGenerator<TAbstract> _generator;
    protected override FluentTypeBuilder<TAbstract> CreateTypeBuilder(Type[] additionalInterfaces)
    {
      var result = base.CreateTypeBuilder(additionalInterfaces);
      _generator = new NotifyPropertyChangedGenerator<TAbstract>(result, _eventHandlerFieldExpression);
      return result;
    }

    protected override void ImplementProperty<T>(
      FluentTypeBuilder<TAbstract> typeBuilder, PropertyInfo property)
    {
      _generator.AddProperty<T>(property.Name);
    }

    protected override void ImplementEvent<T>(
      FluentTypeBuilder<TAbstract> typeBuilder, EventInfo eventInfo)
    {
      if (eventInfo.EventHandlerType != typeof(PropertyChangedEventHandler))
        throw new NotSupportedException();
    }

    protected override void ImplementMethod(
      FluentTypeBuilder<TAbstract> typeBuilder, MethodInfo method)
    {
      throw new NotSupportedException();
    }

    protected override void ImplementIndexerUntyped(
      FluentTypeBuilder<TAbstract> typeBuilder, PropertyInfo indexer)
    {
      throw new NotImplementedException();
    }
  }
}

这个类能够实现类(或接口)的抽象属性,以便它们遵循以下模式:get是直接的字段访问,set应该在新值与旧值不同时调用PropertyChanged事件。

如您所见,如果存在任何抽象方法或索引器,或者存在不是PropertyChanged的事件,它将抛出NotSupportedException。您还可以注意到,要添加属性,需要调用_generator.AddProperty

这样的生成器(NotifyPropertyChangedGenerator泛型类)的唯一目的是创建遵循INotifyPropertyChanged模式的属性,但它具有以下功能:

  • 如果它正在实现一个没有PropertyChanged事件的接口或抽象类,它可以添加该事件;
  • 如果您正在向已经有此类事件的抽象类添加新属性,您可以提供一个表达式来访问事件处理程序字段,以便新属性将触发已存在的事件;
  • 您可以告知它在该属性更改时应通知其他属性的更改(如果您有计算属性,则很有用);
  • 您可以添加一个属性,提供get和set方法。这不是错误,您提供了不生成事件的get和set方法,生成器将能够实现set来验证值是否已更改,调用您的set,然后生成事件。这在您想要创建计算属性、想要在set上进行验证或想要创建将所有get和set重定向到模型并添加更改通知的ViewModel时很有用。

所以,这里有一些关于如何使用它的基本示例。

// The following generator does not tells how to access the
// field that stores the PropertyChanged handler, so it is considered
// that such event does not exist and must be declared.
var generator = NotifyPropertyChangedGenerator.Create(typeBuilder);

// The next statement adds a property named Name, which have its 
// own backing field and will only notify its own change.
generator.AddProperty<string>("Name");

// The previous line could be written as:
generator.AddProperty("Name", typeof(string));

// The next line is an example of a property that notifies that more
// than one property was changed:
generator.AddProperty<DateTime>("Birthdate", "Age");

// And again, it could be written as:
generator.AddProperty("Birthdate", typeof(DateTime), "Age");


// And the next one is the most complex case: The get and set
// will be given. In the set, there is a validation before
// actually accepting the value:
int positiveIntField = 0;
typeBuilder.AddField("positiveIntField", () => positiveIntField);

int value = 0;
var setAction = FluentMethodBuilder.CreateAction().
  Body.
    If(() => value < 0).
      Throw(() => new ArgumentException("Value must be greater than 0.")).
    EndIf().
    Assign(() => positiveIntField, () => value).
  EndBody();

generator.AddProperty("PositiveInt", () => 
        positiveIntField, setAction, () => value, "Age");

动态关键字

至少在.NET 4中,dynamic关键字无法访问运行时生成的类型。起初我以为是我的错误,因为我将所有方法都生成为私有方法,但即使它们是公共的,也无法访问。我觉得这真的很奇怪,因为反射可以看到所有属性和方法,WPF能够绑定到运行时生成对象的属性,我可以通过接口访问它们,但dynamic关键字就是无法访问它们。

即使这很奇怪,我也不觉得有什么大问题。如果您读取一个名为X的属性,您就知道存在这样的属性,那么为什么不创建一个具有该属性的接口并要求运行时生成的类型实现它呢?而且,如果您真的想要对对象的运行时访问(例如DataGrid所做的),它也会工作。但我之所以评论这个问题,是因为我第一次尝试用动态创建的对象做的事情就是通过dynamic关键字访问它,但失败了。

Roslyn

我知道微软创建了Roslyn项目,将C#和VB编译器作为服务公开。我从未真正使用过它,但我相信有人可能会认为我的项目也在做同样的事情。

我不知道Roslyn是否有一个流畅的API来创建动态方法,但我相信我的库比Roslyn更小、更专注。我不是试图创建一个完整的编译器或暴露可供编译器使用的组件,我只是创建一个小型的库,允许轻松创建动态类型和方法。如果您正在使用Roslyn并且它对您来说运行良好,我真的不认为这个库会对您有用。但是,如果您想要一个可以完成工作的小型库,那么这个库可能就是您需要的。

示例

可下载的解决方案包含库和一个小示例。您可以将该示例视为如何做事的指南,而不是应用程序。如果您想运行它并看到一些美好的东西,那么您会感到沮丧。但是,如果您有兴趣了解如何使用该库,它可能会给您一些示例。

与其他文章不同,本文中的库不包含我的任何其他个人库。它只是一个您可以随意使用的小型库。

我的测试和决定的概述

我肯定不会写关于如何编写这个库的教程,但我会解释一些关于它是如何实现的细节。

在文章的开头,我说表达式只能编译单个方法,而Reflection.Emit动态模块无法调用其他库中声明的私有成员(甚至不能调用创建它的库中的私有成员)。

因此,为了解决这个问题,我决定将两者结合起来:我使用表达式来编译单个方法,然后使用Reflection.Emit来创建一个包含我需要的所有方法的动态模块,以简单地重定向到由表达式创建的方法。

我以为那将直接可行,但Expression编译其方法的模块也无法被动态模块访问。看到LambdaExpression中有CompileToMethod()方法,我尝试了它,相信也许那会解决我的问题。但那个方法有两个问题:

  1. 它只是抛出一个没有告诉我哪里出错了的异常。在网上搜索后,我发现它只能编译为静态方法;
  2. 我尝试创建静态方法,并从实例方法重定向到静态方法,但生成的代码仍然存在无法调用其他模块私有字段的限制。这看起来像是违反了可见性特性,但我们在表达式中直接使用的所有局部变量自然地被编译为私有字段。

但我找到了一个解决方案:动态模块中的方法可以调用表达式生成的委托。所以,最初,我编译表达式(每个FluentMethodBuilder将生成一个表达式并编译它),将所有委托放入一个数组中,然后动态模块被编译,其中包含访问该数组、获取正确委托并调用它的代码。

这在第一次测试中运行良好,但随后出现了大问题:我如何访问将添加到我的类型中的字段?

即使我在编译表达式之前调用DefineField(),类型也尚未编译,并且表达式只能调用已经编译的类型。最初,我没有尝试改变逻辑,而是将每个字段视为对ConditionalWeakTable的访问。这有效,但除了这样做带来的性能影响之外,我不喜欢这样调试类型时,它简单地没有任何字段的事实。

但我做错了一件事:我正在编译表达式,以便能够获取它们的委托类型(以便动态模块可以调用它)。但是动态模块只需要知道委托类型,而不是实际的委托。

考虑到委托类型由表达式参数和返回类型定义,我决定创建一个“预编译”。在生成动态模块时,我只生成一个带有所有参数和返回类型的空表达式。通过这种方式,我可以获取委托类型,而无需尝试真正编译将访问尚不存在的字段的表达式。有了委托类型,我就能够编译动态模块。

然后我访问并用对真实字段的访问(毕竟,它现在已编译)替换表示字段访问的表达式,一切都正常工作。

所以,这就是我构建一个由许多单独编译的方法组成的类型的方式,这些方法能够很好地访问相同的字段。重要的是要知道,即使表达式方法是“静态的”,也总是有可能将它们将作用的实例作为参数给出,我当然会这样做。

流畅接口

该库的流畅接口本身就是另一个问题。我从一段“代码块”开始。它只有一些方法(比如Do()),这些方法添加了表达式并返回了this

通过返回this,返回类型是块本身的类型(FluentBlockBuilder)。

问题出现在我添加If()方法时,它也是一个块。首先,我希望通过调用EndIf(),它能返回到正确的块。其次,由于它是一个块,我考虑从FluentBlockBuilder继承,但是由于对Do()的调用已经返回了作为FluentBlockBuilder强制转换的this,我失去了Else()方法。

这里我有许多解决问题的方法:

  • 我可以把所有方法(比如Else()Catch()等)都放在基块中,即使它们只在特定块中起作用。当表达式写得很好时,这会很好用,但那样我就只能有一个EndBlock()方法,而不是更具体的EndIf()EndTry()等。此外,用户会一直看到这些Catch()Else()等,这可能会让人困惑。
  • 我可以将所有FluentBlockBuilder方法创建为受保护且不带返回值。然后每个继承者都应该创建所有相同的方法来调用真实方法并返回正确类型的this。这肯定会奏效,但每次添加新方法时都必须重新审视所有可能的块构造是可怕且容易出错的。
  • 我可以将所有FluentBlockBuilder方法创建为扩展方法。例如,我在CTM模型中就是这样做的。扩展方法可以是泛型的,因此结果类型可以与输入类型相同。也就是说,像T Do<T>(T instance)这样的方法,如果instanceint,结果类型将是int;如果instancestring,结果类型将是string;对于我的问题,如果实例是FluentIfBuilder类型,结果类型将是FluentIfBuilder。但我个人尽量避免扩展方法,因为如果您在没有添加正确的using的情况下访问对象,编辑器将不会显示扩展方法,编译器也找不到它们。
  • 最后,我使用了一个解决方案。我让FluentBlockBuilder接收两个泛型参数。其中一个是要返回的类型。也就是说,如果我有一个Loop(),然后有一个If(),调用EndIf()应该知道它将返回到Loop块。另一个是实际的子类型。我应该说,在正常情况下,我会说这样做的人不理解OOP或对泛型理解很差,但它有效。这就是为什么FluentIfBuilder有一个泛型参数(它将返回的类型)并继承自FluentBlockBuilder<TParentBuilder, FluentIfBuilder<TParentBuilder>>。它可能看起来“if”继承自“if”,但它说的是:我继承自FluentBlockBuilder,我希望this的所有结果都被转换为这个特定的If块,而不是一些不太精确的类型。当然,如果我创建了一个类型X,它将不同的类型作为第二个泛型参数,这可能会成为一个问题,但由于FluentBlockBuilder不能从其他库继承,所以这没问题。

我相信还有其他可能感兴趣的事情,但在我看来,这些是最重要的。

版本历史

  • 2013年10月16日。将许可证更改为Apache并添加了codeplex链接;
  • 2013年6月9日。在AddMethod()方法中添加了respectVisibility参数,并添加了子主题“性能”;
  • 2013年6月7日。初始版本。
© . All rights reserved.