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

Flexpressions

starIconstarIconstarIconstarIconstarIcon

5.00/5 (19投票s)

2012年9月9日

CPOL

10分钟阅读

viewsIcon

43377

downloadIcon

575

生成 LINQ 表达式的直观流畅 API。

Flexpressions - By Andrew Rissing

有关中间版本,请参阅 GitHub 仓库[^]

引言

随着 .NET 3.5 的推出,开发人员获得了一个强大的工具,用于在运行时生成代码,即 System.Linq.Expressions。它结合了编译代码的效率和反射命名空间的灵活性等优点。

遗憾的是,当表达式以 lambda 表达式 的形式表示时,其使用存在相当大的限制。以下是 C# 语言规范 5.0(即 .NET 4.5)的摘录:

某些 lambda 表达式无法转换为表达式树类型:即使转换存在,它也会在编译时失败。如果 lambda 表达式

  • * 具有块体
  • * 包含简单或复合赋值运算符
  • * 包含动态绑定表达式
  • * 是异步的

Flexpressions(Fluent-expressions)是我针对前两条(*)提出的解决方案。此外,我还添加了一些高级抽象,以简化表达式的构建。此外,我还包含了一些对于处理表达式的人可能有用的实用程序类。

示例 #1

为了理解 API 的工作原理,以下是使用 Flexpressions 编写的求和函数:

Func<int[], int> sumFunc = Flexpression<Func<int[], int>>
    .Create(false, "input")
        .If<int[]>(input => input == null)
            .Throw(() => new ArgumentNullException("input"))
        .EndIf()
        .If<int[]>(input => input.Length == 0)
            .Throw(() => new ArgumentException("The array must contain elements.", "input"))
        .EndIf()
        .Declare<int>("sum")
        .Set<int>("sum", () => 0)
        .Foreach<int, int[], int[]>("x", input => input)
            .Set<int, int, int>("sum", (sum, x) => sum + x)
        .End()
        .Return<int, int>(sum => sum)
    .CreateLambda()
    .Compile();
 
var result = sumFunc(Enumerable.Range(0, 10).ToArray()); // 45
var result2 = Enumerable.Range(0, 10).Sum(); // 45

要使用表达式生成上述代码,则需要编写相当复杂的表达式,跨越数页,并与反射代码交错,如 此处 所示。仅从代码维护的角度来看,Flexpressions 的优势显而易见。

示例 #2

虽然 Flexpression 类是流畅的,但也可以以非流畅的方式使用。因此,API 允许各种解决方案,这些解决方案可以具有表现力和动态性。以下是一个用于字符串化给定类型属性和字段的函数:

class Program
{
    static void Main(string[] args)
    {
        var action = Expressions.Stringifier<MyClass>(true, false);
        var result = action(new MyClass() { A = 123, B = 23.21, C = 31241, D = "abcdf" });
    }
}

public class MyClass
{
    public int A { get; set; }
    public double B { get; set; }
    public long C { get; set; }
    public string D { get; set; }
}

public static class Expressions
{
    public static Func<T, string> Stringifier<T>(bool writeProperties, bool writeFields)
    {
        var block = Flexpression<Func<T, string>>.Create(false, "obj");

        // The Flexpression objects don't need to be called fluently.
        if (typeof(T).IsClass)
            block.If<T>((obj) => obj == null)
                .Throw(() => new ArgumentNullException("obj"))
                .EndIf();

        return block
            .Set<StringBuilder>("sb", () => new StringBuilder())
            .WriteMembers<Flexpression<Func<T, string>>, T>(writeProperties, writeFields)
            .Return<StringBuilder, string>(sb => sb.ToString())
            .Compile();
    }

    // Using extension methods, the fluent nature of the API can be extended seamlessly.
    private static Block<T> WriteMembers<T, O>(this Block<T> block, bool writeProperties, bool writeFields) where T : IFlexpression
    {
        MemberExpression memberExpression;
        ParameterExpression paramSb = block.GetVariablesInScope().First(x => x.Name == "sb");
        ParameterExpression paramObj = block.GetVariablesInScope().First(x => x.Name == "obj");

        foreach (MemberInfo mi in typeof(O).GetMembers())
        {
            if (((mi.MemberType == MemberTypes.Field) && writeFields) || ((mi.MemberType == MemberTypes.Property) && writeProperties))
            {
                string prefix = string.Format("{0}: ", mi.Name);

                // sb.Append(prefix);
                block.Act
                (
                    Expression.Call
                    (
                        paramSb,
                        typeof(StringBuilder).GetMethod("Append", new[] { typeof(string) }),
                        Expression.Constant(prefix, typeof(string))
                    )
                );

                memberExpression = Expression.MakeMemberAccess(paramObj, mi);

                // No error checking here for nulls, but you get the idea...
                // sb.AppendLine(obj.Member.ToString());
                block.Act
                (
                    Expression.Call
                    (
                        paramSb,
                        typeof(StringBuilder).GetMethod("AppendLine", new[] { typeof(string) }),
                        Expression.Call(memberExpression, memberExpression.Type.GetMethod("ToString", Type.EmptyTypes))
                    )
                );
            }
        }

        return block;
    }
}

