创建 CodeDOM:对代码语义进行建模 (第 2 部分)





5.00/5 (17投票s)
为 C# 创建 CodeDOM
介绍
本文旨在介绍如何创建代码模型(CodeDOM),该模型用于模拟编程语言(特别是 C#)的语义。本文提供了 C# CodeDOM 的源代码,以及创建和编辑 CodeDOM 树的示例。这是 CodeDOM 系列文章的第 2 部分。请参阅第 1 部分,其中讨论了(在我看来)什么是“CodeDOM”,包括构建 CodeDOM 的设计目标。
选择语言
CodeDOM 可以为一门全新的语言创建,也可以模拟一门现有语言。为了获得更广泛的采用,我希望创建一个 CodeDOM,使其能够与现有的主流基于文本的语言互操作。我还希望使用托管平台。这很快将我的选择范围缩小到 Java 或 C#。我个人更喜欢 C# 而不是 Java,因为它具有更丰富的功能集,所以我选择为 C# 创建 CodeDOM,即使它是一门非常复杂且发展迅速的语言。
为什么不支持多种语言?
支持多种语言的 CodeDOM(如 .NET)可能看起来是个好主意——大多数语言都有共同的特性,这将允许将相同的 CodeDOM 对象映射到不同的文本语言。然而,我认为更重要的是类名和功能与所建模的语言相匹配(参见我第一篇文章中的设计目标 A)。我定义的 CodeDOM 应该是该语言,因此不应为了支持多种语言而对其进行“破坏”。因此,我将紧密地建模 C#,将来对其他语言的任何支持都将涉及创建新的 CodeDOM 类集。始终可以创建转换器来在不同语言的 CodeDOM 之间进行转换。
System.CodeDOM、表达式树和 Roslyn 如何?
乍一看,System.CodeDOM
命名空间中的类可能看起来正是我所谈论的那种 CodeDOM,并且它们自 .NET 1.1 以来就已经存在。不幸的是,它们的设计初衷与我所寻找的完全不同:从一组对象生成多种语言的文本代码。代码生成向导可以使用它们手动构建代码对象树,然后输出等效的 C# 或 VB 代码。这些类违反了我的主要设计目标 (A),不具备所需的功能,最重要的是它们缺少对许多 C# 语言特性的支持。
表达式树随 .NET 3.5(在 `System.Linq.Expressions` 中)一起出现。这些类作为对 LINQ 支持的一部分添加,它们模拟 C# 表达式。它们只能用于表示单表达式 lambda,并且不支持语句。因此,有点讽刺的是,它们提供了一些 `System.CodeDOM` 中缺少的功能,但它们本身缺少对除表达式之外的大多数语言功能的支持。它们在 .NET 4.0 中得到了扩展,以提供对一些语句(如 try/catch 块和 switch)的支持,但这种支持是用于动态语言的——语句支持尚不适用于 lambda 表达式。同样,这是为其他目的设计的,对于我们的目的来说,缺少的东西太多了,无法使用。
Roslyn 项目目前尚未发布最终版本(在我撰写本文时),但已提供预览版,它确实旨在为所有 C# 语言功能提供类。Roslyn 当然更像是我一直在谈论的通用 CodeDOM,但 Roslyn 的设计目标似乎与我的 CodeDOM 设计目标大相径庭——例如,它专注于典型的语法(AST)模型。无论如何,我的代码实际上已经开发了多年,我认为它比 Roslyn 具有一些显著的优势。所以,我将首先(在一系列文章中)介绍我的 CodeDOM 类和所有支持代码,然后在以后的文章中,我将研究 Roslyn 并将其进行比较。
一般 CodeDOM 观察
在对表示代码的类进行建模时,会出现许多有趣的问题。
- 很快就会发现,许多文本语法主要用于解析器,与对象树无关——例如分号终止符、逗号分隔符、大括号、许多括号等。在代码对象树中没有必要表示这些东西。
- 期望许多注释能够附加到它们所记录的特定代码对象上,例如文档化后续代码行或代码块的注释,或者在行尾注释的情况下文档化先行代码行的注释。
- 文档注释最好用对象而不是 XML 来表示,因为这样更容易以编程方式处理它们。
- 大多数格式,例如制表符和其他空白,与代码对象树的显示无关,因为它们可以使用可配置设置生成。但是,即使对于对象,换行符仍然是相关的,以便它们可以在不进行太多水平滚动的情况下显示。
- 行号和列号对于代码对象树的显示并不真正相关,但它们对于等效的已解析和/或生成的文本或引用它的消息仍然有用。
- 基于文本的语言的特性实际上可能受到文本语法本身的限制。使用对象时一切皆有可能,但创建合理且易于解析的文本可能会很棘手。
C# CodeDOM 介绍
本文附带的源代码是 C# 2.0 的完整 CodeDOM,减去了“不安全”功能。我实际上已经完全实现了 C# 5.0 的所有功能,但我现在有意限制了功能集,以使其更小、更容易理解(也因为我还没有准备好开源我的所有辛勤工作)。即使没有这些较新的功能,仍然有大量的代码需要涵盖(超过 250 个类),它们的省略并不会真正影响 CodeDOM 系列文章以及创建和使用它们所涉及的许多问题。
由于需要讨论的类很多,我们将它们按逻辑组进行介绍。我在表格中列出了类名,派生类通过缩进表示,相关类组使用单独的表格。此外,我省略了大多数具体类的描述,其功能通过类名就相当明显,主要展示基类的描述。此外,我不会详细讨论类或其成员——这只是一个高层次的概述。本文附带的源代码包含文档注释,以供进一步参考。
CodeDOM 的主要类(以及一个枚举)如表 1 所示。所有 CodeDOM 对象的公共基类是 CodeObject
。它包含一个 Parent 引用、一个可选的 Annotations 集合(注释、属性等),以及一个标志枚举 FormatFlags
,主要用于次要格式(如换行符)。存在许多有用的标志属性,例如 IsFirstOnLine
(如果对象在新行上开始,则为 true)、NewLines
(对象之前的换行符总数)和 IsSingleLine
(如果对象内部没有任何换行符,则为 true)。还有许多有用的属性用于处理注释,以确定对象上是否存在某些类型的注释(HasComments
、HasAttributes
等),或者轻松获取或设置某些注释(DocComment
、EOLComment
等)。包含的测试和示例将帮助演示如何使用各种可用的属性和方法。
表 1 - 主要类型
CodeObject | 所有代码对象的公共基类。 |
声明
|
所有语句的公共基类。 |
BlockStatement
|
所有可以包含代码体的语句的公共基类。 |
表达式
|
所有表达式的公共基类。 |
运算符
|
所有操作(二元、一元或其他)的公共基类。 |
SymbolicRef
|
所有符号引用的公共基类。 |
注释
|
所有代码注释的公共基类。 |
Attribute
|
表示与代码对象关联的元数据。 |
CommentBase
|
所有注释的公共基类。 |
Comment 、DocComment |
|
CompilerDirective
|
所有编译器指令的公共基类。 |
Message
|
表示与代码对象关联的生成消息。 |
块
|
表示 BlockStatement 的主体(包含 0 个或更多代码对象)。 |
命名空间
|
表示类型声明和可选子命名空间的命名空间。 |
RootNamespace
|
表示顶级(全局)命名空间。 |
NamespaceTypeDictionary
|
表示命名空间和类型的字典。 |
NamespaceTypeGroup
|
表示具有相同名称的类型和/或命名空间组。 |
NamedCodeObjectDictionary
|
表示命名代码对象的字典。 |
NamedCodeObjectGroup
|
表示具有相同名称的命名代码对象组。 |
ChildList<T>
|
表示特定类型的子代码对象的集合。 |
修饰符
|
适用于许多声明语句的修饰符枚举。 |
Public , Protected , Internal , Private , Static , New , Abstract , Sealed , Virtual ,Override , Extern , Partial , Implicit , Explicit , Const , ReadOnly , Volatile , Event |
所有 C# 语句都有一个 Statement 基类,所有可以有子代码块的语句都有一个 BlockStatement
。C# 表达式都派生自 Expression
,并分为 Operator
和 SymbolicRef
两个子类别。所有众多的语句和表达式子类将在表 2 到表 6 中列出和讨论。Attribute
类处理 C# 属性,CompilerDirective
是所有编译器指令的基类(参见表 8),这两者都是代码上的 Annotation
,以及常规 Comment
、文档注释(表 7)和 Message
(与代码相关的错误、警告或信息消息)。
Block
类用于 BlockStatement
的主体。Namespace
类表示一个包含类型和子命名空间(内部使用 NamespaceTypeDictionary
和 NamespaceTypeGroup
)的命名空间,RootNamespace
是一个表示全局命名空间的子类。NamedCodeObjectDictionary
和 NamedCodeObjectGroup
类型由 Block
用于索引所有具有名称的子对象——这些与 Namespace
使用的类非常相似,但它们处理的是命名 CodeObject
而不是类型和命名空间,并且这些差异足以证明单独的类是合理的。泛型 ChildList<T>
类是 CodeObject
或任何派生类型的集合,并在需要子对象集合的任何地方使用(Block
类、Annotation
集合等)。此集合确保添加到其中的对象的 Parent 属性引用集合的所有者(而不是集合本身)。Modifiers
枚举由类型和成员声明使用,以指定其访问级别和其他特殊属性。
所有 Statement
类都在表 2 中显示。其中许多也是 BlockStatement
,可以有代码主体。大多数直接映射到 C# 语句,并且不言自明。C# 源文件由 CodeUnit
表示(通常是根对象,除非处理隔离的代码片段),它派生自 NamespaceDecl
,因为它们共享许多功能——它表现为具有隐式“namespace global”的根级 NamespaceDecl
(例如,两者都可以有 'using' 指令)。选择名称 CodeUnit
是因为它可以是内存中的,而不是映射到文件,“编译单元”对编译来说太具体且太长。虽然我通常避免在名称中使用缩写,但你会注意到在许多类名中使用了“Decl”,作为“Declaration”的缩写。不要将用于导入命名空间的 UsingDirective
与使用相同关键字的 Using
语句混淆。
表 2 - 语句类
ExternAlias
|
与编译器选项一起使用以创建额外的根级命名空间。 | |
UsingDirective
|
将命名空间的内容导入当前作用域。 | |
别名
|
表示命名空间或类型别名的声明(“using name = …”)。 | |
BlockStatement
|
所有可以包含代码体的语句的公共基类。 | |
NamespaceDecl
|
声明一个命名空间以及属于它的声明体。 | |
CodeUnit | 声明属于根级命名空间的独立代码单元。通常是源文件的内容,但也可以是仅内存中的。 | |
TypeDecl
|
所有类型声明的公共基类。 | |
BaseListTypeDecl
|
所有带有可选基类型列表的类型的公共基类。 | |
ClassDecl 、EnumDecl 、InterfaceDecl 、StructDecl |
||
DelegateDecl
|
||
MethodDeclBase
|
所有方法声明语句的公共基类。 | |
MethodDecl
|
表示具有唯一名称和返回类型的方法。 | |
GenericMethodDecl
|
表示带有类型参数的泛型方法。 | |
AccessorDecl
|
所有访问器的公共基类。 | |
AccessorDeclWithValue
|
所有带值参数的访问器的公共基类。 | |
SetterDecl
|
表示用于写入属性值的方法。 | |
AdderDecl
|
表示用于向事件添加委托的方法。 | |
RemoverDecl
|
表示用于从事件中移除委托的方法。 | |
GetterDecl
|
表示用于读取属性值的方法。 | |
OperatorDecl
|
所有用户定义运算符的公共基类。 | |
ConversionOperatorDecl
|
||
ConstructorDecl 、DestructorDecl |
||
PropertyDeclBase
|
所有类似属性的声明语句的公共基类。 | |
PropertyDecl 、IndexerDecl 、EventDecl |
||
IfBase
|
If 和 ElseIf 语句的公共基类。 | |
If 、ElseIf |
||
SwitchItem
|
Case 和 Default 语句(在 Switch 中)的公共基类。 |
|
Case 、Default |
||
BlockDecl
|
表示限制在局部作用域(被大括号包围)内的代码块。 | |
Else 、For 、ForEach 、While 、Try 、Catch 、Finally 、Using 、Lock 、CheckedBlock 、UncheckedBlock |
||
Break 、Continue 、Goto 、Label 、Return 、Throw |
||
VariableDecl
|
所有变量声明语句的公共基类。 | |
FieldDecl 、LocalDecl 、ParameterDecl 、EnumMemberDecl |
||
MultiFieldDecl 、MultiLocalDecl 、ValueParameterDecl 、MultiEnumMemberDecl |
||
YieldStatement
|
YieldBreak 和 YieldReturn 语句的公共基类。 |
|
YieldBreak 、YieldReturn |
请注意,有单独的类可以在单个语句中声明多个字段或局部变量。它们派生自支持单个声明的类——例如,MultiFieldDecl
派生自 FieldDecl
。这样做是为了使单个声明这种最常见的情况保持简单和轻量,同时以尽可能少的代码需要特别了解多重声明的方式支持多重声明。多重声明实际上是单个声明的集合,其中修饰符和类型被强制为相同。EnumDecl
总是使用 MultiEnumMemberDecl
来包含其所有 EnumMemberDecl
条目。ValueParameterDecl
类用于属性 setter(以及事件 adder/remover)的隐式“值”参数。
与泛型(类型或方法)相关的类如表 3 所示——它们对类型参数、约束子句和单个约束类型进行建模。
表 3 - 泛型支持类
TypeParameter
|
表示泛型类型或方法声明的类型参数。 |
ConstraintClause
|
表示对类型参数的一个或多个约束。 |
TypeParameterConstraint
|
所有类型参数约束的公共基类。 |
ClassConstraint 、StructConstraint 、NewConstraint 、TypeConstraint |
Expression
的最大子类别是 Operator
,如表 4 所示。运算符通常是自解释的。它们主要分为二元、一元和带参数的(可变数量或单个参数)。Conditional
(a ? b : c) 运算符是一个特例。
表 4 - 运算符类
BinaryOperator
|
所有二元运算符的公共基类。 |
赋值
|
将右侧表达式赋值给左侧。也是所有复合赋值运算符的公共基类。 |
AddAssign (+=)、BitwiseAndAssign (&=)、BitwiseOrAssign (|=)、BitwiseXorAssign (^=)、DivideAssign (/=)、LeftShiftAssign (<<=)、ModAssign (%=)、MultiplyAssign (*=)、RightShiftAssign (>>=)、SubtractAssign (-=) |
|
BinaryArithmeticOperator
|
所有二元算术运算符的公共基类。 |
Add (+)、Divide (/)、Mod (%)、Multiply (*)、Subtract (-) |
|
BinaryBitwiseOperator
|
所有二元位运算符的公共基类。 |
BitwiseAnd (&)、BitwiseOr (|)、BitwiseXor (^) |
|
BinaryBooleanOperator
|
所有结果为布尔值的二元运算符的公共基类。 |
And (&&)、Or (||)、Is |
|
RelationalOperator
|
所有关系运算符的公共基类。 |
Equal (==)、GreaterThan (>)、GreaterThanEqual (>=)、LessThan (<)、LessThanEqual (<=)、NotEqual (!=) |
|
BinaryShiftOperator
|
所有移位运算符的公共基类。 |
LeftShift (<<)、RightShift (>>) |
|
As 、Dot ( . )、IfNullThen ( ?? )、Lookup ( :: ) |
|
UnaryOperator
|
所有一元运算符的公共基类。 |
PreUnaryOperator
|
所有前缀一元运算符的公共基类。 |
Cast 、Complement (~)、Decrement (--)、Increment (++)、Negative (-)、Not (!)、Positive (+) |
|
PostUnaryOperator
|
所有后缀一元运算符的公共基类。 |
PostIncrement (++)、PostDecrement (--) |
|
ArgumentsOperator
|
所有带可变参数的运算符的公共基类。 |
NewOperator
|
NewArray 和 NewObject 运算符的公共基类。 |
NewArray 、NewObject |
|
呼叫
|
表示对方法或委托的调用,包括任何参数。 |
ConstructorInitializer
|
BaseInitializer 和 ThisInitializer 的公共基类。 |
BaseInitializer 、ThisInitializer |
|
目录 ([ ])
|
|
SingleArgumentOperator
|
所有带单个参数的运算符的公共基类。 |
CheckedOperator
|
Checked 和 Unchecked 运算符的公共基类。 |
Checked 、Unchecked |
|
RefOutOperator
|
Ref 和 Out 运算符的公共基类。 |
Ref 、Out |
|
TypeOperator
|
TypeOf 、SizeOf 、DefaultValue 运算符的公共基类。 |
TypeOf 、SizeOf 、DefaultValue |
|
Conditional ( ? : )
|
表示条件 if/then/else (“a ? b : c”) 表达式。 |
Expression
的另一个重要子类别是 SymbolicRef
,如表 5 所示——它们表示“符号引用”,即对其他地方定义的命名代码对象的引用。请注意,这些类名中使用了“Ref
”,作为“Reference”的缩写。我选择将对对象的符号引用表示为直接对象引用。未解析的符号引用由 UnresolvedRef
表示,它通过名称引用。当此类引用解析为特定对象时(解析将在以后的文章中讨论),UnresolvedRef
对象将被替换为适当类型的另一个对象,例如 TypeRef
或 MethodRef
,它使用对象引用。使用直接引用使事情更快更容易,并使对象更小。重命名任何对象不需要任何“修复”——所有引用将自动从目标对象获取新名称。
C# 中的大多数表达式将评估为一种类型,通常由 TypeRefBase
表示,因为虽然它通常是 TypeRef
,但它也可以是 UnresolvedRef
或 MethodRef
。请注意,TypeRef
用于引用任何 C# 类型(无论是类、结构、接口、枚举还是委托)——到目前为止还没有必要为它们创建派生引用类型,并且它们可以通过 IsEnum
等属性轻松区分。
表 5 - SymbolicRef 类
TypeRefBase
|
TypeRef 、MethodRef 和 UnresolvedRef 的公共基类。 |
MethodRef
|
表示对方法声明的引用。 |
AnonymousMethodRef 、ConstructorRef 、OperatorRef |
|
TypeRef
|
表示对类型声明的引用。 |
AliasRef 、TypeParameterRef |
|
UnresolvedRef
|
表示尚未解析为直接引用的符号引用。 |
GotoTargetRef
|
LabelRef 和 SwitchItemRef 的公共基类。 |
LabelRef 、SwitchItemRef |
|
SelfRef
|
ThisRef 和 BaseRef 的公共基类。 |
ThisRef 、BaseRef |
|
VariableRef
|
所有变量引用的公共基类。 |
PropertyRef
|
表示对属性声明的引用。 |
IndexerRef
|
|
EventRef 、EnumMemberRef 、FieldRef 、LocalRef 、ParameterRef |
|
ExternAliasRef 、NamespaceRef 、DirectiveSymbolRef |
还有一些表达式不是运算符或符号引用。它们显示在表 6 中,其中 Literal
是迄今为止使用最广泛的。为了保留字面值的确切文本,例如字符串或字符中的转义序列,或数字的确切格式(例如十六进制和/或后缀字符),字面值作为字符串存储在 Literal
对象中。提供了一个 GetValue()
方法,返回适当类型的实际常量值。
表 6 - 其他表达式类
AnonymousMethod |
表示可以分配给委托的未命名方法。 |
Initializer |
表示数组的初始化。 |
字面量 |
表示特定类型(字符串、整数、布尔值等)的字面值。 |
文档注释类列在表 7 中。所有标准 XML 标签都提供了类,因此它们可以被视为对象,无需担心处理 XML(我更倾向于将其视为实现细节)。非标准标签可以使用 DocTag
表示。DocComment
类既是公共基类,也是一个具体类,可以用于表示一些文本或子对象集合。DocC
类可以包含一个代码 Expression
,DocCode
类可以包含一个代码 Block
——因此文档注释中的代码示例可以像“真实”代码一样是实际的代码对象。通过 DocCodeRefBase
和 DocNameBase
类对代码的引用使用 SymbolicRef
直接引用代码。
表 7 – 文档注释
DocComment
|
表示代码的用户文档,也是所有文档注释的公共基类。 |
DocCodeRefBase
|
所有带有“cref”属性的文档注释标签的公共基类。 |
DocException 、DocPermission 、DocSee 、DocSeeAlso |
|
DocNameBase
|
所有带有“name”属性的文档注释标签的公共基类。 |
DocParam 、DocParamRef 、DocTypeParam 、DocTypeParamRef |
|
DocB 、DocC 、DocCDATA 、DocCode 、DocExample 、DocI 、DocInclude 、DocList 、DocListDescription 、DocListHeader 、DocListItem 、DocListTerm 、DocPara 、DocRemarks 、DocReturns 、DocSummary 、DocTag 、DocText 、DocValue |
编译器指令如表 8 所示。它们在将代码建模为对象时带来了一个问题,因为它们实际上在预处理阶段,在将其解析为对象之前,作用于语言的文本形式。条件指令可以将原本逻辑上是单个代码对象的内容“拆分”成多个部分。编译器指令的处理方式是将其视为可以附加到任何其他代码对象(或者它们也可以像注释一样在 Block
级别独立存在)的注释。特别是条件指令将在解析时进行评估(我们将在以后的文章中添加该功能),这可能会影响代码对象的创建方式。更改定义的符号最简单的处理方式是重新解析整个 CodeUnit
,尽管理论上也可以通过仅重新解析受影响的代码片段来完成——无论如何,这最终应该在编辑时得到支持。
表 8 – 编译器指令类
ConditionalDirectiveBase
|
所有条件指令的公共基类。 |
ConditionalDirective
|
“开放”条件指令的公共基类。 |
ElseDirective
|
|
ConditionalExpressionDirective
|
带表达式的条件指令的公共基类。 |
IfDirective 、ElIfDirective |
|
EndIfDirective
|
|
MessageDirective
|
所有带消息的指令的公共基类。 |
RegionDirective 、EndRegionDirective 、ErrorDirective 、WarningDirective |
|
PragmaDirective
|
所有 pragma 指令的公共基类。 |
PragmaChecksumDirective 、PragmaWarningDirective |
|
SymbolDirective
|
所有符号指令的公共基类。 |
DefineSymbol 、UnDefSymbol |
|
LineDirective
|
最后一组类型是表 9 中所示的接口。它们用于存在跨类的共同行为但没有逻辑上的共同基类来放置它的地方。例如,所有具有 Name
属性的代码对象都实现 INamedCodeObject
接口,这样只关心名称的代码(例如基于名称的字典)就可以处理所有实现该接口的对象。
表 9 – 接口
IBlock
|
由所有具有 Block 主体的代码对象实现。(BlockStatement 、AnonymousMethod 和 DocCode ) |
IModifiers
|
由所有具有 Modifiers 的代码对象实现。(TypeDecl 、MethodDeclBase 、PropertyDeclBase 、FieldDecl ) |
IMultiVariableDecl
|
由支持语句中多个声明的所有 VariableDecls 实现。 |
INamespace
|
由所有表示命名空间的代码对象实现(Namespace 和 Alias )。 |
INamedCodeObject
|
由所有具有 Name 属性的代码对象实现。 |
IParameters
|
由所有具有参数的代码对象实现。(MethodDeclBase 、IndexerDecl 、DelegateDecl 和 AnonymousMethod ) |
ITypeDecl
|
由所有表示类型声明的代码对象实现。(TypeDecl 、TypeParameter 和 Alias ) |
ITypeParameters
|
由所有具有 TypeParameter 的代码对象实现。(TypeDecl 和 GenericMethodDecl ) |
IVariableDecl
|
由所有表示变量声明的代码对象实现。(VariableDecl 和 PropertyDeclDecl ) |
所有 CodeDOM 类都支持手动创建实例和分配子对象。可以手动构建一个对象树来表示任何 C# 源代码。当然,将现有代码解析为 CodeDOM 对象也是有意义的——这将在以后的文章中介绍。还支持编辑操作。命名对象可以通过简单地设置其 Name
属性来更改名称。子对象可以通过将属性设置为新创建或克隆的对象来更改。现有对象可以通过简单地分配它们来移动,这会自动更改它们的 Parent 属性以引用它们的新父对象。
CodeDOM 也可以轻松地渲染为 C# 文本。由于此功能非常关键且经常使用(例如在调试时),文本渲染直接集成到 CodeDOM 类中,使用以“AsText…”开头的方法。代码使用每个对象上的 FormatFlags
进行格式化,这些标志默认情况下按照典型的 C# 格式进行设置,但可以根据需要覆盖。这些标志决定了换行符的出现位置,表达式上是否存在括号,代码块上是否存在大括号,甚至分号是否终止语句或表达式。
支持类
CodeWriter
类(在 Rendering 文件夹中)用于以文本格式渲染 CodeDOM 类。使用任何类的 AsText()
方法将其渲染为文本时,都需要此类的实例,您可以在 CodeUnit.SaveAs()
或某些特殊的重载 CodeObject.AsText()
方法中看到示例,这些方法可以将任何 CodeObject
及其所有子对象渲染为文本。
有一些实用类(在 Utilities 文件夹中),它们提供用于处理数组、集合、文件、字符串以及各种 Reflection 类型的静态帮助方法。这些方法没有作为扩展方法实现,因为我们正在建立这个代码库以使其能够解析自身,并且目前不支持扩展方法。您可能会发现其中一些对您自己的应用程序有用,并且它们可以很容易地转换为扩展方法。
Configuration
类用于读取应用程序“.config”文件中的条目。它非常小(主要的 LoadSettings 方法只有 60 行代码),但它是一个非常方便的类,欢迎您“借用”并修改以用于自己的应用程序。它通过读取配置文件特定部分中的条目来工作,并使用反射来查找和设置类的静态成员的值。
Log
类用于记录消息——可以记录到控制台,也可以记录到选择拦截它们的任何客户端代码。它支持不同级别的日志记录,并对异常进行特殊处理。它也是一个简单方便的类,可以被其他应用程序重用。
使用附加源代码
本项目选择的“代号”是 Nova,因此包含的源代码中的解决方案名为“Nova.sln
”。前面章节中描述的所有 CodeDOM 类和支持类都位于“Nova.CodeDOM.csproj
”项目中。如果您希望用自己的客户端代码进行实验,只需引用生成的 Nova.CodeDOM.dll
(或将项目添加到您自己的解决方案中)。
解决方案中还包含另外两个项目。Nova.Examples
项目包含一些使用 CodeDOM 的简单示例,随着我们添加更多功能,将会有更多示例。这是一个控制台应用程序,您可以运行它来查看结果。Nova.Test
项目包含一个文件“FullTest.cs
”,它实现了 CodeDOM 旨在支持的大多数 C# 功能,“ManualTests.cs
”包含手动构建等效 CodeDOM 对象树的代码。这是一个控制台应用程序,运行它将构建对象树,将其发出到“FullTest.generated.cs
”文件,然后将结果与“FullTest.cs
”进行比较(ManualTests.cs
中还有几个非常小的测试将首先运行)。
检查 ManualTests.cs
中的代码,了解如何使用为各种语言功能提供的所有类手动创建 CodeDOM。定义了许多隐式转换运算符以使事情变得更容易。您可以简单地将“0
”传递给期望 Expression
的方法参数,它将等同于“new Literal(0)
”,或者传递一个类型,例如“typeof(int)
”(或另一个反射对象,如 MethodInfo
),它将等同于“new TypeRef(typeof(int))
”(或另一个适当的引用)。
摘要
我们现在有了一套完整的 CodeDOM 类,可以手动实例化以创建表示几乎任何 C# 程序的树形对象,我们还可以将这些对象的任何树形结构渲染为 C# 源代码。但是,我们仍然缺少一些显而易见的东西——如何解析现有程序呢?而且,我们是否需要 Solution 和 Project 对象才能处理“.sln”和“.csproj”文件?这两者都在路上,但首先我认为我们需要一种比仅仅将其转换为文本更好的 CodeDOM 显示方式。
在我的下一篇文章中,我将介绍使用 WPF 渲染 CodeDOM 的代码,使其更易于检查和理解。现在,事情将开始变得更有趣……或者,至少更具吸引力。点击此处查看第 3 部分。