MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Rust const和static关键字区别

2022-07-046.8k 阅读

Rust 中的 const

在 Rust 编程语言里,const 关键字用于声明常量。常量是一种具有固定值的标识符,其值在编译时就已经确定,并且在程序的整个生命周期内都不会改变。

const 的基本语法

声明 const 常量的基本语法如下:

const IDENTIFIER: TYPE = EXPRESSION;

其中,IDENTIFIER 是常量的名称,遵循 Rust 的命名规则,一般使用大写字母和下划线的组合,以提高可读性。TYPE 是常量的数据类型,必须显式指定,除非 Rust 编译器能够根据 EXPRESSION 推断出类型。EXPRESSION 是一个表达式,用于计算常量的值,这个表达式必须在编译时就能求值。

例如,声明一个表示圆周率的常量:

const PI: f64 = 3.141592653589793;

在这个例子中,我们声明了一个名为 PIconst 常量,其类型为 f64(64 位浮点数),值为 3.141592653589793

const 常量的特点

  1. 编译时求值const 常量的值在编译时就确定下来,这意味着编译器会在编译阶段对常量表达式进行计算。例如:
const SQUARE_OF_TWO: i32 = 2 * 2;

在这个例子中,2 * 2 这个表达式在编译时就会被计算,结果 4 被赋给 SQUARE_OF_TWO。这一特性使得 const 常量在编译时就能被优化和内联,从而提高程序的性能。

  1. 不可变且无状态:一旦 const 常量被声明,其值就不能被修改。这与变量不同,变量的值可以在程序运行过程中改变。例如:
const COUNT: i32 = 10;
// COUNT = 20; // 这会导致编译错误,因为常量不可变

同时,const 常量不具有任何状态,它们仅仅是一个固定值的别名。

  1. 类型必须明确:在声明 const 常量时,必须显式指定其类型,除非编译器能够根据上下文推断出来。例如:
const ZERO: i32 = 0; // 显式指定类型
const ZERO_AGAIN = 0; // 编译器可以推断类型为 i32

但在某些复杂情况下,为了代码的清晰性和避免潜在的类型错误,建议始终显式指定类型。

  1. 作用域规则const 常量遵循一般的 Rust 作用域规则。它们在声明之后的代码块内可见。例如:
fn main() {
    const LOCAL_CONST: i32 = 5;
    println!("Local const value: {}", LOCAL_CONST);
}
// println!("Local const value: {}", LOCAL_CONST); // 这会导致编译错误,因为 LOCAL_CONST 超出作用域

如果 const 常量在模块级别声明(不在任何函数或代码块内),则在整个模块内可见。如果希望在其他模块中使用该常量,可以使用 pub 关键字将其声明为公共的。例如:

// mod.rs
pub const GLOBAL_CONST: i32 = 10;

// main.rs
mod mod1;
fn main() {
    println!("Global const value: {}", mod1::GLOBAL_CONST);
}

const 常量的使用场景

  1. 数学和物理常量:如前面提到的圆周率 PI,以及一些物理常量,如真空中的光速 C 等。这些常量在整个程序中具有固定的值,使用 const 声明可以确保它们在任何地方都具有一致的值,并且提高代码的可读性和可维护性。
const C: f64 = 299792458.0; // 真空中的光速,单位:m/s
  1. 配置参数:在一些应用程序中,可能需要一些配置参数,如数据库连接字符串、默认端口号等。将这些参数声明为 const 常量,可以方便地在整个程序中使用,并且在需要修改时,只需要在一个地方修改即可。
const DB_CONNECTION_STRING: &str = "mongodb://localhost:27017";
const DEFAULT_PORT: u16 = 8080;
  1. 数组长度和其他固定大小的值:在 Rust 中,数组的长度是类型的一部分,并且必须是编译时已知的常量。使用 const 可以方便地定义数组长度,并且在代码中使用这些常量来创建数组,使得代码更加灵活和易于维护。
