在Rust中向不可变向量添加元素

3

我正在尝试使用函数式编程和递归来创建一个Rust用户输入验证函数。如何返回一个不可变向量,并将一个元素连接到末尾?

fn get_user_input(output_vec: Vec<String>) -> Vec<String> {
    // Some code that has two variables: repeat(bool) and new_element(String)
    if !repeat {
        return output_vec.add_to_end(new_element); // What function could "add_to_end" be?
    }
    get_user_input(output_vec.add_to_end(new_element)) // What function could "add_to_end" be?
}

对于其他操作都有相应的函数:
push 将可变向量添加到可变向量中
append 将元素添加到可变向量的末尾
concat 将不可变向量添加到不可变向量中
??? 将元素添加到不可变向量的末尾

我目前能够工作的唯一解决方案是使用:

[write_data, vec![new_element]].concat()

但是这种方法似乎效率不高,因为我只为一个元素创建了一个新的向量(因此大小在编译时已知)。


1
为什么你不能只使用mut output_vec: Vec<String>?你不可变地取值的原因是什么?(或者output_vec应该是一个引用?) - cdhowie
1
我可以使它可变,但我希望有一种遵循函数式编程原则的方法来实现这一点。 - Alex
1
add_to_end 应该返回一个新的 Vec 吗?看起来它实际上是 let mut result = output_vec.clone(); result.push(new_element) - Herohtar
@Alex,你已经在示例代码中尝试使用了类似于Vec::push()的等效方法,这不是纯函数式的。你会发现,在Rust中,由于其借用系统,纯函数式方法并不适用。此外,请注意,与诸如JavaScript或Java这样的语言不同,你实际上从未真正拥有一个对象(而是总是拥有引用),但在Rust中,你可以拥有一个对象本身,这就是 output_vec 所代表的。你不必担心不要改变不属于你的对象,因为output_vec属于该函数,因为它是按值获取的。 - cdhowie
6
我在质疑这个限制的理由,因为我认为这不是一个有用的限制,并且反映了对 Rust 中所有权概念的误解。 - cdhowie
显示剩余4条评论
1个回答

18
你将Rust和一种只有对象引用的语言混淆了。在Rust中,代码可以对对象拥有独占所有权,因此您不需要像处理可能共享的对象时那样小心,因为您知道该对象是否被共享。
例如,以下是有效的JavaScript代码:
const a = [];
a.push(1);

这能正常工作是因为a不包含数组,它包含一个指向数组的引用。1 const防止a被指向到另一个对象,但它不会使数组本身成为不可变的。
所以,在这些类型的语言中,纯函数式编程试图避免任何状态的变异,例如将项目推送到作为参数接受的数组中:
function add_element(arr) {
  arr.push(1); // Bad! We mutated the array we have a reference to!
}

相反,我们会这样做:

function add_element(arr) {
  return [...arr, 1]; // Good! We leave the original data alone.
}

在 Rust 中,根据您的函数签名,您拥有的是完全不同的场景!在您的情况下,output_vec 是由函数本身拥有的,程序中没有其他实体可以访问它。因此,如果这是您的目标,就没有理由避免对其进行变异:

fn get_user_input(mut output_vec: Vec<String>) -> Vec<String> {
//       Add mut  ^^^

请记住,任何非引用值都是所拥有的值。&Vec<String> 是对某个其他元素拥有的向量的不可变引用,但 Vec<String> 是这段代码拥有的向量,没有其他人可以访问。

不信?下面是一个简单的破坏性代码示例:

fn take_my_vec(y: Vec<String>) { }

fn main() {
    let mut x = Vec::<String>::new();
    
    x.push("foo".to_string());
    
    take_my_vec(x);
    
    println!("{}", x.len()); // E0382
}
x.len()导致编译时错误,因为向函数参数中传递的向量x已被移动,我们不再拥有它。
那么为什么函数不能变异它现在所拥有的向量呢?调用者无法再使用它了。
综上所述,在Rust中,函数式编程看起来有些不同。在其他没有办法通信“我正在给你这个对象”的语言中,必须避免变异您收到的值,因为调用者可能不希望您更改它们。在Rust中,谁拥有一个值是清楚的,并且参数反映了这一点:
- 参数是值(Vec<String>)吗?函数现在拥有该值,调用者已经放弃并且不能再使用它。如果需要,请对其进行变异。 - 参数是不可变引用(&Vec<String>)吗?函数不拥有它,并且无法变异它,因为Rust不允许。您可以克隆它并使克隆变异。 - 参数是可变引用(&mut Vec<String>)吗?调用者必须明确给出一个可变引用,因此授权给函数变异它——但函数仍然不拥有该值。函数可以变异它、克隆它或两者都可以——这取决于函数的预期行为。
如果按值接受参数,则没有多少理由不对其进行mut,如果因任何原因需要更改它。请注意,此详细信息(函数参数的可变性)甚至不是函数的公共签名的一部分,因为这不是调用方的业务。他们已经放弃了该对象。
请注意,在具有类型参数的类型(如Vec)中,还可以使用其他所有权表达式。以下是一些示例(这不是详尽无遗的列表):
- Vec<&String>: 您现在拥有一个向量,但您不拥有其中包含的引用到String对象。 - &Vec<&String>: 您被授予对字符串引用向量的只读访问权限。您可以克隆这个向量,但仍然不能更改字符串,例如只能重新排列它们。 - &Vec<&mut String>: 您被授予对可变字符串引用向量的只读访问权限。你不能重新排列字符串,但你可以更改字符串本身。 - &mut Vec<&String>: 类似于上面但相反: 您被允许重新排列字符串引用,但不能更改字符串。

1一个好的思考方式是,在JavaScript中,非原始值总是Rc<RefCell<T>>的值,因此您正在传递一个具有内部可变性的对象句柄。 const仅使Rc<>不可变。


如果将列表或数组作为值传递给函数,调用函数如何停止拥有它?是否只是发生了隐式克隆? - user5536315
2
@IvenMarquardt 不是的,该值被移动到函数参数中,调用者不再拥有它。除非将其移动到其他位置(例如通过返回),否则该值将在函数返回时被销毁。在我的示例代码中,在调用take_my_vec(x)之后,局部变量x可以被认为是“未初始化”的。(由于xmut,您可以将另一个向量分配给它,然后再次使用该变量,但曾经在x中的向量已转移到函数参数中。) - cdhowie
2
@IvenMarquardt 有一个内置函数 std::mem::drop()。这个函数的目的是销毁你不再需要的值,释放与之相关的任何内存分配或其他资源(文件/网络句柄等)。它是如何做到的呢?这个函数的定义是字面意思上的 pub fn drop<T>(_x: T) { }。这里没有什么魔法--该值被销毁是因为你将其移动到函数参数中,函数立即返回,销毁该值,因为它现在是所有者,拥有变量正在超出范围。 - cdhowie
1
@cdhowie 我希望我能给这个详细解释20个赞!太棒了!非常感谢。 - Nirmalya

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