Rust中Move语义下的Copy与Clone

问题

在写Rust代码的时候,在遇到函数、闭包甚至是循环等作用域的切换时,不知道当前要操作的对象是被borrow或者move,所以经常会报一些错误,想借用一些示例来测试切换作用域时Rust会做一些什么操作,也由此延伸出了Copy与Clone的操作差异

测试场景

使用多线程、闭包来模拟作用域的切换

测试对象没有去指定Send+Sync,因为没有涉及数据竞争

let some_obj=xxx
let handle=std::thread::spawn(move ||{
    println!("{:#?}", some_obj);
});

handle.join().unwrap();
println!("{:#?}", some_obj);

测试对象

按照Rust中的定义,可以分为2种

1 可知固定长度的对象,在其他语言中有时会使用值对象来作为定义

2 运行时动态长度的对象,一般内存在heap中,stack上分配的是指针,指向heap中的地址,其他语言中有时会使用引用对象作为定义

值对象

可以使用数字类型来代表此类对象

let num_f=21.3;
let num_i=33;
let char='a';

let handle=std::thread::spawn(move ||{
  println!("{:?} : {:#?}",std::thread::current().id(), num_f);
  println!("{:?} : {:#?}",std::thread::current().id(), num_i);
  println!("{:?} : {:#?}",std::thread::current().id(), char);
});

handle.join().unwrap();

println!("{:?} : {:#?}",std::thread::current().id(), num_f);
println!("{:?} : {:#?}",std::thread::current().id(), num_i);
println!("{:?} : {:#?}",std::thread::current().id(), char);
ThreadId(3) : 21.3
ThreadId(3) : 33
ThreadId(3) : 'a'
ThreadId(2) : 21.3
ThreadId(2) : 33
ThreadId(2) : 'a'

如果去掉move关键字,会有什么情况?以下是运行的结果,直接报错