const ARRAY_SIZE: usize = 5;
let my_array: [i32; ARRAY_SIZE] = [0; ARRAY_SIZE];

Rust 中的 static

static 关键字在 Rust 中用于声明静态变量。静态变量与 const 常量有一些相似之处,但也存在重要的区别。

static 的基本语法

声明 static 变量的基本语法如下:

static IDENTIFIER: TYPE = EXPRESSION;

const 声明类似,IDENTIFIER 是变量的名称,TYPE 是变量的数据类型,EXPRESSION 是用于初始化变量的表达式。

例如,声明一个静态字符串变量:

static GREETING: &str = "Hello, world!";

在这个例子中,我们声明了一个名为 GREETING 的静态变量,其类型为 &str(字符串切片),值为 "Hello, world!"

static 变量的特点

  1. 静态生命周期static 变量具有 'static 生命周期,这意味着它们在程序启动时创建,在程序结束时销毁。它们存在于整个程序的生命周期内,与任何函数或代码块的执行无关。例如:
static COUNTER: i32 = 0;
fn increment_counter() {
    COUNTER += 1;
    println!("Counter: {}", COUNTER);
}
fn main() {
    increment_counter();
    increment_counter();
}

在这个例子中,COUNTER 是一个静态变量,每次调用 increment_counter 函数时,它的值都会增加,因为它在整个程序生命周期内保持其状态。

  1. 可变与不可变:默认情况下,static 变量是不可变的,但可以使用 mut 关键字将其声明为可变的。例如:
static mut MUTABLE_COUNTER: i32 = 0;
fn increment_mutable_counter() {
    unsafe {
        MUTABLE_COUNTER += 1;
        println!("Mutable Counter: {}", MUTABLE_COUNTER);
    }
}
fn main() {
    increment_mutable_counter();
    increment_mutable_counter();
}

需要注意的是,访问可变的静态变量需要使用 unsafe 块,因为 Rust 编译器无法在编译时保证对可变静态变量的访问是线程安全的。这是因为多个线程可能同时访问和修改可变静态变量,从而导致数据竞争问题。

  1. 存储位置static 变量存储在静态内存区域,这与栈上的局部变量和堆上的动态分配内存不同。静态内存区域在程序启动时就已经分配,并且在程序运行期间一直存在。这种存储方式使得 static 变量可以在不同的函数和代码块之间共享数据。

  2. 初始化表达式static 变量的初始化表达式在程序启动时执行,而不是在编译时。这意味着初始化表达式可以包含一些在运行时才能确定的值,例如函数调用。但需要注意的是,初始化表达式必须是 const 表达式或者是在 const 上下文中允许的表达式。例如:

fn get_initial_value() -> i32 {
    42
}
static VALUE: i32 = get_initial_value();

在这个例子中,get_initial_value 函数在程序启动时被调用,返回值 42 被赋给 VALUE 静态变量。

static 变量的使用场景

  1. 全局共享数据:当需要在整个程序中共享一些数据,并且这些数据需要在程序的整个生命周期内存在时,可以使用 static 变量。例如,在一个多线程应用程序中,可能需要一个全局的配置对象,供所有线程访问。
static CONFIG: AppConfig = AppConfig::new();
struct AppConfig {
    // 配置参数的字段
    db_connection_string: String,
    log_level: String,
}
impl AppConfig {
    fn new() -> Self {
        AppConfig {
            db_connection_string: "mongodb://localhost:27017".to_string(),
            log_level: "INFO".to_string(),
        }
    }
}
  1. 单例模式static 变量可以用于实现单例模式,即确保某个类型在整个程序中只有一个实例。通过将单例实例声明为 static 变量,可以保证其唯一性和全局可访问性。
