深入探索可扩展的AspectJ编译器abc
1. abc编译器的基础与驱动类
abc编译器为了实现功能扩展,需要借助特定的驱动类将新功能整合起来,以便基础编译器能够通过反射调用这些新功能。这里有两个关键的驱动类:
-
AbcExtension类
:当使用
-ext
标志调用abc编译器时,可以指定扩展的核心包名,随后通过反射加载该包中的
AbcExtension
类。abc编译器的所有可扩展钩子都通过这个类传递。
abc.main
包中提供了该类的默认实现,扩展必须继承这个默认实现。
-
ExtensionInfo类
:它是Polyglot可扩展机制的一部分,除词法分析器外的所有前端扩展都通过继承这个类进行注册。继承
AbcExtension
类的子类会返回这个类的新实例。
此外,一些扩展需要在AspectJ运行时得到支持,为了访问新类型连接点的反射信息,通常需要扩展运行时,这一般是实现新扩展的最后一步。
abc编译器的可扩展性来源多样,Polyglot提供了语法扩展性,abc在此基础上增加了可扩展的词法分析器。abc中抽象语法树(AST)节点的扩展方式基于Polyglot的原则,但具体的接口(如实现切入点的接口)是abc特有的。超过一半的abc编译过程是特定于AspectJ的,因此引入新方面特性的可扩展性在很大程度上取决于这些编译过程的设计。Polyglot中对现有AST类的少量重写证明了其Java编译器本身的可扩展性。abc的织入器各部分都是独特的,不过与ajc有很多共同结构。使用Jimple中间表示形式是abc织入器可扩展性的一个重要特性,因为它比Java源代码或字节码更容易分析和操作,扩展者可以更轻松地实现关键组件,如新型影子匹配器。
2. 扩展词法分析器
abc的词法分析器是有状态的,有四种主要的词法分析器状态用于处理AspectJ的不同子语言:JAVA、ASPECTJ、POINTCUT和POINTCUTIFEXPR。前三种状态分别用于Java代码、AspectJ代码和切入点表达式。POINTCUTIFEXPR状态与普通的JAVA状态分开,因为
if
切入点允许在
POINTCUT
内部嵌套Java表达式,当遇到匹配的右括号
)
时,需要返回到
POINTCUT
状态,而JAVA状态以右花括号
}
结束。
每种状态的关键字存储在特定状态的
HashMap
中,这些关键字映射到实现
LexerAction
接口的对象。该接口声明了一个方法
public int getToken(AbcLexer lexer)
,当识别到相应关键字时调用该方法,其返回值会转换为解析器令牌并传递给解析器进行进一步分析。
getToken(...)
方法会传入词法分析器实例的引用,因此可以对词法分析器产生副作用(如改变词法分析器状态)。接口提供了默认实现,除了90多个Java和AspectJ关键字中的5个,默认实现足以将关键字与解析器令牌关联,并可选择改变词法分析器状态。自定义的
LexerAction
实现可以提供更多灵活性。
实现eaj扩展需要添加几个新关键字,例如,在
POINTCUT
状态下引入了
cast
关键字,在所有四种词法分析器状态下引入了
global
关键字。
private
和
throw
已经是所有状态的关键字,因此不需要为私有切入点变量和抛出切入点扩展专门引入。以下是向相应状态添加关键字的代码:
public void initLexerKeywords(AbcLexer lexer)
{
// keyword for the “cast” pointcut extension
lexer.addPointcutKeyword("cast",
new LexerAction c(new Integer(abc.eaj.parse.sym.PC CAST)));
// keyword for the “global pointcut” extension
lexer.addGlobalKeyword("global",
new LexerAction c(new Integer(abc.eaj.parse.sym.GLOBAL),
new Integer(lexer.pointcut state())));
// Add the base keywords
super.initLexerKeywords(lexer);
}
这两个关键字都使用了
LexerAction
的默认实现,即
LexerAction c
类。可以看到该类的单参数和双参数构造函数,第一个参数始终是关键字应返回的解析器令牌,第二个参数(如果存在)是关键字之后应选择的词法分析器状态。如前文所述,可以通过继承
LexerAction c
类实现更多逻辑。
3. 扩展解析器
下面的语法片段展示了如何为私有切入点变量和
cast
切入点添加两个新的产生式,这些切入点可以出现在普通切入点可以出现的任何位置:
extend basic pointcut expr ::=
PRIVATE:x LPAREN formal parameter list opt:a RPAREN
LPAREN pointcut expr:b RPAREN:y
{:
RESULT =
parser.nf.PCLocalVars(parser.pos(x,y), a, b);
:}
|
PC CAST:x LPAREN type pattern expr:a RPAREN:y
{:
RESULT =
parser.nf.PCCast(parser.pos(x,y), a);
:}
;
这段代码与流行的CUP解析器生成器使用的代码非常相似,除了
extend
关键字,它表示这两个产生式将添加到非终结符号
basic pointcut expr
的现有规则中。
第一个新产生式用于私有切入点变量,从这个例子可以看出,终结符用大写表示。可以将解析每个语法符号的结果绑定到一个标识符,通过冒号和名称指定。例如,将识别
PRIVATE
令牌的结果绑定到
x
,将识别
pointcut expr
的结果绑定到
b
。这些命名结果可以在与产生式关联的解析器动作中使用,解析器动作由花括号和冒号分隔。这里使用产生式右侧第一个和最后一个符号的结果来计算整个私有切入点变量声明的位置(通过调用
parser.pos(x, y)
)。Polyglot中的位置信息始终是起始位置(源文件、行号、列号)和结束位置的组合,在abc中会非常小心地保留这些位置信息,以便即使在应用优化之后也能跟踪每段代码的来源。第二个语法产生式用于
cast
切入点,由于它比第一个产生式简单,这里不再进一步讨论。
除了扩展现有非终结符的可选产生式,Polyglot解析器生成器PPG还允许删除产生式、将产生式从一个非终结符转移到另一个非终结符,以及重写特定非终结符的产生式。
4. 添加新的AST节点
abc的前端基于Polyglot可扩展编译器框架构建,从Polyglot的角度来看,abc只是另一个扩展,这意味着abc“继承”了Polyglot提供的所有可扩展机制。在编写编译器扩展时,添加新的AST节点很常见,因此提供一种简单而健壮的机制非常重要。前面讨论的四个扩展都需要新的AST节点,为了简洁起见,这里仅介绍全局切入点扩展引入的节点,其他情况处理方式非常相似。
要编写一个干净的Polyglot扩展,需要严格使用工厂和接口来创建节点并调用其成员。首先,需要为新的AST节点定义一个接口,声明它要向外部展示的所有功能:
public interface GlobalPointcutDecl extends PointcutDecl
{
public void registerGlobalPointcut(GlobalPointcuts visitor,
Context context,
EAJNodeFactory nf);
}
这里提供了一个方法,用于将切入点插入到一个静态数据结构中,该结构跟踪程序中定义的全局切入点。注意,这个接口继承了abc的
PointcutDecl
接口,因此提供了与切入点声明相关的所有功能。
接下来,编写实现该接口的类,需要一些样板代码(构造函数和允许访问者访问节点的方法),当然,
registerGlobalPointcut()
方法需要给出具体实现。
为了确保能够实例化这种新的节点类型,需要继承abc的默认节点工厂(它又派生自Polyglot的节点工厂),并创建一个方法来获取
GlobalPointcutDecl
的实例:
public GlobalPointcutDecl
GlobalPointcutDecl (
Position pos,
ClassnamePatternExpr aspect pattern,
Pointcut pc, String name,
TypeNode voidn )
{
return new GlobalPointcutDecl c(pos, aspect pattern,
pc, name, voidn);
}
现在,扩展后的解析器在遇到适当的令牌时可以生成
GlobalPointcutDecl
对象。
所有更改都局限于新创建的类(实际上,这些类在一个完全独立的包中),abc本身无需进行任何更改,这使得扩展在abc升级时具有很强的健壮性。此外,由于新的AST节点扩展了现有节点,几乎不需要重新实现太多功能,相关接口只需要声明新节点特定功能的方法。
同样,为私有切入点变量和
cast
切入点扩展定义了
PCLocalVars
和
PCCast
接口以及实现类,并向扩展后的AspectJ节点工厂添加了相应的工厂方法。
5. 添加新的前端编译过程
实现“全局切入点”扩展需要两个新的编译过程:首先收集所有全局切入点,然后将每个切入点替换为原始切入点与所有适用全局切入点的合取。
Polyglot基于访问者的架构使得实现这一点非常容易,添加了两个新的编译过程。第一个过程将所有全局切入点存储在一个静态变量中,第二个过程将该切入点应用到相关代码。为了简洁起见,这两个过程由同一个类
GlobalAspects
实现,它使用一个成员变量
pass
来区分执行的是哪个功能。
AST的遍历由Polyglot的
ContextVisitor
类完成,新的编译过程扩展了
ContextVisitor
类,并在遇到相关AST节点时执行所需的操作。以下代码片段展示了新访问者进入AST节点时的行为:
public NodeVisitor enter(Node parent, Node n) {
if (pass == COLLECT
&& n instanceof GlobalPointcutDecl) {
((GlobalPointcutDecl) n).
registerGlobalPointcut(this, context(), nodeFactory);
}
return super.enter(parent, n);
}
如前所述,两个新的编译过程由同一个类实现,因此通过检查
pass == COLLECT
确保执行正确的操作。如果当前节点是
GlobalPointcutDecl
(在前面章节中定义的新AST节点之一),则调用其特殊方法,使其在存储全局切入点的数据结构中注册自己,然后将其余工作(实际的遍历)委托给超类。
leave()
方法在访问者离开AST节点时调用,并且可以根据需要重写节点,其实现与
enter()
方法非常相似。如果
pass == CONJOIN
并且处于适当的节点,则返回该节点与全局切入点的合取。
编译器执行的编译过程序列在特殊的单例
ExtensionInfo
类中指定,通过继承该类并在重写的方法中插入新的编译过程,然后调用原始方法,可以确保原始编译过程序列不受干扰。这种机制使得扩展在abc基础编译过程发生变化时具有很强的健壮性,可以添加和重新排列编译过程而不会破坏扩展。
6. 添加新的连接点
为了实现
cast
和
throw
切入点,首先需要扩展连接点类型列表。这通过向一个工厂对象列表中添加元素来完成,切入点匹配器会遍历这个列表以查找所有连接点影子。
listShadowTypes
方法在
AbcExtension
类中定义,并且在eaj中被重写:
protected List /*<ShadowType>*/ listShadowTypes()
{
List /*<ShadowType>*/ shadowTypes =
super.listShadowTypes();
shadowTypes.add(CastShadowMatch.shadowType());
shadowTypes.add(ThrowShadowMatch.shadowType());
return shadowTypes;
}
CastShadowMatch
和
ThrowShadowMatch
的定义非常相似,这里只讨论前者。
CastShadowMatch.shadowType()
方法返回一个匿名工厂对象,它将查找连接点的工作委托给
CastShadowMatch
类中的静态方法
matchesAt(...)
。该方法接受一个描述程序中位置的结构,并返回一个表示连接点影子的新对象或
null
。
public static CastShadowMatch
matchesAt(MethodPosition pos)
{
if (!(pos instanceof StmtMethodPosition))
return null;
Stmt stmt = ((StmtMethodPosition) pos).getStmt();
if (!(stmt instanceof AssignStmt))
return null;
Value rhs = ((AssignStmt) stmt).getRightOp();
if (!(rhs instanceof CastExpr))
return null;
Type cast to = ((CastExpr) rhs).getCastType();
return new CastShadowMatch(
pos.getContainer(), stmt, cast to);
}
MethodPosition
参数的作用是让abc遍历方法中所有可能出现连接点影子的部分,并询问每个工厂对象是否存在连接点影子。对于普通的AspectJ影子,有四种类型的
MethodPosition
:
- 整体体影子:执行、初始化、预初始化
- 单语句影子:方法调用、字段设置、字段获取
- 语句对影子:构造函数调用
- 异常处理影子:处理程序
大多数影子属于“整体体”或“单语句”类别,构造函数调用连接点和处理程序连接点比较特殊。在Java字节码中,构造函数调用不是单个指令,而是由两个独立的指令组成:
new
创建一个新实例,
invokespecial
初始化它,因此构造函数调用连接点包含这两个指令。处理程序连接点只能通过查看方法的异常处理表来找到,而不是通过其语句。如果新的连接点需要一种全新的方法位置类型,则可以重写遍历这些位置的代码。
matchesAt(...)
方法的首要任务是检查是否处于
cast
切入点的适当位置,即单语句位置。接下来,需要检查该位置是否实际发生了
cast
操作,由于Jimple的语法,
cast
操作只能发生在赋值语句的右侧。如果未找到这样的操作,则返回
null
;否则,构造一个适当的对象。
定义
CastShadowMatch
类还需要一些其他方法,这些方法与定义相关
args
切入点要绑定的正确值、报告运行时构造
JoinPoint.StaticPart
对象所需的信息,以及在织入器可以使用的适当位置记录切入点在该影子匹配的信息有关。由于篇幅原因,这里省略了这些细节。
7. 扩展切入点匹配器
这里描述
cast
切入点的实现,
throw
切入点的实现几乎相同,因此不再讨论。定义了相应的连接点影子后,编写适当的后端类就很简单了。切入点匹配器会在找到的每个连接点影子上尝试每个切入点,因此
cast
切入点只需检查当前影子是否为
CastShadowMatch
,如果是,则验证要转换的类型是否与
cast
切入点作为参数给出的
TypePattern
匹配:
protected Residue matchesAt(ShadowMatch sm)
{
if (!(sm instanceof CastShadowMatch))
return null;
Type cast to = ((CastShadowMatch) sm).getCastType();
if (!getPattern().matchesType(cast to))
return null;
return AlwaysMatch.v();
}
AlwaysMatch.v()
值是一个动态残差,表示切入点在该连接点无条件匹配。对于那些无法静态确定匹配的切入点,会用一个在影子处插入一些代码以在运行时检查条件的残差来代替。
8. 扩展运行时库
AspectJ通过
thisJoinPoint
和相关特殊变量提供当前连接点的动态和静态信息。对于
cast
切入点扩展,扩展了这个运行时接口以显示匹配
cast
的签名。例如,以下方面选择所有
cast
操作(除了通知体中的
cast
),并使用运行时反射显示每个连接点要转换的类型:
import org.aspectbench.eaj.lang.reflect.CastSignature;
aspect FindCasts
{
before():
cast(*) && !within(FindCasts)
{
CastSignature s = (CastSignature)
thisJoinPointStaticPart.getSignature();
System.out.println("Cast to: " +
s.getCastType().getName());
}
}
实现这一点需要在编译器的后端进行更改(在其中对静态连接点信息进行编码,以便运行时库稍后读取),并添加新的运行时类和接口。
静态连接点信息编码在一个字符串中,运行时由一个工厂类解析该字符串,以构造可以从
thisJoinPointStaticPart
访问的对象。这只在连接点影子所在类的静态初始化器中进行一次,直接生成代码来构造这些对象会导致生成的字节码体积过大,而使用字符串提供了一种紧凑的表示方式,且运行时开销不大。
cast
切入点的静态信息编码如下:为了方便重用现有的字符串解析器,会生成大量与
cast
连接点不相关的属性对应的虚拟信息。例如,
public
等修饰符对于与方法或字段签名相关的连接点很重要,但对于
cast
连接点没有意义。
cast
切入点的字符串由四部分组成:
- 修饰符(编码为整数,
cast
为0)
- 名称(通常是方法或字段名称,但对于
cast
只是“cast”)
- 声明类型 - 连接点所在的类
- 转换的类型
例如,在
IntHashTable
类的方法中,将从
HashMap
中检索的值转换为
Integer
的
cast
连接点将生成以下编码字符串:
"0-cast-IntHashTable-Integer"
综上所述,abc编译器通过一系列的扩展机制,包括驱动类的使用、词法分析器和解析器的扩展、AST节点的添加、前端编译过程的调整、连接点的扩展、切入点匹配器的增强以及运行时库的扩展,实现了强大的可扩展性。这些扩展机制使得开发者能够根据具体需求灵活地定制和扩展编译器的功能,以满足不同的应用场景。无论是添加新的语法特性、实现新的切入点类型,还是扩展运行时的信息展示,abc编译器都提供了丰富的手段和清晰的实现路径。通过合理运用这些扩展机制,开发者可以在不破坏原有编译器结构的基础上,高效地开发出符合特定需求的编译器扩展。
深入探索可扩展的AspectJ编译器abc
9. 总结与展望
abc编译器作为一个可扩展的AspectJ编译器,展现出了强大的灵活性和可定制性。通过对其各个组件的扩展,如词法分析器、解析器、AST节点、前端编译过程、连接点、切入点匹配器和运行时库,开发者能够根据不同的需求对编译器进行定制,实现新的功能和特性。
下面通过一个表格来总结abc编译器的主要扩展点及其作用:
|扩展点|作用|
| ---- | ---- |
|驱动类(AbcExtension、ExtensionInfo)|将新功能整合,便于基础编译器通过反射调用,注册前端扩展|
|词法分析器扩展|支持新的关键字,处理不同子语言的状态|
|解析器扩展|添加新的语法产生式,支持新的切入点和变量声明|
|AST节点添加|引入新的抽象语法树节点,扩展编译器的表达能力|
|前端编译过程添加|实现新的编译逻辑,如全局切入点的收集和应用|
|连接点扩展|支持新的连接点类型,增强切入点的匹配能力|
|切入点匹配器扩展|实现新的切入点匹配逻辑|
|运行时库扩展|提供新的运行时信息,支持新的切入点特性|
为了更清晰地展示abc编译器扩展的整体流程,下面是一个mermaid格式的流程图:
graph LR
A[开始] --> B[确定扩展需求]
B --> C[扩展驱动类]
C --> D[扩展词法分析器]
D --> E[扩展解析器]
E --> F[添加新的AST节点]
F --> G[添加新的前端编译过程]
G --> H[扩展连接点]
H --> I[扩展切入点匹配器]
I --> J[扩展运行时库]
J --> K[测试与验证]
K --> L[结束]
在未来的开发中,abc编译器的可扩展性可以进一步应用于更多领域。例如,在软件开发中,可以根据特定项目的需求,快速定制编译器以支持新的编程范式或语言特性。在学术研究方面,研究人员可以利用abc编译器的扩展机制,探索新的面向方面编程技术和理论。
同时,随着技术的不断发展,abc编译器也可以进一步优化其扩展机制。例如,提高扩展的性能和稳定性,减少扩展对原有编译器结构的依赖,以及提供更友好的扩展开发接口,降低开发者的学习成本和开发难度。
10. 实际应用案例分析
为了更好地理解abc编译器扩展的实际应用,下面通过一个具体的案例来进行分析。假设我们正在开发一个面向对象的软件开发项目,需要实现一种新的切入点类型,用于捕获对象属性的变更事件。
10.1 扩展词法分析器
首先,我们需要在词法分析器中添加新的关键字。假设我们引入了一个新的关键字
propertyChange
,用于表示属性变更切入点。代码如下:
public void initLexerKeywords(AbcLexer lexer)
{
lexer.addPointcutKeyword("propertyChange",
new LexerAction c(new Integer(abc.custom.parse.sym.PROPERTY_CHANGE)));
super.initLexerKeywords(lexer);
}
10.2 扩展解析器
接着,我们在解析器中添加新的语法产生式,以支持
propertyChange
切入点。
extend basic pointcut expr ::=
PROPERTY_CHANGE:x LPAREN type pattern expr:a RPAREN:y
{:
RESULT =
parser.nf.PCPropertyChange(parser.pos(x,y), a);
:}
;
10.3 添加新的AST节点
为了表示
propertyChange
切入点,我们需要添加新的AST节点。
public interface PCPropertyChange extends Pointcut
{
public void handlePropertyChange(Context context, NodeFactory nf);
}
public class PCPropertyChangeImpl implements PCPropertyChange
{
// 实现handlePropertyChange方法和其他必要的方法
}
public class CustomNodeFactory extends AbcDefaultNodeFactory
{
public PCPropertyChange PCPropertyChange(Position pos, TypePatternExpr pattern)
{
return new PCPropertyChangeImpl(pos, pattern);
}
}
10.4 扩展连接点
我们需要定义新的连接点类型,以捕获对象属性的变更事件。
protected List /*<ShadowType>*/ listShadowTypes()
{
List /*<ShadowType>*/ shadowTypes = super.listShadowTypes();
shadowTypes.add(PropertyChangeShadowMatch.shadowType());
return shadowTypes;
}
public static PropertyChangeShadowMatch
matchesAt(MethodPosition pos)
{
// 实现匹配逻辑,检查是否为属性变更操作
if (/* 检查条件 */) {
return new PropertyChangeShadowMatch(pos.getContainer(), /* 相关信息 */);
}
return null;
}
10.5 扩展切入点匹配器
编写切入点匹配器,检查当前连接点是否与
propertyChange
切入点匹配。
protected Residue matchesAt(ShadowMatch sm)
{
if (!(sm instanceof PropertyChangeShadowMatch))
return null;
// 检查属性类型等条件
if (/* 检查条件 */) {
return AlwaysMatch.v();
}
return null;
}
10.6 扩展运行时库
在运行时库中添加新的信息,以便在属性变更时提供相关的上下文信息。
import org.aspectbench.custom.lang.reflect.PropertyChangeSignature;
aspect PropertyChangeAspect
{
before():
propertyChange(*)
{
PropertyChangeSignature s = (PropertyChangeSignature)
thisJoinPointStaticPart.getSignature();
System.out.println("Property changed: " + s.getPropertyName());
}
}
通过以上步骤,我们成功地扩展了abc编译器,实现了一个新的切入点类型,用于捕获对象属性的变更事件。这个案例展示了abc编译器扩展机制的灵活性和实用性,开发者可以根据具体需求,通过一系列的扩展步骤,实现各种新的功能和特性。
11. 开发者指南与最佳实践
对于想要使用abc编译器进行扩展开发的开发者,以下是一些实用的指南和最佳实践:
11.1 遵循模块化原则
在进行扩展开发时,尽量将新功能封装在独立的模块中。例如,将新的AST节点、前端编译过程等放在单独的包中,这样可以提高代码的可维护性和可扩展性。当abc编译器进行升级时,独立的模块可以更容易地适应变化,减少对其他部分的影响。
11.2 充分利用现有机制
abc编译器已经提供了丰富的扩展机制,开发者应该充分利用这些机制,避免重复造轮子。例如,在扩展词法分析器和解析器时,可以使用现有的接口和类,通过继承和重写方法来实现新的功能。
11.3 注意位置信息的保留
在扩展过程中,要特别注意保留代码的位置信息。位置信息在调试和错误定位时非常重要,abc编译器在很多地方都依赖位置信息来跟踪代码的来源。在编写解析器动作和处理AST节点时,要确保正确计算和传递位置信息。
11.4 进行充分的测试
扩展开发完成后,要进行充分的测试。测试可以帮助发现潜在的问题,确保扩展的功能正常工作。可以编写单元测试来测试新的AST节点、切入点匹配器等,同时进行集成测试来验证整个扩展在abc编译器中的运行情况。
11.5 参考文档和示例代码
abc编译器可能提供了相关的文档和示例代码,开发者应该仔细阅读这些文档,参考示例代码来进行开发。文档可以帮助开发者了解扩展机制的细节和使用方法,示例代码可以作为开发的参考模板。
12. 与其他编译器的比较
为了更好地理解abc编译器的优势和特点,下面将abc编译器与其他常见的AspectJ编译器进行比较。
| 编译器 | 可扩展性 | 灵活性 | 性能 | 社区支持 |
|---|---|---|---|---|
| abc编译器 | 高,提供多种扩展机制,如驱动类、AST节点扩展等 | 强,可根据需求定制新的语法和功能 | 较好,使用Jimple中间表示形式便于分析和操作 | 适中,有一定的开发者社区 |
| ajc | 传统的AspectJ编译器,扩展性相对较弱 | 一般,扩展功能有限 | 较高,经过优化的编译过程 | 广泛,有大量的文档和社区资源 |
| 其他自定义编译器 | 扩展性取决于具体实现,可能需要大量的底层开发 | 差异较大,有些可能非常灵活,有些则受限 | 差异较大,取决于实现的质量 | 因编译器而异,可能缺乏社区支持 |
从比较中可以看出,abc编译器在可扩展性和灵活性方面具有明显的优势,适合需要进行定制开发的项目。虽然在社区支持方面可能不如ajc,但随着abc编译器的不断发展,其社区也在逐渐壮大。
综上所述,abc编译器通过丰富的扩展机制为开发者提供了强大的定制能力。无论是在学术研究还是实际项目开发中,都可以利用这些扩展机制实现新的功能和特性。开发者在使用abc编译器进行扩展开发时,遵循一定的指南和最佳实践,可以更高效地完成开发任务。同时,与其他编译器的比较也凸显了abc编译器在可扩展性和灵活性方面的独特优势。未来,abc编译器有望在面向方面编程领域发挥更大的作用。
超级会员免费看
42

被折叠的 条评论
为什么被折叠?