示例 #3

如果您想使用 API,但又不介意其潜在的性能开销,那么 T4(文本模板)和 ExpressionExtensions.ToCSharpString() 可以提供帮助。

通过修改示例 #2 中的 Stringifier 方法以生成 LambdaExpression,而不是立即进行编译,您可以创建如下所示的 T4 文件:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System.Core" #>
<#@ Assembly Name="$(ProjectDir)$(OutDir)$(TargetFileName)" #>
<#@ Import Namespace="Test" #>
using System;

namespace Test
{
    public class Utility
    {
        public static Func<MyClass, string> GetMyClassStringifier()
        {
<#= Expressions.Stringifier<MyClass>(true, true).ToCSharpString() #>
            return expression.Compile();
        }
    }
}

ExpressionExtensions.ToCSharpString() 将在变量 expression 中生成表达式,该变量只需包装在方法中并编译即可生成所需的委托。通过这种方式使用 API,可以将 Flexpressions 的可维护性与硬编码表达式树的性能结合起来。

诚然,如果您打算以这种方式使用 Flexpressions,您可能应该投入时间来从 T4 模板生成代码。不过,表达式并不遵守与编译代码相同的规则(例如,访问私有成员),因此可能有有限的用途。最终,此示例的目标是强调 ExpressionExtensions.ToCSharpString() 的强大功能。

特点

Flexpression API 目前提供以下功能:

  • 可以构建任何 Func/Action 委托类型。
  • 可以限制或允许使用 外部(或捕获)变量
  • 可以生成表达式树或直接生成类型化委托。
  • 可以为委托的输入提供参数名称,如果没有,则会自动生成。
  • 语言结构
    • 通用操作(例如,方法调用)(参见 Block 上的 Act 方法)
      • Act 方法还提供了直接提供 Expression 对象的能力,以规避 API 的干扰。
    • Do/While/Foreach 循环
      • Break
      • Continue
    • 插入标签
    • Goto 语句
    • If/ElseIf/Else 块
    • 赋值(参见 Block 上的 Set 方法)
      • 如果变量尚未声明,则自动声明。
    • Switch 语句
      • Case/Default 块
      • Case 和 Default 语句可以链接在一起(这不是 switch 语句的 fall through)
    • Throw 语句
    • Try 块
      • Try/Finally
      • Try/Catch
      • Try/Catch/Finally
      • Catch
      • Catch<T>(Exception)
      • Catch<T>(Exception e)
    • Using 块
  • 100% 代码覆盖率的单元测试