static INSTANCE: MySingleton = MySingleton::new();
struct MySingleton {
    // 单例的字段
    data: i32,
}
impl MySingleton {
    fn new() -> Self {
        MySingleton { data: 0 }
    }
    fn get_data(&self) -> i32 {
        self.data
    }
    fn set_data(&mut self, new_data: i32) {
        self.data = new_data;
    }
}
fn main() {
    let instance1 = &INSTANCE;
    let instance2 = &INSTANCE;
    assert!(instance1 === instance2);
}

conststatic 的区别本质

  1. 求值时间

    • const:常量的值在编译时就确定,其初始化表达式必须是编译时可求值的常量表达式。这使得 const 常量在编译时就能被优化和内联,从而提高程序的性能。例如 const SQUARE: i32 = 3 * 3;3 * 3 在编译时就被计算为 9
    • static:静态变量的初始化表达式在程序启动时执行,它可以包含一些在运行时才能确定的值,如函数调用。例如 static RESULT: i32 = calculate_result();calculate_result 函数在程序启动时被调用以初始化 RESULT
  2. 可变性

    • constconst 常量一旦声明,其值就不可改变,这是 Rust 语言设计中保证常量安全性和一致性的重要特性。例如 const MAX: i32 = 100;,之后不能再对 MAX 进行赋值操作。
    • static:默认情况下 static 变量是不可变的,但可以通过 mut 关键字声明为可变的。不过,访问可变的 static 变量需要使用 unsafe 块,因为这可能会导致线程安全问题。例如 static mut COUNTER: i32 = 0;,在修改 COUNTER 时需要 unsafe 块。
  3. 生命周期

    • constconst 常量不具有传统意义上的生命周期概念,因为它们在编译时就已经确定,并且在任何地方使用时,其值都是相同的编译时常量。可以理解为它们在整个程序的逻辑中都是“无处不在”的,只要在其作用域内。
    • staticstatic 变量具有 'static 生命周期,即在程序启动时创建,在程序结束时销毁。它们在整个程序的生命周期内存在,并且可以在不同的函数和代码块之间共享数据。
  4. 存储位置

    • constconst 常量的值直接嵌入到使用它们的代码中,不占据额外的运行时内存空间。例如,当在多个地方使用 const PI: f64 = 3.14159; 时,实际上并没有为 PI 单独分配内存,而是在需要使用 PI 的地方直接使用 3.14159 这个值。
    • staticstatic 变量存储在静态内存区域,这是程序运行时内存的一个特定区域,与栈和堆不同。静态内存区域在程序启动时分配,并且在程序运行期间一直存在,用于存储 static 变量的数据。
  5. 类型推断

    • const:虽然在简单情况下 Rust 编译器可以推断 const 常量的类型,但一般建议显式指定类型,以提高代码的清晰度和避免潜在的类型错误。例如 const ZERO = 0; 编译器可以推断为 i32,但更好的写法是 const ZERO: i32 = 0;
    • static:对于 static 变量,必须显式指定类型,因为编译器无法从初始化表达式中可靠地推断出 static 变量的类型。例如 static MESSAGE = "Hello"; 会导致编译错误,正确的写法是 static MESSAGE: &str = "Hello";
  6. 使用场景侧重

    • const:更侧重于声明那些在编译时就确定且不会改变的值,如数学常量、配置参数、数组长度等。这些值在程序的整个生命周期内都不会发生变化,并且通过编译时求值和内联,可以提高程序的性能和安全性。
    • static:主要用于需要在整个程序生命周期内共享数据的场景,例如全局配置对象、单例实例等。虽然 static 变量也可以是不可变的,但它的可变性以及运行时初始化的特点,使其适用于那些需要在程序运行过程中保持状态的情况,尽管需要注意线程安全问题。

代码示例对比

  1. 简单常量与静态变量声明对比
// const 常量声明
const MAX_NUMBER: i32 = 100;
// static 静态变量声明
static CURRENT_NUMBER: i32 = 0;
fn main() {
    println!("Max number: {}", MAX_NUMBER);
    println!("Current number: {}", CURRENT_NUMBER);
}

