从 Typescript 角度学习 Rust

背景

我写这篇文章时,是去年写的,就是全面放开以后(羊了个羊),公司就我没有中招,闲着也是闲着,找点乐子充实一下无聊生活,都说 Rust 很火,前端基建都拥抱它。

为什么写完没有发了,电脑坏了,后面修复了,就一直忙工作,这不快过年了,又闲的慌,把它找出来,整理一下,也是一个学习总结吧。

其实一开始我只是把它当作一个学习对比心态来写这个文章,在今年参加一个 c++ 考试改变我这个心态。语言是相通的,它们是有共性的,到考试前我也没有时间看书,去真正了解 c++ 语法特性,考前做了几套模拟试卷,考试前一晚还在加班,空余时间就问了 ChatGPT 几个关于 c++ 语言层面的问题,它告诉我一些大概的写法和简单概念。最终考试 77,最后一道大题15分没做,要我打开一个文件,对文件里的内容进行排序然后打印出来。这要我用 Nodejs 写,很容易,要我用 c++ 写就不是那么容易了。

为什么能这么轻松应对考试,那是因为我写了这篇文章,一个一个语法去实践,去类比。虽然 c++Rust 很多地方不一样。只要你有语言基础(特别有静态类型的语言),学其他语言都能快速入门。

废话不多说,赶紧上车吧!

开发环境搭建

古人云:"工欲善其事,必先利其器"。

学习一门新语言,先要搭建必要语言运行环境。

前往 www.rust-lang.org/install.htm... 并按照说明安装 Rust