Flexpression API 中打包的实用程序类提供以下功能:

  • 将表达式树反向工程为 C# 代码
    • 用于学习表达式树、调试和使用 T4 模板生成代码。
  • 表达式重写器,用于将表达式的参数替换为提供的列表。
  • 提取类型的真实名称,而不是别名版本(例如,List`1 => List<int>)。

API 概述

表达式是不可变的。重写表达式树是 可能的,但这样做实际上只是根据旧树构建新树。最终,构建表达式树的最佳方法是使用外部脚手架。这种设计约束最终有利于流畅的接口。

在设计流畅接口时,我使用了 EditorBrowsableAttribute 来减少智能感知中的混乱。该属性可以简化 API,以帮助减少使用 Flexpressions 时的心理障碍(有关更多信息,请参见 此处)。

注意事项

  • 您仍然可以调用带有 EditorBrowsableAttribute 的方法,但这样做不推荐,因为它可能产生意外行为。
  • EditorBrowsableAttribute 仅在 Flexpression 项目不包含在当前解决方案中时才有效。

Flexpression API 中的每个类都是泛型的。除了 Flexpression 类(最顶层的对象)之外,使用泛型类型的原因是为了在当前对象的操作结束时强制执行正确的函数。

例如,一旦您结束了一个 If<Block<Flexpression<Action>>>,它将返回其父级 Block<Flexpression<Action>> 类型,然后该父级提供了对 Block 操作的访问。

在每个新级别,前一个父级都存储在子类型的泛型参数中。这可能会导致类名非常长并生成大量类型,但它确实为 API 带来了简洁性,并隐式强制了正确的语法。

幕后

Flexpressions 跟踪查询中使用的所有 ParameterExpression(即参数和变量)。然后,当提供表达式时,代码会根据名称将每个参数重新映射到现有的 ParameterExpression。通过分部分获取每个组件,Flexpressions 能够规避 .NET 强制执行的限制。

如 API 概述部分所述,表达式是不可变的。虽然 Flexpressions 中包含的大多数操作会立即生成一个表达式对象,但有些操作会延迟,因为所有组件尚未定义。例如,If 类会延迟 ConditionalExpression 的构造,直到 true(可能还有 false)情况的主体已填满。直到 Flexpression 对象转换为表达式树后,If 才会构造一个 ConditionalExpression

类概述

以下是 API 公开的公共类概述。

注意:此处列出的许多方法都有重载,但为简单起见,我仅提供通用概述,以免造成过多混淆。

Flexpression

Flexpression 类是生成表达式树的起点。
  • Parameters - 基于签名 S 的输入参数集合。
  • Compile() - 创建表达式树,进行编译,并返回类型为 Sdelegate
  • Create() - 创建 Flexpression 实例。
  • CreateLambda() - 创建表达式树并返回它。
  • GetLabelTargets() - 返回当前的标签集合。
  • GetVariablesInScope() - 在此级别,它等同于仅遍历 Parameters 属性。

Block 类是 Flexpression API 的主要工作负载。它负责 Flexpressions 中您将生成的大部分内容。

  • Variables - 在此 Block 定义的变量集合,可供此 Block 及其所有子项使用。
  • Act() - 将 ExpressionExpression<T> 插入 Block。如果发现任何限制,可以使用此方法来规避 API。
  • Break() - 执行 break 操作,仅在循环结构内有效。
  • Continue() - 执行 continue 操作,仅在循环结构内有效。
  • Declare<V>() - 声明一个类型为 V,名为指定名称的新变量。
  • Do() - 返回 do 循环的 Block,并在第一次循环迭代后检查提供的条件。
  • End() - 结束当前块并返回到父级。
  • Foreach<V, R>() - 返回 foreach 循环的 Block,其中包含类型为 V 的变量和类型为 R 的集合。
  • GetLabelTargets() - 返回当前的标签集合。
  • GetVariablesInScope() - 遍历此 Block、任何父级 Block 以及最终 Flexpression 实例中的所有变量。
  • Goto() - 跳转到提供的标签,该标签作为名称或 LabelTarget 实例提供。
  • If() - 返回一个带有提供条件的 If 块。
  • InsertLabel() - 在 Block 的当前位置插入一个新标签。
  • Return() - 在 Block 中插入一个 return 语句(带或不带值)。
  • Set<R>() - 使用提供的值设置类型为 R 的命名变量。如果变量尚未声明,将在设置值之前进行声明。
  • Switch<R>() - 返回一个值为类型 R 的 Switch。
  • Throw() - 在 Block 中插入一个 throw 语句,并提供异常。
  • Try() - 返回 Try 语句的 Block
  • Using<R>() - 返回 using 语句的 Block
  • While() - 返回 while 循环的 Block,并在第一次循环迭代之前检查提供的条件。

If

If 类封装了 C# 中的 if 语句。
  • Else() - 返回 if 语句的 false 分支的 Block
  • ElseIf() - 返回提供的条件的 true 分支的 Block
  • EndIf() - 结束当前的 if 语句。

Switch

Switch 类封装了一个值为类型 R 的 switch 构造。

  • Case() - 返回一个值为类型 RSwitchCase
  • Default() - 返回一个没有 case 值的默认 SwitchCase
  • EndSwitch() - 结束当前的 switch 语句。

SwitchCase

SwitchCase 类封装了一个值为类型 R 的 switch case。
  • Begin() - 返回 SwitchCaseBlock
  • Case() - 为当前的 SwitchCase 添加另一个 case 值。
  • Default() - 为当前的 SwitchCase 添加一个默认 case。
  • EndCase() - 结束当前的 SwitchCase

试试

Try 类封装了 C# 中的 try 语句。
  • Catch() - 返回 catch 语句的 Block,并带有可选的变量名和 Exception 类型。
  • EndTry() - 结束当前的 Try
  • Finally() - 返回 finally 语句的 Block

性能

为了更形象地说明,我创建了一个基准单元测试,用于比较使用 Flexpressions 和使用硬编码 Expression 树创建 LambdaExpression 的性能。Flexpression API 的性能在 3.5 到 4 倍于硬编码 Expression 树之间波动。诚然,这是 10,000 次迭代 1.5 秒的差异(每操作慢约 150 µs)。与开发便利性以及结果很可能以某种方式被缓存的事实相比,我认为这是完全可以接受的。

由于生成的泛型类型数量,内存是另一个可能受到影响的关键因素,但同样,由于一个典型方法不太可能产生超过 10 种类型,并且每种类型可能在其他 Flexpression 操作中可重用,因此这可能可以忽略不计。

未来发展

目前未计划实现的功能

  • For 循环 - For 循环需要三个不同的参数,其中两个有 16 种不同的变体。最终,它将产生总共 256 种不同的重载,这有点繁琐。我可以将其分解,但这会破坏语句的简洁性。因此,目前,除非有人有好的想法来解决这个问题,否则我没有计划实现它。

我请求使用该框架的人们——如果您有任何改进 API 的建议,请告诉我。我希望看到这个框架变得更有用和更灵活,所以如果您有建议,请传递过来。谢谢。

历史

  • 2012 年 9 月 9 日 - 1.0.1.0
    • 从接口中删除了 EditorBrowsableAttribute,以简化 API 的扩展。
    • 从项目文件中删除了“Debug - No Moles”生成配置(未使用)。
    • 在文章中添加了两个新示例。
  • 2012 年 9 月 8 日 - 1.0.0.0 - 初始发布
© . All rights reserved.