在Rust中编写getter/setter属性

55

我正在编写一个非常简单的 getter/setting 模型,我想开始在 Rust 中使用它,出于简单性的原因,我将使用 structimpl

struct Person {
    firstName: String,
    lastName: String,
}

impl Person {
    fn get_first_name(&mut self) -> String { return self.firstName; }
    fn get_last_name(&mut self) -> String {  return self.lastName; }

    fn set_first_name(&mut self, x: String) { self.firstName = x; }
    fn set_last_name(&mut self, x: String) { self.lastName = x; }

    fn default() -> Person {
        Person {firstName: "".to_string(), lastName: "".to_string()}
    }
}

fn main() {
    let mut my_person : Person = Person{ ..Person::default() };

    my_person.set_first_name("John".to_string());
    my_person.set_last_name("Doe".to_string());

    println!("{}", my_person.firstName);
    println!("{}", my_person.lastName);
}

运行这段代码时,我得到了以下错误。

src\main.rs:7:53: 7:57 error: cannot move out of borrowed content [E0507]
src\main.rs:7     fn get_first_name(&mut self) -> String { return self.firstName; }
                                                                  ^~~~
src\main.rs:8:53: 8:57 error: cannot move out of borrowed content [E0507]
src\main.rs:8     fn get_last_name(&mut self) -> String {  return self.lastName; }
                                                                  ^~~~
error: aborting due to 2 previous errors
Could not compile `sandbox`.

由于我对 Rust 很陌生,请问有人能指出我的错误吗?

如果有更好的写法建议,也欢迎提供。我一直在寻找更易读、更快速的方法。


6
你为什么认为需要使用getter和setter呢?为什么不使用更简单的struct Person { pub first_name: String, pub last_name: String, }呢? - Matthieu M.
3
请注意,有时候 getter 和 setter 仍然很有用,例如当您在 trait 中定义默认方法时,因为目前您无法在 trait 中定义/访问任何字段。请参见 https://internals.rust-lang.org/t/fields-in-traits/6933 - xji
4个回答

71

好的,这里具体的问题是无法移出借用的内容。这个问题已经在不同情况下被回答 很多 之前,更不用说 Rust Book中关于所有权主题的章节

更有趣的一个问题是关于getter和setter的。是的,你可以在Rust中编写它们,但它们可能不是最佳选择。

在我继续之前,我只想指出,在getter上要求&mut self绝对没有理由……除非你打算修改该值以删除该值作为其中一部分,但那时你就不再处理getter了。

其次,在getter中不应该使用clone。如果用户只是想读取值,这样做会非常浪费。最好返回一个不可变借用,用户可以在需要时clone

无论如何,如果您编写这些代码是因为想要运行某种逻辑以验证新值,请继续使用setter。否则,您可以像这样操作:
#[derive(Default)]
struct Person {
    first_name: String,
    last_name: String,
}

impl Person {
    // Immutable access.
    fn first_name(&self) -> &str {
        &self.first_name
    }
    fn last_name(&self) -> &str {
        &self.last_name
    }

    // Mutable access.
    fn first_name_mut(&mut self) -> &mut String {
        &mut self.first_name
    }
    fn last_name_mut(&mut self) -> &mut String {
        &mut self.last_name
    }
}

fn main() {
    let mut my_person = Person::default();

    *my_person.first_name_mut() = String::from("John");
    *my_person.last_name_mut() = "Doe".into();

    println!("first_name: {}", my_person.first_name());
    println!("last_name: {}", my_person.last_name());
    
    // Can't do this efficiently with getter/setter!
    {
        let s = my_person.last_name_mut();
        s.truncate(2);
        s.push('w');
    }

    println!("first_name: {}", my_person.first_name());
    println!("last_name: {}", my_person.last_name());
}