在这个示例中,MAX_NUMBERconst 常量,其值在编译时确定且不可变。CURRENT_NUMBERstatic 变量,虽然这里它是不可变的,但它在程序启动时初始化,并且可以在运行时通过可变声明和 unsafe 块进行修改。

  1. 使用场景对比 - 配置参数与全局计数器
// const 用于配置参数
const DEFAULT_PORT: u16 = 8080;
// static 用于全局计数器
static mut COUNTER: i32 = 0;
fn increment_counter() {
    unsafe {
        COUNTER += 1;
        println!("Counter: {}", COUNTER);
    }
}
fn main() {
    println!("Default port: {}", DEFAULT_PORT);
    increment_counter();
    increment_counter();
}

这里 DEFAULT_PORT 作为配置参数,使用 const 声明,因为它在整个程序中不会改变。而 COUNTER 作为全局计数器,需要在程序运行过程中保持状态并可以修改,所以使用 static 声明,并且通过 mut 关键字使其可变,但访问时需要 unsafe 块。

  1. 求值时间对比
fn get_runtime_value() -> i32 {
    42
}
// const 不能使用运行时求值的表达式
// const RUNTIME_CONST: i32 = get_runtime_value(); // 编译错误
// static 可以使用运行时求值的表达式
static RUNTIME_STATIC: i32 = get_runtime_value();
fn main() {
    println!("Runtime static value: {}", RUNTIME_STATIC);
}

此示例展示了 const 不能使用运行时求值的表达式,而 static 可以。get_runtime_value 函数在程序启动时为 RUNTIME_STATIC 提供初始化值。

  1. 可变性与线程安全对比
use std::thread;
// const 常量不可变
const FIXED_VALUE: i32 = 10;
// 可变 static 变量
static mut SHARED_COUNTER: i32 = 0;
fn increment_shared_counter() {
    unsafe {
        SHARED_COUNTER += 1;
    }
}
fn main() {
    let mut handles = vec![];
    for _ in 0..10 {
        let handle = thread::spawn(|| {
            increment_shared_counter();
        });
        handles.push(handle);
    }
    for handle in handles {
        handle.join().unwrap();
    }
    unsafe {
        println!("Shared counter value: {}", SHARED_COUNTER);
    }
    // FIXED_VALUE = 20; // 编译错误,const 常量不可变
}

在这个多线程示例中,FIXED_VALUEconst 常量,不可改变。SHARED_COUNTER 是可变的 static 变量,多个线程尝试对其进行修改。由于访问可变 static 变量需要 unsafe 块,并且这里没有采取额外的线程同步措施,可能会导致数据竞争问题。这体现了 const 的绝对不可变性和 static 可变时需要额外注意线程安全的区别。

  1. 存储位置与生命周期对比
fn print_const_address() {
    const LOCAL_CONST: i32 = 5;
    // 这里实际上没有为 LOCAL_CONST 分配内存,只是在使用时嵌入值
    println!("Local const value: {}", LOCAL_CONST);
}
fn print_static_address() {
    static LOCAL_STATIC: i32 = 5;
    println!("Local static address: {:p}", &LOCAL_STATIC);
}
fn main() {
    print_const_address();
    print_static_address();
}

在这个示例中,print_const_address 函数展示了 const 常量不占据额外运行时内存空间,只是在使用处嵌入值。print_static_address 函数展示了 static 变量有自己在静态内存区域的地址,因为它在程序启动时创建并在整个生命周期内存在。

通过以上详细的介绍、特点分析、使用场景探讨以及丰富的代码示例对比,希望能帮助你深入理解 Rust 中 conststatic 关键字的区别。在实际编程中,根据具体需求准确选择使用 const 常量或 static 变量,对于编写高效、安全和可维护的 Rust 程序至关重要。无论是编译时确定且不变的值,还是需要在程序运行期间共享和可能改变状态的数据,正确使用这两个关键字都能让你的代码更加清晰和健壮。