Rust中Move语义下的Copy与Clone

问题

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

测试场景

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

测试对象没有去指定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就会是FnOnceui

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

这里虽然有了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处理指针的方式,就是大杂烩。

相关文章
相关标签/搜索