46 |     let handle=std::thread::spawn( ||{
   |                                    ^^ may outlive borrowed value `num_f`
47 |         println!("{:?} : {:#?}",std::thread::current().id(), num_f);
   |                                                              ----- `num_f` is borrowed here
   |
note: function requires argument type to outlive `'static`
  --> src/thread_shared_obj/thread_test.rs:46:16
   |
46 |       let handle=std::thread::spawn( ||{
   |  ________________^
47 | |         println!("{:?} : {:#?}",std::thread::current().id(), num_f);
48 | |         println!("{:?} : {:#?}",std::thread::current().id(), num_i);
49 | |         println!("{:?} : {:#?}",std::thread::current().id(), char);
50 | |     });
   | |______^
help: to force the closure to take ownership of `num_f` (and any other referenced variables), use the `move` keyword
   |
46 |     let handle=std::thread::spawn( move ||{

may outlive borrowed value ,由此可知闭包默认使用的是borrow ,而不是move,对应的Trait是 Fn,如果是使用move关键字,对应的Trait就会是FnOnce

继续看这句报错,回过头看代码,可知的是,在线程中的作用域使用num_f这个变量时,由于num_f也在外面的作用域,Rust编译器不能确定在运行时外面是否会修改这个变量,对于此种场景,Rust是拒绝编译通过的

这里虽然有了move关键字,但对于值对象来说,就是copy了一个全新的值

线程中的值对象和外面作用域的值对象,此时实际上变成了2分,Copy动作是Rust编译器自动执行的

引用对象

字符串

字符串在运行时是可以动态改变大小的,所以在stack上会有指向heap中内存的指针

let string_obj="test".to_string();

let handle=std::thread::spawn( move ||{
	println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
});

handle.join().unwrap();

println!("{:?} : {:#?}",std::thread::current().id(), string_obj);

运行结果

61 |    let string_obj="test".to_string();
   |        ---------- move occurs because `string_obj` has type `String`, which does not implement the `Copy` trait
62 | 
63 |     let handle=std::thread::spawn( move ||{
   |                                    ------- value moved into closure here
64 |         println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
   |                                                              ---------- variable moved due to use in closure
...
69 |     println!("{:?} : {:#?}",std::thread::current().id(), string_obj);
   |                                                          ^^^^^^^^^^ value borrowed here after move

这里会产生问题,和值对象一样,使用了move关键字,但为什么字符串这里报错了

看报错的语句

move occurs because `string_obj` has type `String`, which does not implement the `Copy` trait

在值对象的示例中,并没有这样的错误,也由此可推断值对象是实现了Copy Trait的,并且在作用域切换的场景中,直接使用Copy,在官方文档中,关于Copy特别说明了是简单的二进制拷贝。

这里可以有的猜测是,关于字符串,由于不知道运行时会是什么情况,所以无法简单定义Copy的行为,也就是简单的二进制拷贝,需要使用Clone来显式指定有什么样的操作。

官方文档果然是这样说的

如果这里修改一下代码,是可以通过的

let string_obj = "test".to_string();
let string_obj_clone = string_obj.clone();

let handle = std::thread::spawn(move || {
	println!("{:?} : {:#?}", std::thread::current().id(), string_obj_clone);
});

handle.join().unwrap();

println!("{:?} : {:#?}", std::thread::current().id(), string_obj);

运行结果

ThreadId(3) : "test"
ThreadId(2) : "test"

就像值对象的处理方式,只不过这里是显式指定clone,让对象变成2分,各自在不同的作用域

Vec也是同理

自定义结构体

Rust中没有类的概念,struct实际上会比类更抽象一些

Rust设计有意思的地方也来了,可以为结构体快捷的泛化Copy,但是很不幸的是,如果是类似于String这种没有Copy的,仍然要显式实现Clone以及显示调用Clone

可以Copy的结构体

结构体定义如下

#[derive(Debug,Copy,Clone)]
pub struct CopyableObj{
    num1:i64,
    num2:u64
}

impl CopyableObj{
    pub fn new(num1:i64,num2:u64) -> CopyableObj{
        CopyableObj{num1,num2}
    }
}

测试代码如下

let st=CopyableObj::new(1,2);
let handle = std::thread::spawn(move || {
	println!("{:?} : {:#?}", std::thread::current().id(), st);
});

handle.join().unwrap();

println!("{:?} : {:#?}", std::thread::current().id(), st);

结果

ThreadId(3) : CopyableObj {
    num1: 1,
    num2: 2,
}
ThreadId(2) : CopyableObj {
    num1: 1,
    num2: 2,
}

在结构体上使用宏标记 Copy&Clone,Rust编译器就会自动实现在move时的copy动作

不可以Copy的结构体

如果把结构体中的字段换成String

#[derive(Debug,Copy, Clone)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

运行

78 | #[derive(Debug,Copy, Clone)]
   |                ^^^^
79 | pub struct UncopiableObj{
80 |     str1:String
   |     ----------- this field does not implement `Copy`

如果去掉宏标记的Copy

#[derive(Debug)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

运行

80 |     let st=UncopiableObj::new("test".to_string());
   |         -- move occurs because `st` has type `shared_obj::UncopiableObj`, which does not implement the `Copy` trait
81 | 
82 |     let handle = std::thread::spawn(move || {
   |                                     ------- value moved into closure here
83 |         println!("{:?} : {:#?}", std::thread::current().id(), st);
   |                                                               -- variable moved due to use in closure
...
88 |     println!("{:?} : {:#?}", std::thread::current().id(), st);
   |                                                           ^^ value borrowed here after move

由此可知,这里是真的move进入线程的作用域了,外面的作用域无法再使用它

仍然是使用Clone来解决这个问题,但实际上这里可以有2种Clone,1种是默认的直接全部深度Clone,另外1种则是自定义的

先看看Rust自动的Clone

#[derive(Debug,Clone)]
pub struct UncopiableObj{
    str1:String
}

impl UncopiableObj{
    pub fn new(str1:String) -> UncopiableObj{
        UncopiableObj{str1}
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());
    let st_clone=st.clone();

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st_clone);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

运行结果

ThreadId(3) : UncopiableObj {
    str1: "test",
}
ThreadId(2) : UncopiableObj {
    str1: "test",
}

再看看自定义的Clone

#[derive(Debug)]
pub struct UncopiableObj{
    str1:String
}
impl Clone for UncopiableObj{
    fn clone(&self) -> Self {
        UncopiableObj{str1: "hahah".to_string() }
    }
}

pub fn test_uncopiable_struct(){
    let st=UncopiableObj::new("test".to_string());
    let st_clone=st.clone();

    let handle = std::thread::spawn(move || {
        println!("{:?} : {:#?}", std::thread::current().id(), st_clone);
    });

    handle.join().unwrap();

    println!("{:?} : {:#?}", std::thread::current().id(), st);
}

运行结果

ThreadId(3) : UncopiableObj {
    str1: "hahah",
}
ThreadId(2) : UncopiableObj {
    str1: "test",
}

嵌套的结构体

如果字段实现了Copy或者Clone,则结构体可以直接使用宏标记指明,是直接泛化的。

结论

在作用域有变更的场景下,如果实现了Copy的(一般情况是知道内存占用长度的对象),在move语义中,实际上会被Rust编译器翻译成Copy;而没有实现Copy的(一般情况是值不知道运行时内存占用长度的对象),在move语义中,所有权会被直接转移到新的作用域中,原有作用域是无法再次使用该对象的。

所以没有实现Copy的对象,在move语义中,还可以选择显式指定Clone或者自定义Clone。

String的Clone是已经默认实现了的,所以可以直接使用Clone的方法。

扩展结论

move语义定义了所有权的动作,值对象会自动使用Copy,但仍然可以使用borrow,例如在只读的场景中。

由于Rust是针对内存安全的设计,所以在不同的场景下需要选择不同的语义。

例如,没有实现Copy的自定义结构体,

在move语义中,如果实现了Clone,其实是类似于函数式编程的无副作用;如果没有实现Clone,则是直接转移了所有权,只在当前作用域生效;如果想使用类似于c++的指针,则可以使用borrow(不可变或者可变,需要考虑生命周期);还有一种简便的方法,使用Rust提供的智能指针。

这几种在不同作用域中切换指针的方式实际上对应了不同场景的不同指针使用策略,同时也是吸收了函数式、c++智能指针、Java处理指针的方式,就是大杂烩。

原文地址:https://www.cnblogs.com/dopeter/p/14438029.html