Windows 需要安装 Microsoft C++ Build Tools,也不是必须,如果你有安装 Visual Studio 可以跳过。(注意 :不是 vs code
我是 Windows, 下载 rustup-init.exe 文件。

打开它,根据自己选择:

  1. Proceed with installation (default)
  2. Customize installation
  3. Cancel installation

选择 1,一路回车就行了。出现 Rust is installed now. Great! 安装完成了。

被安装在 C:\Users\{PC}\.cargo\bin 里:

text 复制代码
cargo.exe
cargo-clippy.exe
cargo-fmt.exe
cargo-miri.exe
clippy-driver.exe
rls.exe
rustc.exe
rustdoc.exe
rustfmt.exe
rust-gdb.exe
rust-gdbgui.exe
rust-lldb.exe
rustup.exe

打开命令行:

bash 复制代码
rustc --version

然后就会看到:

bash 复制代码
rustc 1.67.1 (d5a82bbd2 2023-02-07)

如果有表示你安装成功了,如果没有,把 C:\Users\{PC}\.cargo\bin 添加到环境变量 PATH 里,如果没有生效,重启一下即可。

如果一切正确但 Rust 仍不能使用,建议重装个系统,实在不行换个电脑。再不行就放弃治疗,别玩了,浪费生命。

前端开发一般都用 Nodejs

在官网下载 nodejs.exe 或者使用 nvm 安装。

第一件事就是查看版本:

bash 复制代码
node -v
npm -v

那对应 Rust

bash 复制代码
rustc -V
cargo -V

注意 :node和npm 使用是 -v 小写,rustc和cargo 使用是 -V 大写。

通过 rustc.rs 文件编译成执行文件 .exe,它是是 Rust 的编译器,类似于 Typescript 中的 tsc,把 .ts 编译成 .js

cargo 就是是 Rust 的包管理器,类似于 Nodejs 中的 npm

对于其他系统安装,这里不展开,自行查阅官网的文档安装

Hello World

网上流行:当我们学习一门编程语言时,都是从 Hello, World! 开始。

学习 Rust 也不例外。

我使用 VS code 作为开发工具。

先用 Typescript 实现,然后使用 ts-node 运行:

需要建立一个项目:

hello.ts

ts 复制代码
function main() {
    console.log("Hello, world!");
}

(() => {
   main(); 
})()

命令行运行:

bash 复制代码
npx tsc hello.ts

需要建立一个项目:

hello.rs

rs 复制代码
fn main() {
    println!("Hello, world!");
}

命令行运行:

bash 复制代码
rustc hello.rs
./hello # Windows 上需要输入 `.\hello`

很多语言都用一个入口函数(main 函数)。你发现平常写 js 都没有这种讲究,实际你已经在使用入口函数,只不过方式不同,现代框架里面都是 SPA 单入口形式,Webpack 默认配置打包就是 main.js

有了一门语言的基础,学起其他语言来相对比较轻松,学习一门语言基础无非就是:

  • 数据类型与数据结构
  • 变量定义与常量
  • 条件判断与循环语句
  • 字符串操作与数字运算符和运算
  • 函数和数组(元组)
  • 类(对象、结构体)
  • 模块(包管理)
  • 错误处理

以上列举一些基础。相当于是学习新语言的模板。

特此说明:以下所有写法尽量靠近 Rust 语法。所有的 Rust 都是包裹在 fn main() {} 里,后面除了特殊要求写明外。
注意

  1. Rust 一定要注意分号(;)的使用,在 JSer 界是流派(要分号流,不要分号流),但在 Rustacean 界强制使用。
  2. Rust 一定要注意引号(")的使用,在 JSer 界是流派(单引号流,双引号流),但在 Rustacean 界强制使用双引号。

打印输出日志

  1. 基本写法:

Typescript

ts 复制代码
console.log('hello world!!');

Rust

rs 复制代码
println!("hello world!!");
  1. 占位符写法

Typescript

ts 复制代码
console.log('hello world!! %d', 100_000);
// 多个占位符
console.log('%d hello world!! %d', 100_000, 100_000);

Typescript 占位符的种类:

  • %s 类型是 String 字符串
  • %d or %i 类型是 Integer 整数
  • %f 类型是 Float 浮点数
  • %o 类型是 Object 对象

Typescript 里使用 %[s|d|f|o] 作为占位符,如果需要输出 %[s|d|f|o] 占位符,需要 %%[s|d|f|o] 这样书写。

Rust

rs 复制代码
println!("hello world!! {}", 100_000);
// 多个占位符
println!("{} hello world!! {}", 100_000, 100_000);
// 更简单写法
println!("{0} hello world!! {0}", 100_000);
  1. 打印基本变量值

Typescript

ts 复制代码
let num: number = 100_000;
console.log(`number: ${num}`);

Rust

rs 复制代码
let num: u32 = 100_000;
println!("number: {num}");

Rust 里使用 {} 作为占位符,如果需要输出 {} 字符串,需要 {{}} 这样书写。

  1. 打印复杂变量值

Typescript

ts 复制代码
let num: number = 100_000;
console.log(`number: ${num}`);

Rust

rs 复制代码
let num: u32 = 100_000;
println!("number: {num}");

数据类型

TypescriptJavaScript 整体书写语法层面,并没有太大差别,最大区别就是类型系统。 JavaScript 就被戏称 AnyScript

概念:

  • 数据类型是指编程语言中用来定义数据的分类或类型。
  • 数据结构是指在计算机中组织和存储数据的方式和方法。数据类型是数据的分类,而数据结构是数据的组织方式。

常见的数据类型包括:

  1. 布尔类型(Boolean):表示真(true)或假(false)的值。
  2. 数字类型(Number):表示整数或浮点数。
  3. 字符串类型(String):表示文本。
  4. 数组类型(Array):表示按顺序排列的值的集合。
  5. 对象类型(Object):表示键值对的集合。
  6. 函数类型(Function):表示可执行的代码块。
  7. 空类型(Null):表示一个空值。
  8. 未定义类型(Undefined):表示一个未定义的值。

基本所有语言都包含这些类型,只是叫法不一样而已,有些有更多扩展类型。

常见的数据结构包括:

  1. 数组(Array):按顺序存储多个值的集合。
  2. 链表(Linked List):通过节点和指针连接起来的数据结构。
  3. 栈(Stack):遵循后进先出(LIFO)原则的数据结构。
  4. 队列(Queue):遵循先进先出(FIFO)原则的数据结构。
  5. 树(Tree):由节点和边组成的层次结构。
  6. 图(Graph):由节点和边组成的非线性数据结构。
  7. 哈希表(Hash Table):使用键值对存储数据的数据结构。
  8. 堆(Heap):按照特定规则组织的完全二叉树。

数据结构定义了数据如何组织和存储。有些语言对这些数据结构做了封装直接提供使用。几乎所有语言都提供了数组这种底层的数据结构。

数据类型和数据结构是编程中非常重要的概念。了解不同的数据类型和数据结构可以帮助你更好地组织和处理数据,提高代码的效率和可读性。

变量定义与常量

变量是存储在内存中的值,这就意味着在创建变量时会在内存中开辟一个空间。

基于变量的数据类型,解释器会分配指定内存,并决定什么数据可以被存储在内存中。

因此,变量可以指定不同的数据类型,这些变量可以存储整数,浮点数或字符。

变量的命名规则

  • 变量名中可以包含 字母数字下划线,例如:user_nameage18
  • 变量名必须以 字母下划线 开头,例如:abc_abc
  • 变量名是 区分大小 写的,例如:Abcabc 是 2 个 变量
  • 变量名不能是语言中的关键字保留字,例如:forpackage

TypescriptRust 基本都遵守上面规则。

Rust 不能使用 $,否则就会抛出 expected pattern 错误。

Typescript 是允许使用 $,当你看见 $ 就会想起 jQuery,看见 click$ 就知道你在使用 Rxjs

声明语法

定义变量

使用 let 关键字定义变量

text 复制代码
let variable_name = value;            // 不指定变量类型(根据值自动推导类型)
let variable_name:dataType = value;   // 指定变量类型

Typescript 语法规范里,let 关键字在同一个作用域中变量名不能重复:

ts 复制代码
let num: number = 25_000;
let num: number = 25_000;
// ts-node Error: Cannot redeclare block-scoped variable 'num'
// js Error: Uncaught SyntaxError: Identifier 'num' has already been declared
num = 35_000;

JavaScript 可以随意修改变量值和类型。Typescript 因为有类型系统存在,就做了类型约束限制。

Rust 里默认情况下是不可变更的。允许变量名重复,我们称之为第一个变量被第二个隐藏(Shadowing)了,这意味着当你使用变量的名称时,编译器将看到第二个变量。(简单理解就是被 var 折磨过的 JSer 一样特性)

ts 复制代码
var num: number = 25_000;
var num: number = 25_000;
// ok
num = 35_000;
rs 复制代码
let num: number = 25_000;
let num: number = 25_000;
// Compile ok
num = 35_000;
// error[E0384]: re-assignment of immutable variable `num`

如果想要实现不报错,需要提供了 mut 关键字表示可变更。

text 复制代码
let mut variable_name = value;            // 不指定变量类型(根据值自动推导类型)
let mut variable_name:dataType = value;   // 指定变量类型
rs 复制代码
let mut num: number = 25_000;
let mut num: number = 25_000;
num = 35_000;
// Compile ok

Rust 里使用 let 定义变量不能修改,想要修改要么加上关键词 mut 要么隐藏(重复 let 一次),那该怎么选择:

  • 如果变量保持不可变,使用隐藏 let
  • 如果需要变量类型,使用隐藏 let (当再次使用 let 时,实际上创建了一个新变量)
  • 其他情况可以使用 mut

还有一种特例:

ts 复制代码
let a: boolean;
if (10 > 20) {
    a = false;
} else {
    a = true;
}
console.log(a);
rs 复制代码
let a: bool;
if 10 > 20 {
    a = false;
} else {
    a = true;
}
println!("The value of x is: {a}");

使用之前,一定要先赋值,Typescript 只是显示 undefined,而 Rust 就会抛出异常 "a" used here but it isn't initialized。如果不定义类型,Rust 会自动按第一个赋值的类型推导。防止手滑,最好加上类型限制。我看文档上都没有这样的写法,这是 JSer 陋习呀,哈哈哈哈。

整体来说定义变量都没有太大区别,唯一区别是可变和可重复。里面小坑自己把握。

定义常量

使用 const 关键字定义常量,常量名一般都是 大写字母,常量名不能重复,不能重新赋值,必须赋值,必须定义类型。

text 复制代码
const VARIABLE_NAME:dataType = value;   // 指定变量类型
ts 复制代码
Number.MAX_VALUE
Number.MIN_VALUE
Math.PI

Typescript 常量也是大写字母,但是自己书写时并没有做要求,最佳风格规范里,都是要求开发人员使用大写字母

ts 复制代码
const a;
// Error: 'const' declarations must be initialized.
const a = 1;
// Compile ok
const a:number = 1;
// Compile ok
a = 5;
// Error: Cannot assign to 'a' because it is a constant.
const a:number = 5;
// ts-node Error: Cannot redeclare block-scoped variable 'a'
// js Error:Uncaught SyntaxError: Identifier 'a' has already been declared
rs 复制代码
const a;
// Error: help: provide a definition for the constant: `= <expr>;`
const a = 1;
// Error: help: provide a type for the constant: `: i32`
const a:i32 = 1;
// Compile ok
a = 5;
// Error: cannot assign to this expression
const a:number = 5;
// Error: `a` redefined here
// note: `a` must be defined only once in the value namespace of this block

定义常量基本表现一致,有些人可能要说 Typescript 的对象,数组,Map,Set 等可以修改,你要明白赋值和修改,它们是有区别的。Typescript 是可以修改对象属性和值,但是对象引用地址不能改。

变量定义基本类型写法

Typescript 定义:

ts 复制代码
let num: number = 25_000;
let str: string = 'hello';
let flag: boolean = true;
const PI: number = 3.14;

Rust 定义:

rs 复制代码
let num: i32 = 25_000;
let str:char = 'hello';
let flag: bool = true;
const PI:f32 = 3.14;

Javascript 语言有个区别:无论整型(int),浮点型(float)和数字有关包括 NaN 都是数字类型。其中浮点数采用 IEEE-754 标准表示,默认浮点数使用 64 位二进制格式来表示,其中包括一个符号位、一个指数位和一个尾数位。

条件判断

if/else

有些书或者文档里面叫流程控制。最基本 if/else,大多数语言都是相通的,甚至写法也出奇一致。

  • if (expression) { // executable true }
  • if (expression) { // executable true } else { // executable false }
  • if (expression) { // executable true } else if (expression) { // executable true }

表达式 expressiontrue 执行 if 括号里,否则执行 else 括号里。

还有一种风骚三目写法:

Typescript 写法:let flag = expression ? true : false

Rust 需要这样实现:let flag = if expression { true } else { false }

ts 复制代码
let number = 3;

if (number) {
    console.log("number was three");
}
// [log] "number was three"
if (number < 5) {
    console.log("condition was true");
} else {
    console.log("condition was false");
}
// [log] "condition was true"
if (number % 4 == 0) {
    console.log("number is divisible by 4");
} else if (number % 3 == 0) {
    console.log("number is divisible by 3");
} else if (number % 2 == 0) {
    console.log("number is divisible by 2");
} else {
    console.log("number is not divisible by 4, 3, or 2");
}
// [log]  number is divisible by 3
let flag = number === 3 ? true : false;
console.log("flag is ", flag);
// [log]  flag is true
rs 复制代码
let number = 3;

if number {
    println!("number was three");
}
Error: expected `bool`, found integer
let number = 3;

if number < 5 {
    println!("condition was true");
} else {
    println!("condition was false");
}
// [log]  condition was true
if number % 4 == 0 {
    println!("number is divisible by 4");
} else if number % 3 == 0 {
    println!("number is divisible by 3");
} else if number % 2 == 0 {
    println!("number is divisible by 2");
} else {
    println!("number is not divisible by 4, 3, or 2");
}
// [log]  number is divisible by 3
let flag = if number == 3 { true } else { false };
println!("flag is {}", flag);
// [log]  flag is  true

Rust 的表达式 expression 不需要括号 ,expression 必须时一个 bool 类型,没有隐式转换特性。

类型转换表,这个表老实用了,在执行隐式转换规则时,可以参考这个对照表。

switch/match

switch 用于基于不同的条件来执行不同的动作。

text 复制代码
switch(expression){
    case value:
       // 语句
       break; // 可选
    case value:
       // 语句
       break; // 可选
    //你可以有任意数量的case语句
    default : // 可选
       // 默认语句
}

首先设置表达式 expression(通常是一个变量)。随后表达式的值会与结构中的每个 case 的值做比较。如果存在匹配,则与该 case 关联的代码块会被执行。请使用 break 来阻止代码自动地向下一个 case 运行。

Typescript 中熟悉常见的 switch

ts 复制代码
let grade: string = 'C';

switch (grade) {
  case 'A': {
    console.log("优秀");
    break;
  }
  case 'B':
  case 'C': {
    console.log("良好");
    break;
  }
  case 'D': {
    console.log("及格");
    break;
  }
  case 'F': {
    console.log("你需要再努力努力");
    break;
  }
  default: {
    console.log("未知等级");
  }
}
console.log("你的等级是 " + grade);
// [LOG]: "良好" 
// [LOG]: "你的等级是 C" 

Rust 没有 switch,但有一个类似叫 match

text 复制代码
match expression {
   value => { // 类似 case
      // 语句;
   },
   value => { // 类似 case
      // 语句;
   },
   //你可以有任意数量的case语句
   _ => { // 类似 default (不能省略,否则编译出错)
      // 默认语句
   }
};
rs 复制代码
let grade: char = 'C';
match grade {
    'A' => {
        println!("优秀")
    },
    'B' | 'C' => {
        println!("良好");
    },
    'D' => {
        println!("及格");
    },
    'F' => {
        println!("你需要再努力努力");
    },
    _ => {
        println!("未知等级");
    }
}
println!("你的等级是 {}", grade);

matchcase 中匹配 pattern 有很多种:

  • 固定单一值,数字:1,字符:'A',布尔:true | false
  • 固定多个值,使用 | 分开,数字:1 | 2 | 3,字符:'B' | 'C'
  • 循环多个值,数字:13..=19,字符:'\0'..='@'、'E' 或 'G'..='\u{d7ff}'
  • 如果只有布尔值可以省略默认 _,其他类型不能省略。
rs 复制代码
let boolean = true;
// Match is an expression too
let binary = match boolean {
    // The arms of a match must cover all the possible values
    false => 0,
    true => 1,
    // TODO ^ Try commenting out one of these arms
};

println!("{} -> {}", boolean, binary);

这种写法类似 Typescript 的箭头函数:

ts 复制代码
let boolean = true;
// 隐式转换 Number(true)|+true => 1 Number(false)|+false => 0
let binary = ((boolean) => +boolean)(boolean);

console.log("%s -> %d", boolean, binary);

注意:千万不能加分号,否则就抛出异常错误。

break

Typescriptbreak 有两层意思:

  • 一个 case 穿透,可以匹配多个,类似 Rust 这样的写法: 1 | 2 | 3,如果不使用 break,它会一直匹配 case ,共用一份匹配语句代码。
ts 复制代码
switch (num) {
  case 1: 
  case 2:
  case 3: {
    console.log("这是数字", num);
    break;
  }
  default: {
    console.log("未知");
  }
}
  • 一个阻断代码执行,break 之后代码不会执行,你正常写代码不会在代码没有结束之间写 break,一般会配合条件判断 if 来阻断代码继续执行。
ts 复制代码
switch (num) {
  case 1: {
    if (Math.random() > 0.5) {
        break;
    }
    // 如果前面if条件成立 这里无法执行
    console.log("这是数字", num);
  }
  default: {
    console.log("未知");
  }
}

一般这种写法不多,更多使用的是把 break 换成 return

Rustbreak

可以实现直接使用 case 穿透,匹配多个条件,我们直接看第二个意思,阻断代码。

一开始是没有 break,你需要自己创建一个宏:

rs 复制代码
macro_rules! block {
    ($xs:block) => {
        loop { break $xs }
    };
}

match x {
    1 => block!({
        ...
        if y == 0 { break; }
        ...
    })
    _ => {}
}

可以利用 loop (后面会介绍):

rs 复制代码
fn main() {
    let x = 1;

    loop { 
        match x {
            1 => {
                let y = 0;

                /* break early ... */
                if y == 0 { 
                    break; 
                }

                assert!(y != 0, "y was 0!");
                /* do other stuff in here. */
            }
            _ => {}
        } 
        break; 
    }

    println!("done matching");
}

现在版本可以放心使用:

rs 复制代码
fn main() {
    let x = 1;

    match x {
        1 => 'label: {
            let y = 0;

            /* break early ... */
            if y == 0 {
                break 'label;
            }

            assert!(y != 0, "y was 0!");
            /* do other stuff in here. */
        }
        _ => {}
    }

    println!("done matching");
}

你会看到语法很奇特,这里要注意细节,"'label" 没有结束的 "'"。 在 Rust 里字符串要使用双引号,不能使用单引号(在 JSer 要特别注意这个坑) 在 Rust 里如果不使用引号,它认为是一个类型

return

TypescriptRustreturn 语句只能在函数体中使用。return 之后代码永远不会执行,我们叫它 Unreachable code detected

我们可以利用这个特性,创建一个函数,return 返回值并且阻断后续代码执行。

ts 复制代码
function fn() {
    switch (num) {
        case 1: {
            if (Math.random() > 0.5) {
                return true;
            }
            // 如果前面if条件成立 这里无法执行
            console.log("这是数字", num);
        }
        default: {
            console.log("未知");
        }
    }
    return false;
}
rs 复制代码
fn match_it(val: i8) -> i8 {
    let mut a: i8 = 0;
    match random_or(0, 1) {
        1 => {
                a = 5;
                // other ev1l stuff
                if a == 6 { return 1 }
             },
        0 => a = -5,
        _ => (),
    };
    
    a // return a
}

如果我不想有返回值,又想阻断代码那该如何?可以利用闭包特性:

ts 复制代码
let num = 2;
(() => {
  switch (num) {
      case 1: {
          if (Math.random() > 0.5) {
              return;
          }
          // 如果前面if条件成立 这里无法执行
          console.log("这是数字", num);
      }
      default: {
          console.log("未知");
      }
  }
})()
rs 复制代码
fn main() {
    let x = 1;

    (|| {
        match x {
            1 => {
                let y = 0;

                /* break early ... */
                if y == 0 { return; } // Ok!

                assert!(y != 0, "y was 0!");
                /* do other stuff in here. */
            }
            _ => {}
        }
    })();

    println!("done matching");
}

语言都是相通性,条件判断这是多个语言都是类似,Typescript 需要注意特别隐式转换问题。switch 对标 match 都已经把常用写法都列出了,你可以实现想要的功能。有没有发现 matchswitch 写起来更爽。哈哈哈

循环语句

循环其实就是一种重复,在满足指定的条件下,重复的做某些事情。

编程语言也有循环这种概念,也可以在满足指定的条件下,重复的执行一部分代码,我们把这种重复执行的代码称之为循环语句

大部分语言都用 2 个基本的循环语句: forwhile

在介绍这个 2 个循环之前,我们先介绍 2 个通用的循环语句关键词:breakcontinue

break 和 continue

break 有跳出,打破,阻断意思。跳出循环后,继续执行该循环之后的代码。

continue 有继续,移动意思。中断当前的循环中的迭代,然后继续循环下一个迭代。

都可以指定循环标签,使这些关键字应用于已标记的循环而不是最内层的循环。

ts 复制代码
let i, j;

loop1:
for (i = 0; i < 3; i++) {      //The first for statement is labeled "loop1"
   loop2:
   for (j = 0; j < 3; j++) {   //The second for statement is labeled "loop2"
      if (i == 1 && j == 1) {
         break loop1;
      }
      console.log("i = " + i + ", j = " + j);
   }
}
ts 复制代码
let i, j;

loop1:
for (i = 0; i < 3; i++) {      //The first for statement is labeled "loop1"
    loop2:
    for (j = 0; j < 3; j++) {   //The second for statement is labeled "loop2"
        if (i === 1 && j === 1) {
            continue loop1;
        }
        console.log('i = ' + i + ', j = ' + j);
    }
}

while

while 语句只要指定条件为 true,循环就可以一直执行代码块。

text 复制代码
while(condition) {
   statement
}

如果忘记增加条件中所用变量的值或该值是一个 true,该循环永远不会结束。这可能导致程序崩溃。传说中的死循环。

Typescriptwhile

ts 复制代码
let result = '';
let i = 0;
while (i < 5) {
   i += 1;
   result += i + ' ';
};

console.log(result);
// "1 2 3 4 5 " 

带有循环标签跳出:

ts 复制代码
let n = 0;

top: while (n < 3) {
  if(n === 2) {
    break top;
  }
  n++;
}

console.log(n);
// 2

Typescript 还有类似 do...while

do...while 语法:

text 复制代码
do
   statement
while (condition);

do...while 语句创建一个执行指定语句的循环,直到 condition 值为 false。在执行 statement 后检测 condition,所以指定的 statement至少执行一次。

ts 复制代码
let result = '';
let i = 0;
do {
   i += 1;
   result += i + ' ';
} while (i < 5);

console.log(result);
// "1 2 3 4 5 " 

你会发现 whiledo...while 返回结果一样,如果你把 i 改成 5, while 返回 ""do...while 返回 "6 "

很多人可能对 whiledo...while 分辨得可能不是很清楚。

简单来说,whiledo...while 最大的区别便是 while 会先判断再执行语句,而 do...while 便是先 do 执行再判断,也就是说 do...while 不管条件是否成立,都会先执行一次。而 while 则先判断是否成立,若不成立则退出循环,即一次也不执行。

那么最后我们该如何选择使用哪一种语句呢?

一般来说 while 是通用的语法,可以任意语言切换。这是因为按照一般原则是在执行循环之前测试条件比较好,而且测试放在循环的开头可使程序的可读性更高。还有就是在许多应用中,要求在一开始不满足测试条件时就直接跳过整个循环。如果你希望无论条件是否成立都执行一次,那就使用 do...while。否则推荐使用 while

Rustwhile

rs 复制代码
fn main() {
    // A counter variable
    let mut n = 1;

    // Loop while `n` is less than 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Increment counter
        n += 1;
    }
}

带有循环标签跳出:

rs 复制代码
fn main() {
    // A counter variable
    let mut n = 1;

    'top: while n < 3 {
      if n == 2 {
        break 'top;
      }
      n += 1;
    }

    println!("{}", n);
    // 2
}

RustTypescript 整体没太大差别,condition 一定要是布尔值,跟 if/else 条件判断一样。没有隐式转换特性,这个要特别注意。循环标签前一定要加上 ',否则就会抛出异常,前面已经解释过了。

相比 for 来说,while 语句基本表现一致(除了条件是否有括号语法和隐式转换特性)

for

通用 for 循环遍历数组,字符串,或者数字区间。

Typescriptfor

  • for 按条件遍历
text 复制代码
for (initialization; condition; afterthought)
  statement

initialization, condition, afterthought 都是可选的,如果省略需要有 ;

ts 复制代码
let i = 0;

for (;;) {
  if (i > 3) break;
  console.log(i);
  i++;
}

效果相同:

let i = 0;

while (i <= 3) {
  console.log(i);
  i++;
}

省略 condition:

ts 复制代码
for (let i = 0; ; i++) {
  console.log(i);
  if (i > 3) break;
}

效果相同:
let i = 0;
while (true) {
  console.log(i);
  if (i > 3) break;
  i++;
}

省略 initialization:

ts 复制代码
let i = 0;
for (; i < 9; i++) {
  console.log(i);
  // more statements
}

正常写法:

ts 复制代码
for (let i = 0; i < 9; i++) {
  console.log(i);
  // more statements
}

// 经典面试题:请问输出什么,是否是顺序输出
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 为什么 let i = 0 可以解决问题
  • for...in 代对象的所有枚举字符串属性,包括继承的枚举属性
text 复制代码
for (variable in object)
  statement
ts 复制代码
for(let attr in obj) {
    console.log('key: ' + attr, 'value: ', obj[attr]);
}

如果不希望枚举继承属性,就需要使用 Object.hasOwn() 判断:

ts 复制代码
for (const prop in obj) {
  if (Object.hasOwn(obj, prop)) {
    console.log(`obj.${prop} = ${obj[prop]}`);
  }
}

推荐使用 Object.keys() | Object.getOwnPropertyNames()

ts 复制代码
for (variable of iterable)
  statement

ES6 开始新增规则,遍历带有 [@@iterator]() 迭代器的对象,内置有 Array,String,TypedArray,Map,Set,arguments,NodeList,Generator function。

ts 复制代码
// Array
const iterable = [10, 20, 30];

for (const value of iterable) {
  console.log(value);
}

// String
const iterable = "boo";

for (const value of iterable) {
  console.log(value);
}

// TypedArray
const iterable = new Uint8Array([0x00, 0xff]);

for (const value of iterable) {
  console.log(value);
}

// Map
const iterable = new Map([
  ["a", 1],
  ["b", 2],
  ["c", 3],
]);

for(const [key, value] in iterable) {
    console.log('key: ' + key, 'value: ', value);
}

// Set
const iterable = new Set([1, 1, 2, 2, 3, 3]);

for (const value of iterable) {
  console.log(value);
}

// arguments
function foo() {
  for (const value of arguments) {
    console.log(value);
  }
}

foo(1, 2, 3);

// NodeList
const articleParagraphs = document.querySelectorAll("article > p");
for (const paragraph of articleParagraphs) {
  paragraph.classList.add("read");
}

// Generator function
function* source() {
  yield 1;
  yield 2;
  yield 3;
}

const generator = source();

for (const value of generator) {
  console.log(value);
}

自定义迭代器对象:

ts 复制代码
// 基本写法
const iterable = {
  [Symbol.iterator]() {
    let i = 1;
    return {
      next() {
        if (i <= 3) {
          return { value: i++, done: false };
        }
        return { value: undefined, done: true };
      },
    };
  },
};

for (const value of iterable) {
  console.log(value);
}

// 另一种写法
let i = 1;
const iterator = {
  next() {
    if (i <= 3) {
      return { value: i++, done: false };
    }
    return { value: undefined, done: true };
  },
  [Symbol.iterator]() {
    return this;
  },
};

for (const value of iterator) {
  console.log(value);
}

// 生成器方法迭代对象
const iterable = {
  *[Symbol.iterator]() {
    yield 1;
    yield 2;
    yield 3;
  },
};

for (const value of iterable) {
  console.log(value);
}

ECMAScript 2017 引入 Promise + GeneratorFunction 语法糖 async/await

ECMAScript 2018 引入异步迭代对象 [Symbol.asyncIterator]

ts 复制代码
const LIMIT = 3;

const asyncIterable = {
  [Symbol.asyncIterator]() {
    let i = 0;
    return {
      next() {
        const done = i === LIMIT;
        const value = done ? undefined : i++;
        return Promise.resolve({ value, done });
      },
      return() {
        // This will be reached if the consumer called 'break' or 'return' early in the loop.
        return { done: true };
      },
    };
  },
};

(async () => {
  for await (const num of asyncIterable) {
    console.log(num);
  }
})();

那它和 for...of 有什么区别和联系,联系就是迭代器对象,同步和异步区别。

ts 复制代码
function* generator() {
  yield 0;
  yield 1;
  yield Promise.resolve(2);
  yield Promise.resolve(3);
  yield 4;
}
(() => {
    for (const numOrPromise of generator()) {
        console.log('for...of', numOrPromise);
    }
})();
// es2017
(async () => {
    for (const numOrAwait of generator()) {
        console.log('async/await', await numOrAwait);
    }
})();
// es2018
(async () => {
  for await (const num of generator()) {
    console.log('for await...of', num);
  }
})();

for await...of 也算是 for...of + async/await 的一种语法糖。

Rustfor

  • for and range:遍历区间
rs 复制代码
fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    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);
        }
    }
}

