【问题标题】:Is it possible for one struct to extend an existing struct, keeping all the fields?一个结构是否可以扩展现有结构,保留所有字段?
【发布时间】:2015-09-13 17:36:17
【问题描述】:

使用rust 1.2.0

问题

我仍在学习 Rust(来自 Javascript 背景)并试图弄清楚一个结构 StructB 是否有可能扩展现有结构 StructA 使得 StructB 具有StructA 上定义的所有字段。

在 Javascript(ES6 语法)中,我基本上可以做这样的事情......

class Person {
    constructor (gender, age) {
        this.gender = gender;
        this.age = age;
    }
}
class Child extends Person {
    constructor (name, gender, age) {
        super(gender, age);
        this.name = name;
    }
}

约束

  • StructA 来自我无法控制的外部 cargo 包。

当前进度

我找到了这个blog post on single-inheritance,听起来正是我需要的。

但尝试实现它会导致此错误消息error: virtual structs have been removed from the language。后来搜索了一下,我发现它很快就变成了implemented and then removed per RFC-341

还发现了这个thread about using traits,但由于StructA 来自外部货物包,我认为我不可能将其变成特征。

那么在 Rust 中实现这一点的正确方法是什么?

【问题讨论】:

  • 我认为在不知道您要使用新生成的结构的上下文及其原因的情况下回答这个问题总是次优的。在我看来,这些设计决策很大程度上取决于具体的用例。

标签: inheritance struct rust


【解决方案1】:

没有什么完全匹配的。我想到了两个概念。

  1. 结构组成

    struct Person {
        age: u8,
    }
    
    struct Child {
        person: Person,
        has_toy: bool,
    }
    
    impl Person {
        fn new(age: u8) -> Self {
            Person { age: age }
        }
    
        fn age(&self) -> u8 {
            self.age
        }
    }
    
    impl Child {
        fn new(age: u8, has_toy: bool) -> Self {
            Child { person: Person::new(age), has_toy: has_toy }
        }
    
        fn age(&self) -> u8 {
            self.person.age()
        }
    }
    
    fn main() {
        let p = Person::new(42);
        let c = Child::new(7, true);
    
        println!("I am {}", p.age());
        println!("My child is {}", c.age());
    }
    

    您可以简单地将一个结构嵌入到另一个结构中。内存布局简洁紧凑,但您必须手动将 Person 中的所有方法委托给 Child 或借出 &Person

  2. 特征

    trait SayHi {
        fn say_hi(&self);
    }
    
    struct Person {
        age: u8,
    }
    
    struct Child {
        age: u8,
        has_toy: bool,
    }
    
    impl SayHi for Person {
        fn say_hi(&self) {
            println!("Greetings. I am {}", self.age)
        }
    }
    
    impl SayHi for Child {
        fn say_hi(&self) {
            if self.has_toy {
                println!("I'm only {}, but I have a toy!", self.age)
            } else {
                println!("I'm only {}, and I don't even have a toy!", self.age)
            }
        }
    }
    
    fn greet<T>(thing: T)
        where T: SayHi
    {
        thing.say_hi()
    }
    
    fn main() {
        let p = Person { age: 42 };
        let c = Child { age: 7, has_toy: true };
    
        greet(p);
        greet(c);
    }
    

当然,您可以将这两个概念结合起来。


作为DK. mentions,您可以选择实现DerefDerefMut。但是,我不同意以这种方式使用这些特征。我的论点类似于仅仅为了代码重用而使用经典的面向对象继承是错误的论点。 “优先组合优于继承”=>“优先组合优于 Deref”。但是,我确实希望能够实现简洁委托的语言功能,从而减少组合的烦恼。

【讨论】:

  • 这是我怀疑的。感谢您的确认并感谢您的快速答复!对于那里的其他菜鸟......这些sn-ps会为你编译吗?如果我没有 new 的返回类型,则会出现编译错误。
  • @drebabels 不,这是我转移它时的错误。我确保它现在可以编译:-)
  • @Shepmaster:为Child 实现Deref&lt;Target = Person&gt;DerefMut 将大大有助于使组合方法很好地工作。因为在大多数情况下,您可以将Child 视为Person
  • @ChrisMorgan 这可能是真的,但我属于那些认为这在语义上不正确的人的阵营^_^。有一天我会写我的 RFC 来提出一个替代方案……总有一天……
  • 有这方面的消息吗?
【解决方案2】:

Rust 没有任何类型的结构继承。如果您希望StructB 包含与StructA 相同的字段,则需要使用组合。

struct StructB {
    a: StructA,
    // other fields...
}

另外,澄清一下,特征能够定义方法和相关类型;他们不能定义字段。

如果您希望能够将StructB 用作StructA,您可以通过实现DerefDerefMut 特征来实现一些,这将允许编译器将指向StructBs 的指针隐式转换为指向StructAs 的指针:

struct StructA;

impl StructA {
    fn name(&self) -> &'static str {
        "Anna"
    }
}

struct StructB {
    a: StructA, 
    // other fields...
}

impl std::ops::Deref for StructB {
    type Target = StructA;
    fn deref(&self) -> &Self::Target {
        &self.a
    }
}

fn main() {
    let b = StructB { a: StructA };
    println!("{}", b.name());
}

【讨论】:

    【解决方案3】:

    另一种选择是使用泛型:

    trait IAnimalData {}
    
    struct Animal<D: IAnimalData> {
        name: String,
        age: i64,
        child_data: D,
    }
    
    struct Dog {
        favorite_toy: String,
    }
    
    impl IAnimalData for Dog {}
    

    然后你可以实现这样的“子”方法,它只适用于狗:

    impl Animal<Dog> {
        pub fn bark(&self) -> String {
            return "bark!".to_owned();
        }
    }
    

    如果你想要适用于所有动物的父方法,你可以像这样实现它们:

    // implements the 'breathe' method for all animals
    impl<T: IAnimalData> Animal<T> {
        fn breathe() {}
    }
    

    好处是你不必经历将Dog中的方法转发到Animal中的方法的痛苦;您可以直接在impl Animal&lt;Dog&gt; 中使用它们。此外,您可以通过Animal&lt;Dog&gt; 的任何方法访问Animal 中定义的任何字段。不好的部分是你的继承链总是可见的(也就是说,你可能永远不会在你的代码中使用Dog,而是Animal&lt;Dog&gt;)。此外,如果继承链很长,您可能会得到一些非常愚蠢、冗长的类型,例如Animal&lt;Dog&lt;Chihuahua&gt;&gt;。我想在这一点上一个类型别名是可取的。

    【讨论】:

    • 这是一个很好的解决方案,但其他 Rust 程序员可能会嘲笑你命名特征 ITraitName。只需TraitName 就可以了。特征和类型不是同一个命名空间的一部分,因此很少有任何混淆(至少,只要您将 dyn 用于特征对象类型)。
    • @trentcl “笑你”有点刺耳。惯用语,不,但任何拿别人的惯例取笑的人都需要自我评估。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-11-22
    • 1970-01-01
    • 2011-04-20
    • 1970-01-01
    • 2018-11-08
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多