Skip to main content

Command Palette

Search for a command to run...

Rust中的move语义随笔

Published
•3 min read•View as Markdown
Rust中的move语义随笔

在Rust中,"move 语义" 涉及将所有权(ownership)从一个变量转移到另一个变量的概念。当将一个值赋给另一个变量时,默认情况下会发生移动操作,这意味着原始变量不再拥有该值,而新变量成为该值的所有者。这种操作可以防止数据竞争和内存安全问题,并在编译时捕获潜在的错误。Move 语义是 Rust 的核心概念之一,有助于确保代码的安全性和并发性,因此,非常有必要单独拎出来强化强化。


1. 默认移动

默认移动意味着在Rust中,当你将一个值分配给另一个新变量或者说作为参数传递到函数中时这个过程使用的是move而非clone.这代表者该值的所有权也会随之被转移到新的变量中。

考虑下面的代码:

struct Hi {
    message: String,
}

fn main() {
    let mut h1 = Hi {
        message: String::from("Hello!"),
    };

    let h2 = h1;

    println!("h1:{}", h1.message);
    println!("h2:{}", h2.message);
}

image-20240529110455074

上面的代码中,我们创建了一个结构体,在第一步(箭头)中,执行了h2=h1时,此时h1的所有权就已经被移动到了h2中,h1随之失效,当我们再次使用h1时会编译失败。

image-20240529110802394

这些操作都是Rust中默认的。


2. 作为函数参数时

当我们将 struct 实例作为参数传递给函数时,该 struct 被移动到函数中。这意味着传递的原始变量不再有效,不能再次使用。

struct User {
    name: String,
}

fn say(user: User) {
    println!("Hi,{}", user.name);
}

fn main() {
    let user = User {
        name: String::from("马超"),
    };

    say(user);

    println!("user:{:?}", user);
}

我们定义了一个User结构体,并创建了一个函数say,它的参数就是这个结构体。函数功能很简单,就是打印结构体中字段的内容。

在主函数中,构建了User结构体之后将它传递给函数使用。这之后,我们尝试打印这个结构体,但是编译无法通过,这是因为我们的结构体的所有权已经转移到了函数中,不能被再次使用。

那么,如果我们还是需要继续这个结构体怎么办呢?可以通过使用引用传递参数的方式,或者说使用clone,这样可以避免所有权的转移。

  • 传递引用
#[derive(Debug)]
struct User {
    name: String,
}

fn say(user: &User) {
    println!("Hi,{}", user.name);
}

fn main() {
    let user = User {
        name: String::from("马超"),
    };

    say(&user);

    println!("user:{:?}", user);
}
  • clone
#[derive(Debug, Clone)]
struct User {
    name: String,
}

fn say(user: User) {
    println!("Hi,{}", user.name);
}

fn main() {
    let user = User {
        name: String::from("马超"),
    };

    say(user.clone());

    println!("user:{:?}", user);
}

3. 返回中的转移

在Rust中,当一个函数返回一个结构体时,结构体的所有权会从函数的作用域转移到调用作用域。这意味着结构体从函数的作用域移动到调用作用域。以下是一个示例来演示这种行为:

#[derive(Debug)]
struct MyStruct {
    data: String,
}

fn create_struct() -> MyStruct {
    let data = String::from("Hello");
    MyStruct { data }
}

fn main() {
    let my_struct = create_struct();
    println!("{:?}", my_struct); // 这行代码可以正常工作

    // 下面这行代码会导致编译错误,因为my_struct已经被移动
    // println!("{:?}", my_struct);
}

在这个示例中,create_struct函数创建一个MyStruct实例并返回它。当在main函数中将my_struct赋值为create_struct的返回值时,结构体的所有权转移到了my_struct。因此,你可以打印my_struct一次,但如果在结构体已经被移动后尝试再次使用my_struct,将会导致编译错误。

如果您需要在函数调用后继续使用结构体,可以返回结构体的引用而不是结构体本身。这样,所有权不会被转移,原始结构体仍然有效。


4. Move关键字

move关键字会显式的触发所有权的强制转移。注意,这里说的是转移、而不是复制。

举个move在闭包中使用的栗子:

fn do_sth() {
    let mut x = String::from("Hello");
    let mut y = String::from("World");

    let closure = move || {
        println!("{} {}", x, y);
    };

    closure();

    // println!("x :{}", x);
}

fn main() {
    do_sth();
}
  1. 定义 do_sth 函数:

    • 函数 do_sth 定义了两个可变字符串变量 x 和 y,分别初始化为 "Hello" 和 "World"。
  2. 创建并立即执行带 move 的闭包:

    • 在 do_sth 函数内部,定义了一个匿名闭包(通过 || { ... } 形式)并赋值给了变量 closure。

    • 闭包前面的 move 关键字是重点。它指示闭包在其被调用时,会“移动”捕获的变量(在这里是 x 和 y)的所有权进入闭包内部,而不是仅仅借用它们。这意味着一旦闭包执行完毕,原始变量 x 和 y 将不再可用,因为它们的所有权已经被转移给了闭包。

    • 闭包体内的代码是打印 x 和 y 的内容。

  3. 调用闭包:

    • 通过 closure(); 来执行这个闭包,输出结果为 "Hello World"。
  4. 注释掉的 println!:

    • 之后有一行被注释掉的代码 // println!("x :{}", x);。由于闭包使用了 move,变量 x 和 y 的所有权已经被转移给闭包,所以在闭包执行后,尝试访问 x 会引发编译错误,因为 x 已不再有效。
  5. 在 main 函数中调用 do_sth:

    • main 函数简单调用了 do_sth(),执行上述过程。最终程序输出 "Hello World",且不会尝试访问已被移动的变量。