还可以使用 1..=100 这种区间语法

  • for and iterators:迭代器遍历

迭代器模式是一个设计模式,大部分语言都实现类似迭代器模式里迭代器接口。Rust 看起来 Typescript 差不多,写法很风骚。

通用方法 next() 都没有什么可秀的,真正秀儿 skip()take()。如果用过 Rxjs 可能知道这两个操作符。如果用过后台数据库 ORM 操作数据库也会接触这 2 个方法,经常用于分页查询。一个是跳过多少条数据,一个是拿多少条数据。

Rust iterators 示例。

  • iter():这通过每次迭代借用集合中的每个元素。从而使集合保持不变,并且在循环之后可以重用。
rs 复制代码
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter() {
        println!("Hello {}", name);
    }
    
    println!("names: {:?}", names);
}
// 或者
fn main() {
    let names = ["Bob", "Frank", "Ferris"];

    for name in names {
        println!("Hello {}", name);
    }
    
    println!("names: {:?}", names);
}
// Hello name Bob
// Hello name Frank
// Hello name Ferris
// names: ["Bob", "Frank", "Ferris"]
  • into_iter():这将消耗集合,以便在每次迭代中提供准确的数据。一旦集合被消耗,它就不再可用,因为它已经在循环中"移动"了。
rs 复制代码
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        println!("Hello name {}", name);
    }
    
    println!("names: {:?}", names);
}
// 编译错误 for name in names.clone().into_iter() {
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.clone().into_iter() {
        println!("Hello name {}", name);
    }
    
    println!("names: {:?}", names);
}
// 或者
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in (&names).into_iter() {
        println!("Hello name {}", name);
    }
    
    println!("names: {:?}", names);
}
// 或者
fn main() {
    let names = & ["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        println!("Hello name {}", name);
    }
    
    println!("names: {:?}", names);
}
// Hello name Bob
// Hello name Frank
// Hello name Ferris
// names: ["Bob", "Frank", "Ferris"]