这样用户就可以更或多或少地直接访问字段,而不是实际上直接访问字段。除了编写新值之外,这还允许用户原地突变现有值,这对于大型堆分配的东西非常重要。
此外,我还进行了一些其他更改:
  • 您可以机械地推导Default;在这种情况下没有必要自己编写它。

  • 传统风格是使用snake_case表示字段。

  • 您创建Person的方式是不必要的迂回。


使用&str而不是&String更加正确,不是吗? - W.K.S
@W.K.S,有趣的是我也有同样的问题。我记得读到过使用str代替的内容,但我不记得了... - ajm113
1
通常情况下是这样的。然而,在这种情况下,您还可以对String进行可变访问,并且有一件事情您可以使用&String做而不能使用&str:检查容量。此外,一般来说,这两个指针将是相同类型的。 - DK.
2
@DK:将可变引用交给字段相比于仅将字段公开的好处是什么? - Matthieu M.
2
@MatthieuM。它唯一的好处就是用户无法移出,您可以在运行时更改它存储的位置。除此之外... 耸肩 - DK.
显示剩余3条评论

9
您的getter方法借用了self。当您返回self.name时,您正在移动name,这是不允许的借用引用。您应该返回一个name的副本。
另外,在getter方法中,您不需要传递可变引用self,因为您没有修改内部结构体。
因此,您的getter方法应该是:
fn get_first_name(&self) -> &str {
    self.first_name.as_str()
}
fn get_last_name(&self) -> &str {
    self.last_name.as_str()
}

嘿,感谢您提供非常有帮助的反馈!您这样说让我觉得直接做一切都很有道理。显然,我来自于一个非常重的PHP/Java面向对象编程背景。因此,在这种情况下,我需要开始更简单地考虑函数式编程,这基本上是我需要做的大部分工作。除非我们在谈论向结构添加功能。 - ajm113
@ajm113 这与函数式编程与面向对象编程无关。这与 Rust 中的内存管理有关。在 getter 方法中,不需要返回一个 &mut self,因为获取操作不会改变任何值(mut 表示可变性)。 - Dirk R

2

正如编译器指出的那样,你正在犯的错误是试图移动一个在引用后面的变量的值,但这个单独的解释可能很难理解,如果不了解为什么移动发生以及为什么它“在引用后面”,这对于未经训练的眼睛来说并不那么明显,因此我将尝试解释一下。

引用是不同的类型

get_first_name()get_last_name() 的签名中,你已将参数声明为 &mut self。这意味着在这些函数的主体中,变量 self 的类型为 &mut Person(发音为“可变引用到 Person”),而不是 Person。这两种类型是不同的,就像任何一对不同的类型一样,有不同的规则管理你如何使用它们,因此将它们视为不同的类型非常重要。

移动规则

&mut PersonPerson类型在使用上有很多相似之处(特别是与Person API相关的部分),但它们之间的一个主要区别是,你可以从Person实例的字段中移动值,但不能从&mut Person&Person的字段中移动值:这些类型被称为“引用”或“借用”。

Copy值具有移动语义

任何具有未实现Copy trait的类型的值,在以下情况下都会发生移动:

  • 赋值给变量
  • 传递给函数或方法
  • 从函数或方法返回
你的函数体是 return self.firstName,它试图将 firstName 字段中的值返回给调用者。这个值的类型是 String,它实现 Copy,因此这一行的意思是“将存储在结构体实例中的 firstName 字段的值移动到 get_first_name() 的调用者中”。但是,由于 self 的类型是 &mut Person,所以这是不允许的。

2

您可以尝试实现一些类似于GetterSetter的东西。例如:

struct Person {
    first_name: Gettter<String>,
    last_name: GettterSetter<String>,
}

impl Person {
    fn new() -> Self {
        Self { 
            first_name: Gettter::new(|_| /*do_your_stuff_on_get*/),
            last_name: GettterSetter::new(
              |_| /*do_your_stuff_on_get*/,
              |val| /*do_your_stuff_on_set*/
            ),
        }
    }
}

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