zoukankan      html  css  js  c++  java
  • rust随笔

    # 第二章 语言精要

    ​ 好读书,不求甚解;每有会意,便欣然忘食。

    **动手,动手,动手!!!**

    ## 语句与表达式

    Rust 中语法可以分成两大类:语句 statement 和表达式 expression。语句是指要执行的一些操作和产生副作用的表达式。

    表达式主要用于计算求值。

    语句又分为两种:声明语句 Declaration statment 和表达式语句 Expression statement

    - ​ 声明语句,用于声明各种语言项,包括声明变量、静态变量、常量、结构体、函数等,以及通过extern 和use关键字引入包和模块。
    - 表达式语句,特指以分号结尾的表达式。此类表达式求值结果将会被舍弃,并总是返回单元类型()。

    ```rust
    //extern crate std;
    // use std::prelude::v1::*;
    fn main(){
    pub fn answer() -> () {
    let a = 40;
    let b = 2;
    assert_eq!(sum(a,b), 42);
    }
    pub fn sum(a: i32, b: i32) -> i32{
    a + b
    }
    answer();
    }
    ```

    ​ Extern cate std; 和 use std::prelude::v1::*; 是声明语句,他们并不需要求值,只是用来引入标准库包以及prelude模块。

    ​ rust 会为每个crate都自动引入标准库模块,除非使用#[no_std]属性明确指定了不需要标准库。

    ​ 使用fn关键子定义了函数,函数answer没有输入参数,并且返回类型为**单元类型()**。单元类型拥有唯一的值,就是它本身,为了描述方便,将该值称为**单元值**。单元类型的概念来自Ocmal,他表示“没有什么特殊的值”。所以,这里将单元类型作为函数返回值,就是表示该函数无返回值。通常没有返回值的函数默认是不需要在函数签名中指定返回类型的。

    ​ 使用了let声明变量assert_eq! 则是宏语句,踏实rust提供的断言,允许判断给定的两个表达式求值结果是否相同。像这种名字以叹号结尾,并且可以像函数一样被调用的语句,在rust中叫做**宏**。

    ​ rust编译器在解析代码的时候,如果碰到分号,就会继续往后面执行;如果碰到语句,则执行语句;如果碰到表达式,则会对表达式求值,如果分号后面什么都没有,就会补上单元值()。

    ​ 当遇到函数的时候,会将函数体的花括号识别为块表达式 Block expression。块表达式是由一对花括号和系列表达式组成的。他总是返回块中最后一个表达式的值。

    ​ 从这个角度看,可以讲rust看作一切皆表达式。由于当分号后面什么都没有时自动补单元值()的特点。我们可以将rust中的语句看作计算结果均为()的特殊表达式。而对于普通表达式来说,则会得到正常的求值结果。

    ## 变量与绑定

    通过let关键字来创建变量,这是rust函数从函数式语言中借鉴的语法形式。let创建的变量一般称为绑定 binding。它表明了标识符 identifier 和值value 之间建立的一种关联关系。

    ### 位置表达式和值表达式

    rust中的表达式一般分为位置表达式 place expression 和值表达式 value expression。在其他语言中,一般叫做左值 lvalue和右值rvalue。

    位置表达式就是表示内存位置的表达式。分类有:

    - 本地变量
    - 静态变量
    - 解引用 (*epr)
    - 数组索引(expr[expr])
    - 字段引用(exp.field)
    - 位置表达式组合

    通过位置表达式可以对某个数据单元的内存进行读写。主要进行写操作,这也是位置表达式可以被赋值的原因。

    值表达式一般只引用了某个存储单元地址中的数据。它相当于数据值,只能进行读操作。

    从语义角度来说,位置表达式代表了持久性数据,值表达式代表了临时数据。位置表达式一般有持久的状态,值表达式要么是自面量,要么是表达式求值过程中创建的临时值。

    表达式的求值过程在不同的上下文中会有不同的结果。求值上下文也分为位置上下文 place context 和值上下文 value context。

    下面几种表达式属于位置上下文:

    - 赋值或者复合赋值语句左侧的操作数。
    - 一元引用表达式的独立操作数。
    - 包含隐式借用(引用)的操作数。
    - match 判别式或者let绑定右侧在使用ref模式匹配的时候也是位置上下文。

    除了上述几种情况,其余表达式都属于值上下文。值表达式不能出现在位置上下文中。

    ```rust
    pub fn temp() -> i32{
    return 1;
    }
    fn main() {
    let x = &temp();
    temp() = *x; //e0070: invalid left-hand size expression
    }
    ```

    在main函数中,使用temp函数调用放到赋值语句左边的位置上下文中,此时编译器会报错。因为temp函数调用是一个无效的位置表达式,它是值表达式。

    ### 不可变绑定与可变绑定

    使用let关键字声明的位置表达式默认不可变,为不可变绑定。

    ```rust
    fn main() {
    let a = 1;
    // a = 2; //immytable and error
    let mut b = 2;
    b = 3; // mutable
    }
    ```

    变量a默认是不可变绑定,对其重新复制后编译器会报错。通过mut关键字,可以声明可变位置表达式,即可变绑定。可变绑定可以正常修改和赋值。

    从语义上来说,let声明的不可变绑定只能对相应的存储单元进行读取,而let mut 声明的可变绑定则是可以对相应的存储单元进行写入。

    ### 所有权与引用

    当位置表达式出现在值上下文中,该位置表达式将会把内存地址转移到另一个位置表达式,这是所有权的转移。

    ```rust
    fn main(){
    let place1 = "hello";
    let place2 = "hello".to_string();
    let other = place1;//Copy
    println!("{:?}", place1);
    let other = place2;// Move
    println!("{:?}", place2); //place2 value used here after move
    }
    ```

    place1是绑定的是一个字符串字面量,对于字符串字面量来说,和基本类型都一样都有Copy trait,在赋值操作时执行的是Copy操作,不回所有权的移动,现在出现在赋值操作符右侧,这里let other = place1; 会执行place1的拷贝操作,在执行之后,打印place1仍然是可以的。

    而place2是String类型,没有Copy triat,这里赋值后,会发生所有权的移动。接着打印place2会出现错误,place2 value used here after move

    **在语义上,每个变量实际上都拥有该存储单元的所有权,这种转移所有权地址的行为就是所有权ownership的转移,在rust中称为移动 move 语义。那种不转移的情况。实际是上是一种复制copy语义。Rust没有GC, 所以完全依靠所有权来管理内存管理。**

    在日常开发中,有时并不需要转移所有权。Rust提供**引用操作符&**, 可以直接获取表达式的存储单元地址,即内存地址。可以通过该内存位置对存储进行读取。

    ```rust
    fn main(){
    let a = [1,2,3];
    let b = &a;
    println!("{:p}", b); // 0x7fffcbc067704
    let mut c = vec![1, 2, 3];
    let d = &mut c;
    d.push(4);
    println!("{:?}", d);// [1, 2, 3, 4]
    let e = &42;
    assert_eq!(42, *e);
    }
    ```

    上面定义了固定长度数组a,并使用引用操作符&取得a的内存地址,赋值给b。这种方式不会引起所有权的转移,因为使用引用操作符已经将赋值表达式右侧变成了位置上下文,他只是共享内存地址。通过println! 宏指定{:p}格式,可以打印b的指针地址。也就是内存地址。

    通过let mut 声明了动态长度数组c,然后通过&mut 获取c的可变引用,赋值给d。

    **注意,要获取可变引用,必须先声明可变绑定。**

    对于字面量42来说,其本省属于值表达式。通过引用操作符,相当于值表达式在位置上下文中进行求值,所以编译器会为&42创建一个临时值。

    最后,通过解引用操作符*将引用e中的值取出来,以供assert_eq!宏使用。

    从语义上来说,不管&a还是&mut c;都相当于对a和c所有权的借用,因为a和c还依旧保留他们的所有权,所以引用也被称为借用。

    ## 函数与闭包

    ### 函数定义

    main函数,它代表程序的入口。对于二进制可执行文件来说,main函数必不可少。对于库函数来说,mian函数就没有必要。

    函数是通过关键字fn定义的。

    ```rust
    pub fn fizz_buzz(num: i32) -> String {
    if num % 15 == 0 {
    return "fizzbuzz".to_string();
    }else if num % 3 == 0 {
    return "fizz".to_string();
    }else if num % 5 == 0 {
    return "buzz".to_stirng();
    }else{
    return num.to_string();
    }
    }

    fn main(){
    assert_eq!(fizz_buzz(15), "fizzbuzz".to_string());
    assert_eq!(fizz_buzz(3), "fizz".to_string());
    assert_eq!(fizz_buzz(5), "buzz".to_string());
    assert_eq!(fizz_buzz(13), "13".to_string());
    }
    ```

    使用fn关键字定义了fizz_buzz函数,其函数签名pub fn fizz_buzz(num: i32) -> String 清晰地反映了函数的类型约定:传入i32类型,返回Srtring类型。**Rust编译器会严格遵守此类型的契约,如果传入或返回的不是约定好的类型,则会编译时出错。**

    return表达式用于退出一个函数,并返回一个值。但是如果return后买呢没有值,就会默认返回单元值。

    使用了to_string 方法,他将表达式的求值结果转换成Siting类型。

    ### 作用域与生命周期

    Rust语言的作用域是静态作用域,即词法作用域 Lexical scope。由一对花括号来开辟作用域,其作用域在词法分析阶段就已经确定了,不回动态该改变。

    ```rust
    fn main(){
    let v = "hello world!";
    assert_eq!(v, "hello world!");
    let v = "hello Rsut!";
    assert_eq!(v, "hello Rsut!");
    {
    let v = "hello Wolrd!";
    assert_eq!(v, "hello World!");
    }
    assert_eq!(v, "hello Rust!");
    }
    ```

    先定义了变量绑定v,赋值为hello world!,然后通过断言验证其值。再次通过let声明变量绑定v,赋值为hello Rust!这种定义同名变量的做法叫做**变量遮蔽 variable shadow**。但是最终的变量v的值是由第二个变量定义所决定的。

    在词法作用域内部使用花括号开辟新的词法作用域后,两个作用域是相互独立的。在不同的词法作用域内声明的变量绑定,拥有不同的生命周期 lifetime。 **变量绑定的生命周期总是遵循这样的规律:从使用let声明创建变量绑定开始,到超出词法作用域的范围时结束。**

    ### 函数指针

    在rust 中,**函数为一等公民**。这意味,函数自身既可以作为函数的参数和返回值使用。

    ```rust
    pub fn math(op: fn(i32, i32) -> i32, a: i32, b: i32) -> i32 {
    op(a, b)
    }
    fn sum(a: i32, b: i32) -> i32 {
    a + b
    }
    fn product(a: i32, b: i32) -> i32 {
    a * b
    }
    fn main() {
    let a = 2;
    let b = 3;
    assert_eq!(math(sum, a, b), 5);
    assert_eq!(math(product, a, b), 6);
    }
    ```

    定义了函数math, 其函数签名的第一个参数为fn(i32, i32) -> i32 类型, 这在rust中时函数指针 fn pointer类型。

    这里直接使用函数的名字作为函数指针。

    函数也可以作为返回值使用。

    ```rust
    fn is_true() -> bool {
    true
    }
    fn true_maker() -> fn() -> bool {
    is_true
    }
    fn main(){
    assert_eq!(true_maker()(), true);
    }
    ```

    定义了函数true_maker,返回fn() -> bool 类型,其函数体内直接将is_true函数指针返回。注意此处也使用了函数名字作为函数指针,如果加上括号,就会调用该函数。

    在main函数的断言中,true_maker()()调用相当于(true_maker() ) ()。 首先调用true_maker(), 会返回is_true函数指针;然后再调用is_true()函数,最终得到true。

    ### CTEE机制

    rust编译器可以像C++,拥有编译时函数执行 compile - time function execution , CTTE的能力。

    ```rust
    #![feature(const_fn)]
    const fn init_len() -> usize {
    return 5;
    }
    fn main(){
    let arr = [0; init_len()];
    }
    ```

    使用了const fn 来定义函数init_len, 该函数返回一个固定值5. 并且在main函数中,通过[0; N]这种形式来初始化初始值为0, 长度为N的数组,其中N时由调用函数init_len来求得的。

    Rust中固定的数组必须在编译期就知道长度,否则会编译出错。所以函数init_len必须在编译器求值。这就是CTTE的能力。

    使用const fn 定义的函数,必须可以确定值,不能存在歧义。与定义fn函数的区别在于, const fn 可以强制编译器在编译期执行函数。其中关键字const一般用于定义全局常量。

    除了const fn,官方还在实现const generics特性。 支持const generics 特性,将可以实现类似impel < T, const N: usize> Foo for [T:N] {... } 的代码。可以为所有长度的数组实现trait Foo.

    Rust中的CTTE是由miri来执行的。miri是一个MIR解释器,目前已经被集成到Rust编译器rustc中。rust 编译器目前可以支持的常量表达式有:字面量、元组、数组、字段结构体、枚举、只包含单行代码的块表达式、范围等。

    ### 闭包

    闭包也叫做匿名函数。闭包的特点:

    - 可以像函数一种被调用。
    - 可以捕获上下文环境中的自由变量。
    - 可以自动推断输入和返回的类型。

    ```rust
    fn main() {
    let out = 42;
    // fn add(i:i32, j: i32) -> i32 { i + j + out }
    fn add(i: i32, j: i32) -> i32 { i + j}
    let closure_annotated = |i: i32, j:i32| -> i32 { i + j + out };
    let closure_inferred = |i, j| i + j + out;
    let i = 1;
    let j = 2;
    assert_eq!(3, add(i, j));
    assert_eq!(45, closure_annotated(i,j));
    assert_eq!(45, closure_inferrend(i,j));
    }
    ```

    闭包函数有一个重要的区别,就是闭包可以捕获外部变量,而函数不可以。

    闭包也可以作为函数参数和返回值。

    ```rust
    fn closure_math<F: Fn() -> i32 >(op: F) -> i32 {
    op()
    }
    fn main() {
    let a = 2;
    let b = 3;
    assert_eq!(clousre_math(|| a + b), 5);
    assert_eq!(clousre_math(|| a * b), 6);
    }
    ```

    定义了函数closure_math,其参数是一个泛型F, 并且泛型受Fn() -> i32 trait的限定,代表函数只允许实现Fn()->i32 trait的类型作为参数。

    **Rust中闭包实际上就是一个匿名结构体和trait来组合实现的。**

    闭包也可以作为返回值,

    ```rust
    fn two_times_impl() -> impl Fn(i32) -> i32 {
    let i = 2;
    move |j| j * i
    }
    fn main() {
    let result = two_times_impl();
    assert_eq!(result(2), 4);
    }
    ```

    在函数定义时并不知道具体的返回类型,但是在函数调用时,编译器会推断出来。这个过程也是零成本抽象的,一切都发生在编译器。

    **在函数two_times_imple中最后返回闭包时使用了move关键字,这是因为在一般情况下,闭包默认会岸引用捕获变量。如果将此闭包返回,则引用也会跟着返回变量i的引用,也将成为悬垂指针。使用move关键字,将捕获变量i的所有权转移到闭包中,就不会按引用进行捕获变量,这样闭包才可以安全地返回。**

    ## 流程控制

    流程控制表达式

    ### 条件表达式

    表达式一定会有值,所以if表达式的分支必须返回一个类型的值才可以。if表达式的求值规则和块表达式一致。

    ```rust
    fn main() {
    let n = 13;
    let big_n = if ( n < 10 && n > -10 ){
    10 * n
    }else {
    n / 2;
    };
    assert_eq!(big_n, 6);
    }
    ```

    ### 循环表达式

    rust中包括三种循环表达式: while, loop和for ... in 表达式。

    ```rust
    fn main() {
    for n in 1..101 {
    if n % 15 == 0 {
    println!("fizzbuzz");
    }else if n % 3 == 0{
    println!("fizz");
    }else if n % 5 == 0 {
    println!("buzz");
    }else {
    println!("{}", n);
    }
    }
    }
    ```

    For .. in 表达式本质上是一个迭代器,其中1..101 是一个range类型,它是一个迭代器。for的每一次循环都从迭代器中取值,当迭代器中没有值的时候,for循环结束。

    ```rust
    fn while_true(x: i32) -> i32 {
    while true {
    return x + 1;
    }
    }
    fn main() {
    let y = while_true(5);
    assert_eq!(y, 6);
    }
    ```

    其中while循环条件使用了硬编码true,目的是实现无限循环。这种看似非常正确的代码会引起rust编译器报错。

    错误提示称while true 循环块返回的是单元值,而函数while_true返回值是i32, 所以不匹配。但是在while true 循环中使用了return关键字,应该返回i32类型才行。为什么会报错呢?

    这是因为rust编译器在对while循环做流分析 Flow sensitive 的时候,不会检查循环条件,编译器会认为while循环条件可真可假,所以循环体里的表达式也会被忽略,此时编译器只知道while true 循环返回的是单元值,而函数返回的是i32。其他情况一概不知。这一切都是因为CTEE功能的限制,while条件表达式无法作为编译器常量来使用。

    ```rust
    fn while_true(x: i32) -> i32 {
    while true {
    return x + 1;
    }
    x
    }
    ```

    在while_true函数的最后一行加了x变量,这是为了让编译器以为返回的类型是i32类型,但实际上,程序在运行以后,将永远在while true 循环汇中执行。

    ### match 表达式与模式匹配

    ```rust
    fn main() {
    let number = 42;
    match number {
    0 => println!("Origin"),
    1...3 => println!("All"),
    |5|7|13 => println!("Bad luck"),
    n @ 42 => println!("Answer is {}", n),
    _ => println!("Common"),
    }
    }
    ```

    在rust语言中,match分支使用了模式匹配 pattern matching 技术。模式匹配在数据结构字符串中经常出现,比如在某个字符串中找出与该字符串相同的所有子串。在编程语言中,模式匹配用于判断类型或者值是否存在可以匹配的模式。模式匹配在很多函数式语言中已经被广泛应用。

    在rust语言中,match分支左边就是模式,右边就是执行代码。模式匹配同时也是一个表达式,与if表达式类似,所有分支必须返回一个类型。但是左侧的模式可以是不同的。

    使用操作符@可以将模式中的值绑定给一个变量。供分支右侧的代码使用,这类匹配叫做绑定模式 Bindingmode。match表达式必须穷尽每一种可能,所以一般情况下会使用通配符_来处理剩余的情况。

  • 相关阅读:
    JavaScript等比例缩放图片
    乐器的研究
    乐器的研究
    单位的理解
    单位的理解
    那些狗,那些人
    Opencv+Zbar二维码识别(标准条形码/二维码识别)
    二维码解码器Zbar+VS2012开发环境配置
    条形码、二维码的区别和组成结构介绍
    Caffe-Windows下遇到过的问题、技巧、解决方案
  • 原文地址:https://www.cnblogs.com/Davirain/p/13193302.html
Copyright © 2011-2022 走看看