5. Copy trait

在Rust中,某些类型是默认实现了Copy trait的,这意味着他们的值在被移动是发生的是复制,源值将保留不变。这些类型包括了诸如整数、浮点、布尔、字符等基本数据类型;

fn main() {
    let x = 3;
    let y = x;

    println!("x is {}", x);
}

上面的代码中,虽然我们执行了y=x的赋值操作,但由于基本类型实现了Copy trait,所以源值x还是3;

与之对比的是没有实现Copy trait 的String类型;

fn main() {
    let x = 3;
    let y = x;

    println!("x is {}", x);
}

image-20240529164255374

在这里, 操作y = x 执行后 x被移动 。 x 移动后不再有效。并且编译报错信息也给出了提示,该类型没有实现Copy trait;

注意,任何完全由复制类型组成的类型也是具有Copy属性的。 例如:

#[derive(Debug, Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let origin = Point { x: 1, y: 2 };

    let point = origin;

    println!("origin is {:?}", origin);
}

上述例子中,由于结构体中的字段都是i32的整型,而整型默认实现了Copy trait,此时这个结构体也默认拥有了该特性,当我们执行了let point = origin;之后打印origin的值依旧是原来的结构体,说明它执行了复制操作。


6. 小结

Rust中采取移动的一些优势:

  • 性能:移动不需要值得副本,因此速度很快。

  • 内存:如果不移动,则每当传递或者重新分配值时,就需要更多得内存分配;

  • 唯一性:移动可以强制保持所有权得惟一,确定在给定时间只有一个变量拥有该值,也为内存安全提供了保证;

More from this blog

[c++]浅谈函数重载解析和不明确匹配

先简单回顾下函数重载需要借助函数中的哪些属性来作为是否是重载函数的依据: 函数的参数类型 函数的参数个数(参数长度) 但是,这只是我们定义函数重载概念中的一部分,编译器在实际的函数重载过程中,还会进行一系列的二次判定和相对繁琐的规则匹配,这, 就是本文的内容。 重载决策 对于非重载函数,也就是具有唯一名称的函数嘛,只有一个函数可能与调用匹配,可以说,这样的函数一调一个准,因为唯一,所以没有选择,因为没有选择,所以无需过多的匹配流程。所以这种情况下,调用该函数只有两种 结果: 匹配 ...

Jan 21, 20252 min read6
[c++]浅谈函数重载解析和不明确匹配

提问的智慧-转载

更新日志 2022-9-15 午时 于 杭州 在原文的基础结构上调整了文章目录结构 简单进行了一下md的格式化 修改封面配图 引 ​ 在黑客世界里,当提出一个技术问题时,你能得到怎样的回答?这取决于挖出答案的难度,同样取决于你提问的方法。本指南旨在帮助你提高发问技巧,以获取你最想要的答案。 首先你必须明白,黑客们只偏爱艰巨的任务,或者能激发他们思维的好问题。 如若不然,我们还来干吗?如果你有值得我们反复咀嚼玩味的好问题,我们自会对你感激不尽。好问题是激励,是厚礼,可以提高我们的...

Jan 19, 20252 min read2
提问的智慧-转载

邪恶的非常量全局变量

在编程过程中,避免使用全局变量是一个良好的编程实践建议。当然,这里的全局变量主要是指 非常量全局变量; 尽管在小型项目中,这一点似乎看起来人畜无害,但是在大型项目中往往会出现很多问题。 新手程序员往往比较喜欢使用大量的全局变量,因为这样使用起来方便直接,特别是当设计到不同函数的多次调用传递参数时。 若无特别说明,本文后续内容中提到的全局变量均指 非常量全局变量。 全局变量的潜在危险 到目前为止,全局变量危险的最大原因是因为他们的值可以在任何地方被任何调用的函数更改,并且程序员没有简单的方法...

Jan 12, 20252 min read4
邪恶的非常量全局变量

详解设计模式|单例的进化之路

概念 单例模式(Singleton Pattern)是设计模式中一个重要的模式之一,是确保一个类在任何情况下都绝对只有一个实例。单例模式一般会屏蔽构造器,单例对象提供一个全局访问点,属于创建型模式。 根据初始化时间的不同,可以将单例模式分为两类: 饿汉式单例 懒汉式单例 当然,除了上面的两个分类之外,处于对性能、安全等方面的考量,单例模式还演化出了各种实现版本,每一种版本的演进,都是单例的一次**进化与升级,**下面就来看看单例模式的进化之路上都经历了哪些挑战与对抗。 饿汉式单例 饿...

Jan 5, 20255 min read4
详解设计模式|单例的进化之路

SpringBoot Web开发精解

SpringMVC基础回顾 当在 Spring Boot 中引入 Web 模块时,SpringBoot 会帮我们自动配置 Web 相关的组件,其中 Spring MVC 便是最重要的部分。 组件介绍 上图是 SpringMVC 的工作原理图。先介绍一下原理图中涉及的各个组件。 DispatcherServlet:前端控制器,是整个流程的控制中心,由它调用其他组件处理用户请求。 HandlerMapping:处理器映射器,负责根据用户请求的URL找到相应的Handler处理器。 Handl...

Jan 5, 20256 min read5
SpringBoot Web开发精解

Xayla

26 posts

Simple is efficiency!