在AST操作中,使用Rc包裹下行特征

18

我正在尝试在Rust中操作抽象语法树(AST)。将会有许多操作,并且我想让我的树是不变的,为了节省时间,所有引用都将是Rc

我的树节点将如下所示:

enum Condition {
    Equals(Rc<Expression>, Rc<Expression>),
    LessThan(Rc<Expression>, Rc<Expression>),
    ...
}

enum Expression {
    Plus(Rc<Expression>, Rc<Expression>),
    ...
}

我想要用一个相同类型的节点替换给定类型的随机节点。为了在树上进行通用操作,我已经创建了一个 trait:

trait AstNode {
    fn children(&self) -> Vec<Rc<AstNode>>;
}

所有节点都实现了这个特性。这使我能够在不必为每个操作解构每个节点类型的情况下遍历树,只需调用children()即可。

同时,我希望能够克隆一个节点,只更新其中一个子节点,并将其他子节点保留在原地。假设我已经能够生成正确具体类型的节点(如果我错了,程序就会出问题),我将向trait添加以下方法:

trait AstNode {
    fn clone_with_children(&self, new_children: Vec<Rc<AstNode>>) -> Self
        where Self: Sized;
}

我的计划是获取由children()返回的子节点,替换其中一个节点,并调用clone_with_children()来构建一个相同枚举变体的节点,但其中一个节点已被替换。

我的问题是如何编写clone_with_children()

我需要将Rc<AstNode>向下转型为Rc<Expression>(或其他类似类型),同时保持Rc中的引用计数不变,但是我发现找到的所有向下转型库都似乎无法实现这一点。

我想知道我想要的是否可能实现,还是应该完全改变我的方法?


1
你还有另一个问题需要解决:实际上,你不能返回 Self(特质对象无法通过值使用 Self),你可能需要返回 Rc<AstNode> - Matthieu M.
1
我认为在这里AST操作是一个干扰因素。你真正想知道的是是否可能将Rc<Trait>向下转换为Rc<Object>,其中Object实现了Trait,对吗? - trent
@MatthieuM,Expression确实实现了AstNode,我希望这一点是显而易见的。但这对于向下转型并没有什么帮助。 - rix0rrr
2
我似乎终于理解了你的问题;无论我是对还是错,我都鼓励你提供一个 MCVE 来展示你需要在哪个上下文中进行向下转型(并且不能)。我的 猜测AstNode::clone_with_children 返回了一个 Rc<AstNode>,它不能替换条件和表达式中的 Rc<Expression>,鉴于缺少代码示例... 我可以看到许多绕过这个问题的方法,但很难预测哪些有用,哪些是无用的... - Matthieu M.
我已经稍微编辑了我的回答,但问题的第二部分有点开放式。你可能应该考虑在Programmers.SE或Rust语言论坛上寻求更多的观点。 - trent
显示剩余3条评论
1个回答

8
注意:在本回答中,我将使用dyn Trait语法,以便更清楚地表示类型是一个trait对象。旧的编写Rc<dyn Trait>的方式是Rc<Trait>。参见“类型中的“dyn”是什么意思?
不,你不能将Rc<dyn Trait>向下转换为Rc<Concrete>,因为像dyn Trait这样的trait对象不包含有关数据所属的具体类型的任何信息。
以下是适用于所有指向trait对象(&dyn TraitBox<dyn Trait>Rc<dyn Trait>等)的指针的官方文档摘录:
pub struct TraitObject {
    pub data: *mut (),
    pub vtable: *mut (),
}

data字段指向结构体本身,vtable字段指向一个函数指针集合,每个trait方法对应一个函数指针。在运行时,这就是你拥有的全部内容。但这不足以重构结构体的类型。(使用Rc<dyn Trait>时,data块还包含强引用和弱引用计数,但没有其他类型信息。)

但至少有3种其他选择。

将公共行为放入trait中

首先,您可以将需要在ExpressionCondition上执行的所有操作添加到trait AstNode中,并为每个结构体实现它们。这样,您永远不需要调用在trait对象上不可用的方法,因为trait包含您需要的所有方法。

这可能需要将树中的大多数Rc<Expression>Rc<Condition>成员替换为Rc<dyn AstNode>,因为您无法向下转换Rc<dyn AstNode>(但请参见下面有关Any的内容):

enum Condition {
    Equals(Rc<dyn AstNode>, Rc<dyn AstNode>),
    LessThan(Rc<dyn AstNode>, Rc<dyn AstNode>),
    ...
}

这个的变体可能是在 AstNode 上编写方法,这些方法采用 &self 并返回到各种具体类型的引用:

trait AstNode {
    fn as_expression(&self) -> Option<&Expression> { None }
    fn as_condition(&self) -> Option<&Condition> { None }
    ...
}

impl AstNode for Expression {
    fn as_expression(&self) -> Option<&Expression> { Some(self) }
}

impl AstNode for Condition {
    fn as_condition(&self) -> Option<&Condition> { Some(self) }
}

不要将 Rc<dyn AstNode> 强制转换为 Rc<Condition>,只需将其存储为 AstNode 并调用例如 rc.as_condition().unwrap().method_on_condition(),如果您确信 rc 实际上是一个 Rc<Condition>

加倍使用 enum

其次,您可以创建另一个枚举类型,将 ConditionExpression 统一起来,完全放弃 trait 对象。这就是我在自己的 Scheme 解释器的 AST 中所做的。使用此解决方案,不需要强制转换,因为所有类型信息都在枚举变量中。(此外,使用此解决方案,如果需要从中获取 Rc<Node>,则绝对需要替换 Rc<Condition>Rc<Expression>。)

enum Node {
    Condition(Condition),
    Expression(Expression),
    // you may add more here
}
impl Node {
    fn children(&self) -> Vec<Rc<Node>> { ... }
}

使用Any进行下溯转换

第三种选择是使用Any,并根据需要将每个Rc<dyn Any>向其具体类型的Rc::downcast

稍微变化一下,可以在AstNode中添加一个方法fn as_any(&self) -> &Any { self },然后通过编写node.as_any().downcast_ref::<Expression>().method_on_expression()来调用Expression方法(接受&self)。但目前还没有办法(安全地)向上转换Rc<dyn Trait>Rc<dyn Any>(尽管这可能会在未来发生改变)。

Any严格来说是最接近你问题答案的东西。但我不建议使用它,因为向下转型或需要向下转型通常表明设计有缺陷。即使在具有类继承的语言(如Java)中,如果您想要做同样的事情(例如在ArrayList<Node>中存储一堆节点),您必须将所有所需操作都放在基类上或者枚举可能需要向下转换的所有子类,这是一个可怕的反模式。在这里使用Any所做的任何事情都与将AstNode更改为枚举相当复杂。

TL;DR

您需要将AST的每个节点存储为一种类型,该类型提供了您可能需要调用的所有方法,并统一了您可能需要放入其中的所有类型。选项1使用特质对象,而选项2使用枚举,但原则上它们非常相似。第三种选择是使用Any启用向下转型。

另请参阅


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接