一般来说,你试图完成的任务是不安全的,而 Rust 正确地阻止了你做一些不应该做的事情。举个简单的例子,考虑一个 Vec<u8>
。如果向这个容器中添加另一个值,则会导致重新分配和复制向量中所有值,从而使任何引用无效。这将导致索引中的所有键都指向任意的内存地址,从而导致不安全行为。编译器可以防止这种情况发生。
在这种情况下,程序员知道的有两个额外的信息,但编译器并不知道:
- 有一个额外的间接性——
String
是在堆上分配的,因此移动指向该堆分配的指针并不是真正的问题。
String
将永远不会被更改。 如果更改了它,则可能会重新分配,从而使所引用的地址无效。使用 Box<[str]>
代替 String
可以通过类型系统强制执行这一点。
在这种情况下,只要正确记录为什么它不是不安全的,就可以使用unsafe
代码。
use std::collections::HashMap;
#[derive(Debug)]
struct Player {
name: String,
}
fn main() {
let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
let mut players = Vec::new();
let mut index = HashMap::new();
for &name in &names {
let player = Player { name: name.into() };
let idx = players.len();
let stable_name: &str = unsafe { &*(player.name.as_str() as *const str) };
players.push(player);
index.insert(idx, stable_name);
}
for (k, v) in &index {
println!("{:?} -> {:?}", k, v);
}
for v in &players {
println!("{:?}", v);
}
}
然而,我的猜测是你不想在main
方法中放置这段代码,而是希望从某个函数中返回它。这将会是一个问题,因为你很快就会遇到为什么不能在同一个结构体中存储值和对该值的引用?。
老实说,有些代码风格不太适合 Rust 的限制。如果你遇到这种情况,你可以:
- 决定 Rust 不适合你或你的问题。
- 使用
unsafe
代码,最好经过充分测试并只公开安全的 API。
- 研究替代表示方法。
例如,我可能会重写代码,让索引成为键的主要所有者:
use std::collections::BTreeMap;
#[derive(Debug)]
struct Player<'a> {
name: &'a str,
data: &'a PlayerData,
}
#[derive(Debug)]
struct PlayerData {
hit_points: u8,
}
#[derive(Debug)]
struct Players(BTreeMap<String, PlayerData>);
impl Players {
fn new<I>(iter: I) -> Self
where
I: IntoIterator,
I::Item: Into<String>,
{
let players = iter
.into_iter()
.map(|name| (name.into(), PlayerData { hit_points: 100 }))
.collect();
Players(players)
}
fn get<'a>(&'a self, name: &'a str) -> Option<Player<'a>> {
self.0.get(name).map(|data| Player { name, data })
}
}
fn main() {
let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
let players = Players::new(names.iter().copied());
for (k, v) in &players.0 {
println!("{:?} -> {:?}", k, v);
}
println!("{:?}", players.get("eustice"));
}
另一种选择是,如何以惯用的方式创建一个查找表,该表使用项的字段作为键?所示,您可以将类型包装并存储在集合容器中:
use std::collections::BTreeSet;
#[derive(Debug, PartialEq, Eq)]
struct Player {
name: String,
hit_points: u8,
}
#[derive(Debug, Eq)]
struct PlayerByName(Player);
impl PlayerByName {
fn key(&self) -> &str {
&self.0.name
}
}
impl PartialOrd for PlayerByName {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PlayerByName {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.key().cmp(&other.key())
}
}
impl PartialEq for PlayerByName {
fn eq(&self, other: &Self) -> bool {
self.key() == other.key()
}
}
impl std::borrow::Borrow<str> for PlayerByName {
fn borrow(&self) -> &str {
self.key()
}
}
#[derive(Debug)]
struct Players(BTreeSet<PlayerByName>);
impl Players {
fn new<I>(iter: I) -> Self
where
I: IntoIterator,
I::Item: Into<String>,
{
let players = iter
.into_iter()
.map(|name| {
PlayerByName(Player {
name: name.into(),
hit_points: 100,
})
})
.collect();
Players(players)
}
fn get(&self, name: &str) -> Option<&Player> {
self.0.get(name).map(|pbn| &pbn.0)
}
}
fn main() {
let names = ["alice", "bob", "clarice", "danny", "eustice", "frank"];
let players = Players::new(names.iter().copied());
for player in &players.0 {
println!("{:?}", player.0);
}
println!("{:?}", players.get("eustice"));
}
通过使用Rc
或RefCell
不会增加运行时间。
在没有进行性能分析的情况下猜测性能特征从未是一个好主意。如果需要克隆或丢弃值时递增整数,我真的不相信会有明显的性能损失。如果问题需要同时使用索引和向量,则我会选择某种共享所有权。