发现输出和 iter() 一致,这里有个变更迁移

如果定义数组不加 vec! 使用 into_iter() 就会抛出错误:

text 复制代码
for name in names.iter() {
use `.iter()` instead of `.into_iter()` to avoid ambiguity`

or remove `.into_iter()` to iterate by value
- for name in names.into_iter() {
+ for name in names {    
  • iter_mut():这将可变地借用集合的每个元素,从而允许对集合进行适当的修改。
rs 复制代码
fn main() {
    let mut names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter_mut() {
        println!("Hello name {}", name);
        *name = "Hello";
    }

    println!("names: {:?}", names);
}
// Hello name Bob
// Hello name Frank
// Hello name Ferris
// names: ["Hello", "Hello", "Hello"]
或者
fn main() {
    let slice = &mut [1, 2, 3];

    for element in slice.iter_mut() {
        *element += 1;
    }

    println!("{slice:?}");
    // [2, 3, 4]
}

对于数组以及奇怪的语法暂时按下不表,后面会深入探讨。

Rust 里面还有一个 loop 无限循环语句:

text 复制代码
loop {
    // code
}

你可以简单理解:

text 复制代码
for(;;) {
    // code
}
rs 复制代码
fn main() {
    let mut count = 0u32;

    println!("Let's count until infinity!");

    // Infinite loop
    loop {
        count += 1;

        if count == 3 {
            println!("three");

            // Skip the rest of this iteration
            continue;
        }

        println!("{}", count);

        if count == 5 {
            println!("OK, that's enough");

            // Exit this loop
            break;
        }
    }
}

可以使用 break 跳出循环,continue 跳过当前迭代,进行下一次迭代。

如果不使用 break 或 continue 或者函数里 loop 不使用 return,就会出现程序崩溃,传说的死循环。

整体来说,循环就 2 大类:

  • while
  • for
    • range 区间
    • iterators 迭代器

你可能要说 Typescript 没有这样区间,但你忘了 for(let i = 0; i < 100; i++) {},这不就是所谓区间。只不过需要你手动写,Rust 这个区间相对 Typescript 来说就是一个语法糖。至于迭代器语法,RustTypescript 都是由需要提供迭代器接口,才能正常工作。

在本节结束秀一个帅气的代码:

rs 复制代码
fn main() {
    for number in (1..=4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

发给 Python 同事表示这个代码太秀儿了。

写在最后

通过这篇文章,让我收获颇丰,不仅回顾了 Typescript 类型和 JavaScript 语法。在学习 Rust 过程中看到语言语法通用性,文章中也是一篇学习一门模板,从 Hello world 上车到跑路。学完上面的基础语法和奇怪写法,可以很容易看懂 GithubRust 相关项目大部分代码。这只是开始,学无止境,后续阅读官网文档深入学习。

后续还有字符串,函数等其他入门类型,由于包含大量代码,导致篇幅过长,剩余部分将会在下一篇继续学习这些有意思的东西。

今天就到这里吧,伙计们,玩得开心,祝你好运


谢谢你读到这里。下面是你接下来可以做的一些事情:

  • 找到错字了?下面评论
  • 如果有问题吗?下面评论
  • 对你有用吗?表达你的支持并分享它。
相关推荐
guokanglun几秒前
Vue.js动态组件使用
前端·javascript·vue.js
-seventy-13 分钟前
Ajax 与 Vue 框架应用点——随笔谈
前端
我认不到你29 分钟前
antd proFromSelect 懒加载+模糊查询
前端·javascript·react.js·typescript
集成显卡32 分钟前
axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
前端·ajax·json
焚琴煮鹤的熊熊野火40 分钟前
前端垂直居中的多种实现方式及应用分析
前端
我是苏苏1 小时前
C# Main函数中调用异步方法
前端·javascript·c#
转角羊儿1 小时前
uni-app文章列表制作⑧
前端·javascript·uni-app
大G哥1 小时前
python 数据类型----可变数据类型
linux·服务器·开发语言·前端·python
hong_zc2 小时前
初始 html
前端·html
小小吱2 小时前
HTML动画
前端·html