如何编写一个函数,能够同时接受拥有和非拥有的字符串集合?

18

我遇到了写一个函数的困难,它需要将一个字符串集合作为参数。我的函数看起来像这样:

type StrList<'a> = Vec<&'a str>;

fn my_func(list: &StrList) {
    for s in list {
        println!("{}", s);
    }
}

如果我将Vec<&'a str>传递给函数,一切都很顺利,正如预期的那样。然而,如果我传递一个Vec<String>,编译器会抱怨:

error[E0308]: mismatched types
  --> src/main.rs:13:13
   |
13 |     my_func(&v2);
   |             ^^^ expected &str, found struct `std::string::String`
   |
   = note: expected type `&std::vec::Vec<&str>`
   = note:    found type `&std::vec::Vec<std::string::String>`

这是主要使用的内容:

fn main() {
    let v1 = vec!["a", "b"];
    let v2 = vec!["a".to_owned(), "b".to_owned()];
    my_func(&v1);
    my_func(&v2);
}

我的函数无法处理拥有字符串的向量。相反,如果我将StrList类型改为:

type StrList = Vec<String>;

第一次调用失败,第二次成功。

一个可能的解决方案是从 v2 生成一个 Vec<&'a str>,方法如下:

let v2_1 : Vec<_> = v2.iter().map(|s| s.as_ref()).collect();

但这对我来说似乎很奇怪。my_func 不应该关心字符串的所有权。

my_func 应该使用什么样的参数签名来支持拥有字符串的向量和字符串引用?

2个回答

28

尽管String&str非常相关,但它们并不完全相同。以下是内存中向量的样子:

v1 ---> [ { 0x7890, // pointer to "a" + 7 unused bytes
            1 }     // length of "a"
          { 0x7898, // pointer to "b" + 7 unused bytes
            1 } ]   // length

v2 ---> [ { 0x1230 // pointer to "a" + 7 unused bytes (a different copy)
            8      // capacity
            1 }    // length
          { 0x1238 // pointer ...
            8      // capacity
            1 } ]  // length

每行的内存大小相同(根据指针大小为四或八字节)。你不能将其中一个的内存视为另一个。内存布局不匹配,项目大小不同且布局不同。例如,如果v1将其项目存储在地址X处,而v2将其项目存储在地址Y处,则v1[1]位于地址X + 8,但v2[1]位于地址Y + 12
你可以编写一个通用函数,例如:
fn my_func<T: AsRef<str>>(list: &[T]) {
    for s in list {
        println!("{}", s.as_ref());
    }
}

然后编译器可以为&[String]&[&str]以及其他实现AsRef<str>的类型生成适当的代码。


它确实有效。你能更好地解释一下为什么&[T]有效吗?AsRef<str>是清晰的。 - mbrt
4
你了解切片 &[T],只是想知道为什么函数接受 &[T],但 main 传递的是 &Vec<T>,对吧?答案在于 deref 强制转换。因为 &Vec<T>&[T] 更具体(你可以从很多不是向量的来源获得后者),所以推荐编写接受 &[T] 而不是 &Vec<T> 的函数。为了方便,foo(&vec) 会自动从向量构建切片。 - user395760

5
为了进一步完善 delnan的好答案,我想指出你可以在这里添加更多层次的泛型。你说过:

一个字符串集合

但是除了切片和向量之外,还有更多类型的集合!在你的示例中,你关心的是对项进行单向、逐个访问。这是迭代器的完美例子。下面,我已经改变了你的函数,使其接受任何可以转换为迭代器的类型。然后你就可以传入更多类型的东西。我使用了一个 HashSet 作为示例,但请注意,你也可以传入 v1v2 而不是 &v1&v2,并消耗它们。

use std::collections::HashSet;

fn my_func<I>(list: I)
    where I: IntoIterator,
          I::Item: AsRef<str>,
{
    for s in list {
        println!("{}", s.as_ref());
    }
}

fn main() {
    let v1 = vec!["a", "b"];
    let v2 = vec!["a".to_owned(), "b".to_owned()];
    let v3 = {
        let mut set = HashSet::new();
        set.insert("a");
        set.insert("b");
        set.insert("a");
        set
    };
    let v4 = {
        let mut set = HashSet::new();
        set.insert("a".to_owned());
        set.insert("b".to_owned());
        set.insert("a".to_owned());
        set
    };

    my_func(&v1);
    my_func(v1);
    my_func(&v2);
    my_func(v2);
    my_func(&v3);
    my_func(v3);
    my_func(&v4);
    my_func(v4);
}

这其实更好!@delnan确实回答了我的问题,但是这个版本更通用。 - mbrt

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