为什么Rust在类型构造函数中不能将可变引用强制转换为不可变引用?

6
可以将 &mut T 强制转换成 &T,但如果类型不匹配发生在类型构造器内则无法工作。 playground
use ndarray::*; // 0.13.0

fn print(a: &ArrayView1<i32>) {
    println!("{:?}", a);
}

pub fn test() {
    let mut x = array![1i32, 2, 3];
    print(&x.view_mut());
}

对于上面的代码,我收到以下错误:

  |
9 |     print(&x.view_mut());
  |           ^^^^^^^^^^^^^ types differ in mutability
  |
  = note: expected reference `&ndarray::ArrayBase<ndarray::ViewRepr<&i32>, ndarray::dimension::dim::Dim<[usize; 1]>>`
             found reference `&ndarray::ArrayBase<ndarray::ViewRepr<&mut i32>, ndarray::dimension::dim::Dim<[usize; 1]>>`

安全的将&mut i32强制转换为&i32,所以为什么在这种情况下不适用呢?你能提供一些可能会出现问题的示例吗?
3个回答

6
一般来说,强制将 Type<&mut T> 转换成 Type<&T> 是不安全的。例如,考虑下面这个包装类型,它是在没有任何不安全代码的情况下实现的,因此是可靠的:
#[derive(Copy, Clone)]
struct Wrapper<T>(T);

impl<T: Deref> Deref for Wrapper<T> {
    type Target = T::Target;
    fn deref(&self) -> &T::Target { &self.0 }
}

impl<T: DerefMut> DerefMut for Wrapper<T> {
    fn deref_mut(&mut self) -> &mut T::Target { &mut self.0 }
}

这种类型有一个特性,即&Wrapper<&T>会自动解引用为&T&mut Wrapper<&mut T>会自动解引用为&mut T。此外,如果T可复制,则Wrapper<T>是可复制的。

假设存在一个函数,可以将&Wrapper<&mut T>强制转换为&Wrapper<&T>

fn downgrade_wrapper_ref<'a, 'b, T: ?Sized>(w: &'a Wrapper<&'b mut T>) -> &'a Wrapper<&'b T> {
    unsafe {
        // the internals of this function is not important
    }
}

通过使用此函数,可以同时获得可变和不可变引用指向同一值:

fn main() {
    let mut value: i32 = 0;

    let mut x: Wrapper<&mut i32> = Wrapper(&mut value);

    let x_ref: &Wrapper<&mut i32> = &x;
    let y_ref: &Wrapper<&i32> = downgrade_wrapper_ref(x_ref);
    let y: Wrapper<&i32> = *y_ref;

    let a: &mut i32 = &mut *x;
    let b: &i32 = &*y;

    // these two lines will print the same addresses
    // meaning the references point to the same value!
    println!("a = {:p}", a as &mut i32); // "a = 0x7ffe56ca6ba4"
    println!("b = {:p}", b as &i32);     // "b = 0x7ffe56ca6ba4"
}

完整代码演示

在Rust中,这样做是不允许的,会导致未定义的行为,并意味着downgrade_wrapper_ref函数在这种情况下不可靠。也许还有其他特定情况,您作为程序员可以保证不会发生这种情况,但仍需要使用unsafe代码专门实现这些情况,以确保您承担做出这些保证的责任。


没有 Wrapper,我们在安全 Rust 中不会陷入相同的情况吗? - pkubik
@pkubik 我想这是一个不错的反例,我的代码可能应该更加明确,但如果你在 println! 语句中添加类型注释,你的示例会出现生命周期错误,而我的则不会。比较一下我的代码(已更新,可以编译)和你的代码(已更新,无法编译)。我会更新我的答案来包含这个信息。 - Frxstrem
我猜问题在于它没有被使用为可变引用,所以它实际上并没有检查它是否真的是可变的。添加显式类型注释将确保可变引用被可变使用,这应该会导致生命周期错误,但实际上并没有。 - Frxstrem
是的,实际上有趣的是,引用变量的定义不被编译器视为借用,你必须实际使用它才能导致错误。我想这在组织代码时提供了更多的灵活性。 - pkubik

3

考虑以下检查空字符串的示例,该示例依赖于 contentis_empty 函数的运行时保持不变(仅用于说明目的,请勿在生产代码中使用):

struct Container<T> {
    content: T
}

impl<T> Container<T> {
    fn new(content: T) -> Self
    {
        Self { content }
    }
}

impl<'a> Container<&'a String> {
    fn is_empty(&self, s: &str) -> bool
    {
        let str = format!("{}{}", self.content, s);
        &str == s
    }
}

fn main() {
    let mut foo : String = "foo".to_owned();
    let container : Container<&mut String> = Container::new(&mut foo);

    std::thread::spawn(|| {
        container.content.replace_range(1..2, "");
    });

    println!("an empty str is actually empty: {}", container.is_empty(""))
}

(Playground)

这段代码无法编译,因为 &mut String 不能强制转换为 &String。然而,如果可以的话,新创建的线程可能会在 format! 调用后但在等于比较 is_empty 函数之前更改 content,从而使得前提假设不再成立,即容器的内容是不可变的,这是必须的空检查。


我想我明白了。如果我直接尝试使用字符串,我会得到“无法将foo作为不可变借用,因为它也被作为可变借用”。另一方面,“Container”始终通过不可变引用传递,而不管“content”类型如何,因此不会触发此机制。 - pkubik

0

当数组作为函数参数类型时,似乎类型强制转换不适用于数组元素。

playground


这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - possum

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