从PHP到JS:我的编程语言学习之旅与经验分享
作者 |丹尼尔·布兰特

译者|新月
出品| CSDN(ID:CSDNnews)
当我第一次开始编程时,我使用的是 PHP。就我个人而言,我觉得PHP的语法有点笨拙和不自然,有时甚至很烦人(为什么我必须在每个变量前面加上$前缀?常量前面没有$不是仍然有意义吗?)在学习了其他之后语言,我不再喜欢 PHP,但是 PHP 的一些东西我还是喜欢的。例如,数组循环很容易,并且有多种编程范式:函数式、面向对象、特征等。
后来我又学了JS,它和C语言很相似,随处可见。在此期间,我也参与了一些Java和C#项目,但后来又回到了JS。
我也尝试过学习 C(和 C++),虽然从 Sololearn 获得了证书,但我从未真正使用过这两种语言,它们看起来很复杂:快速访问内存很酷,但为什么我必须使用 free?为什么它不知道超出范围时应该自动释放内存?
因此,我还是更喜欢使用JS编程,因为不需要考虑内存问题。而且,IO相关的操作现在并没有限制V8的速度。
后来我听说了Rust,一种Mozilla开发的语言。它连续多年位列 StackOverflow 上最喜欢的编程语言榜单,甚至超过了我非常喜爱的 Typescript(我喜欢 Typescript 的原因主要是因为类型安全)。所以,我想我应该找个机会尝试一下。
学习资源
我面临的困难之一是寻找易于理解且简短的优质资源。我不喜欢YouTube视频,我更喜欢在上下班途中快速浏览一些文档或阅读一些学习资源,而不消耗大量数据。
这是我找到的资源列表:
●《Rust 编程语言》(
):这是一本在线书籍,描述了可以用 Rust 实现的最常见的功能。
●“Rust 简介”(
):一本简短的书,可以在一两个小时内读完,然后用一两天的时间尝试示例。文章涵盖的内容比较深入,但很容易掌握。
●
:这是一个reddit社区(如果遇到比较复杂的问题,可以在这里发帖,等待别人解答。)
● Discord 社区:您可以通过该社区向其他开发人员询问有关 Rust 的信息。
● Rust 实例(
):介绍了一些例子,可以作为入门的首选书籍。
入门
请参阅 Rust 网站(
),只需使用 rustup 即可。
如果你想创建一个新项目,运行cargo init(无需指定是否在空目录中)。
然后就可以从src/main.rs开始编写了。
与C类似,主程序被包装在main中。不同之处在于它不接受任何参数,也不应该返回整数,这些函数应该使用命名空间 std::env。
另外,我建议使用 CLion 并安装 Rust 扩展。 VSCode 也有 Rust 扩展,但性能相比较差。当然,您可以使用其他 JetBrains 编辑器,但 CLion 具有其他编辑器所没有的一些本机功能(例如调试)。拥有 GitHub Education Pack 的学生可以免费使用该插件。
有趣的笔记
不仅是变量,还可以在函数和特征内部使用嵌套函数和用途。这些无法从外部访问,如果不使用,也不会出现在代码中。至少我是这么认为的。
变量和函数/方法只能使用小写字母、数字和下划线,例如snake_case,但不能将数字放在开头。
结构体(和其他类型)、枚举(包括枚举值)和特征(但不是它们的函数/方法)需要以大写字母开头,并且不能包含任何下划线。
实际上,您可以使用 i += 1。与赋值一样,此表达式返回分配的值(即,将 i 设置为 i + 1,然后返回 i)。
没有 i++(或 ++i、i-- 和 --i),因为这些运算符有点令人困惑。
您确定以下操作的结果吗(特别是在未指定语言的情况下)?
a[i++ + ++i] = i++ + ++i + a[++i]
问题是,直到最近,上述操作的实际行为尚未定义,这意味着不同的编译器(甚至可能是同一编译器的不同版本)可能会产生不同的行为。为了解决这个问题,让代码更具可读性(Rust 非常重视可读性和冗长性,甚至到了敲几次键盘的程度),Rust 只支持 i += 1,这个几乎人人都知道的表达式意味着变量增加i减1并返回最终结果。因此,您不必知道 i++ 实际上返回原始值(而不是新值)并加 1。
另外,运算符重载使用了特征,但本文不打算详细讨论它们。
除了函数调用之外,if、while、match、for 都是表达式。
可以直接使用 if 来代替其他语言中常见的三元运算符:
let var = if something { 1 } else { 2 };
循环将根据对break的调用返回结果。您可以使用它重复重试某个操作,直到成功。
多变的
变量是用 let 声明的,并且有作用域。类型是可选的,Rust 非常擅长推断类型(比 Typescript 更好)。
let var: usize = 1;
上面的变量定义了一个 usize 类型的变量 var(usize 是一个 32 位或 64 位数字,具体取决于计算机体系结构)。
您可以重复声明变量。当重复声明一个变量时,先前声明的变量将被删除(除非该变量被引用,在这种情况下仅保留引用并删除原始变量),并且该变量的类型会更改。
let var = 1;let var = "something";
默认情况下,变量是不可变的。如果要修改它们,需要在let后面添加关键字mut。
let var = 1;var = 2; // 错误!不可以修改不可变的变量
let mut var = 1;var = 2;
功能
fn main(arg: u8) -> u8 { // something arg}
函数的行为几乎与 JS 完全相同,只是它们不是数据类型并且语法略有不同。
参数的规范与Typescript类似,即key: type。返回类型通过->指定。
有趣的是,虽然 Rust 需要分号,但如果忘记了最后一个表达式后面的分号,它将被用作返回值(即使没有 return 关键字)。
如果语句
if something {} else {} else if something_else {}
if语句的使用非常基础,这里不再详细介绍。
需要注意的一件事是,除非必要,否则使用括号实际上是一种错误。您可以使用括号指定执行顺序:
if (something || something_else) && something_other {}
前面提到,if 也可以返回一个值,该值可以用在赋值、参数、返回或其他地方。
let var = if something { 1 } else { 2 };
这里的花括号是必需的。
类型
Rust 有两种类型:基本数据类型(数字、str)和结构体(字符串)。
两者之间的唯一区别是基本类型的初始化可以直接完成,而复杂类型则需要某种构造函数。
堆和栈
我以前几乎不需要考虑堆和堆栈。 (据我所知,JS中的对象存储在堆中,只有基本类型存储在堆栈中。)
堆:
● 慢
● 比较大
堆
● 非常快
● 相对较小
原始类型和原始结构存储在堆栈中。要在堆中存储值,需要使用Box。另外,Vec还可以将值保存到堆中。
如果您使用大量内存,或者需要在结构中使用带有值的枚举,您可能需要使用堆。
如果发生堆栈溢出,则说明您使用了过多的堆栈内存。对于一些较大的值,应使用 Box。
常见基本类型
数字:
● i8、i16、i32、i64、i128:有符号整数,包括负数。该数字表示值中的位数。
● u8、u16、u32、u64、u128:无符号整数,从零开始。它们的最大容量加倍,因为有一个额外的位可用(用于表示有符号整数中的符号)。该数字表示值中的位数。
● f32 和f64:浮点数。 JavaScript 世界中的常见数字。
细绳:
● str:简单的UTF-8 字符串(所有Rust 字符串都是UTF-8。不能使用无效的UTF-8 字符串,因为它会导致异常或panic)。通常用作指针(即&str)。
● String:一种更复杂的类型(严格来说不是原始类型),存储在堆中。
大批:
● T[]:固定长度数组(如果使用Option 类型,则数组包含的元素个数可以小于实际长度)。
元组
元组可以用来存储多个不同类型的值(本质上是可以容纳不同类型的固定大小的数组)。
与数组不同,元组可以通过点 (.) 直接访问。例如tuple.0表示获取第一项,而tuple没有.len()等方法。
let var = (1, "str");
一个有趣的小技巧是你可以通过 () 返回“void”(一个空元组)。既没有 return 语句也没有值的函数 returns ()。
共同结构
选项
● 这是一个值为Some(T) 或None 的枚举。 (我们稍后会讨论枚举,Rust 中的枚举与其他语言略有不同。)
● 如果您想获取该值,您可以使用match,就像任何其他枚举一样,或者使用.unwrap()(如果该值为None,这将导致panic)。
结果
● 该结构与Option 类似,但常用于处理错误(通常由IO 方法返回)。
● 取值为Ok(T)或Err(E)。
● 如果要获取值,可以使用match 块或unwrap()。
● 为了方便使用,当函数返回Result 时,可以使用?在调用返回 Result 的方法后返回错误 E(其中 E 必须是兼容类型)(类似于使用 .unwrap(),但当函数出错时不会引起恐慌)。
fn example() -> Result<(), Error> { // 一种错误类型。为了简便起见,你可以使用String,或自定义enum。 something_that_returns_result()?; Ok(()) // returns empty Tuple}
向量
● 向量是可增长的数组,存储在堆上。
● Vector 支持.push()、.pop()等常用操作。有关详细信息,请参阅 Rust 文档。
盒子
● 将T 存储在堆上。可用于在结构中使用枚举,或释放堆栈空间。
定义结构
结构与对象类似,但它们的大小是静态的。
结构可以通过多种方式定义:
● 使用Tuple作为声明(类似于tuple的别名)
struct Something(u8, u16); // a struct with 2 numbers, one unsigned 8 bit, the other one unsigned 16 bit
● 使用对象表示法(类似于声明类或对象)
struct Something { value: u8, another_value: u16}
● 使用struct 作为别名
struct Something = u8; // a single value
当您尝试创建一个枚举,其值可能是定义的引用该枚举(直接或间接)的结构时,此方法非常有用。
struct MaybeRecursive { possibly_self: Option // error! }struct MaybeRecursive { possibly_self: Option> // fine }
我在为 shell 创建抽象语法树时遇到了这个问题。
创建结构体实例需要使用如下写法(类似于C#中定义数组):
Something { variable: 1, another_variable: 1234}
定义枚举
这是一个例子:
enum EnumName { First, Second}
您可以为枚举指定数值(例如,序列化或反序列化数值时):
enum EnumName { First = 1, Second // auto incremented}
更强大的写法如下:
enum EnumName { WithValue(u8), WithMultipleValues(u8, u64, SomeStruct), CanBeSelf(EnumName), Empty}
您可以使用 match 来提取值。
匹配
match 是 Rust 最强大的功能之一。
Match 是一个更强大的 switch 语句。用法与普通switch语句相同,只不过它必须涵盖所有可能的情况。
let var = 1;match var { 1 => println!("it's 1"), 2 => println!("it's 2"), // following required if the list is not exhaustive _ => println!("it's not 1 or 2")}
您还可以匹配范围:
match var { 1..=2 => println("it's between 1 and 2 (both inclusive)"), _ => println!("it's something else")}
你也可以什么都不做:
match var { _ => {}}
您可以使用 match 来安全地解包 Result 和 Option,以及从其他枚举获取值:
let option: Option<u8> = Some(1);match option { Some(i) => println!("It contains {i}"), None => println!("it's empty :c") // notice we don't need _ here, as Some and None are the only possible values of option, thus making this list exhaustive}
如果您不使用 i (或其他值),Rust 会警告您。您可以使用 _ 代替。
match option { Some(_) => println!("yes"), None => println!("no")}
match 也是一个表达式:
let option: Option<u8> = Some(1);let surely = match option { Some(i) => i, None => 0}println!("{surely}");
您可以查看 Option 文档(或使用 IDE 的自动完成功能来查看可以使用哪些特征或方法)。
您可能已经注意到,您可以使用 .unwrap_or(val) 代替上面的代码(上面的匹配相当于 .unwrap_or(0))。
环形
循环是最简单的循环。只需使用循环即可。
loop { if something { break }}
该代码将一直运行,直到遇到break(或return,它也返回父函数)。
为了
for 循环是最简单、最容易使用的循环。它比传统的 for 循环更容易使用。
for i in 1..3 {} // for(let i = 1; i < 3; i++) // i++ is not a thing, see things to notefor i in 1..=3 {} // for(let i = 1; i <= 3; i++)for i in 1..=var {} // for(let i = 1; i <= var; i++)for i in array_or_vec {} // for(let i of array_or_vec) in JS// again, as most other things, uses a trait, here named "iterator"// for some types, you need to call `.iter()` or `.into_iter()`.// Rust Compiler will usually tell you this.for i in something.iter() {}
尽管
非常简单的循环。与其他语言不同,Rust 没有 do...while,只有最基本的 while。
while condition { looped();}
语法与 if 相同,只不过内容将在循环中执行。
打印出
打印输出可以使用print进行打印!和打印!。
!表明这是一个宏(即可以扩展为其他代码的快捷方式),但你不需要考虑太多。另一个常用的宏是 vec![],它从数组创建 Vec(使用 [] 内的值)。
这些宏有一个简单的模板系统。
● 使用println!() 输出一行。
● 要输出静态字符串,请使用print!("something")。在 println 中!表示line,表示会添加换行符(\n)。 console.log 默认会添加换行符。
● 要输出实现 Display 特征的值(大多数基本类型都实现它),您可以使用 print!("{variable}")。
● 要打印实现 Debug 特征(可以从 Display 继承)的值,请使用 print!("{variable:?}")。
● 要打印实现 Display 特征的更复杂的内容,请使用 print!("{}", variable)。
● 要输出实现Debug 特征的更复杂的内容,请使用print!("{:?}",variable)。
特征
Traits 是 Rust 中最难理解的概念之一,也是最强大的概念之一。
Rust 没有采用基于继承的系统(面向对象的继承,或 JavaScript 的基于原型的继承),而是采用鸭子类型(即,如果某物像鸭子一样嘎嘎叫,那么它就是鸭子)。
每种类型都有一个且仅有一个“默认”(或匿名)特征,该特征只能在与该类型相同的模块中实现。通常方法是该类型特有的。
其他一切都称为特质。例如:
trait Duck { fn quack(&self) -> String; /// returns if the duck can jump fn can_jump(&self) -> bool { // default trait implementation. Code cannot have any assumptions about the type of self. false // by default duck cannot jump }}struct Dog(); // a struct with empty tupleimpl Dog { // a nameless default trait. fn bark(&self) -> String { String::from("bark!") }}impl Duck for Dog { // implement Duck trait for Dog type (struct) fn quack(&self) -> String { String::from("quark!") } // dog kind of quacks differently}let dog = Dog {};dog.bark();dog.quack();
首先,我们定义特征(在面向对象语言中称为接口,但它们只包含方法或函数)。然后实现给定类型(上例中的 Dog)的特征。
有些特征可以自动实现。常见的示例是显示和调试特征。这些特征要求结构中使用的类型必须相应地实现 Display 或 Debug。
#[derive(Display,Debug)]struct Something { var: u8}println!("{:?}", Something { var: 1 });
范围
特征具有范围,并且独立于它们实现的类型的范围。也就是说,您可以使用类型,但不能使用特征的实现(例如,如果实现来自另一个库而不是类型本身)。您可以使用此实现。
自己
特征中的 self 指向它实现的类型。 &self 是指向 self: &Self 的别名,其中 Self 代表类型(上例中的 self: &Dog)。 self也是self的别名:Self,但两者的区别在于后者移动变量(即当变量被消费时,外部无法访问该变量)。
当函数定义不以 self、&self 或 &mut self 开头(&mut self 相当于带有可变引用的 &self)时,它是静态方法。 Traits 仍然可以像任何方法一样定义和实现静态方法。常见的静态方法是new,它用于创建类型或结构的实例:
impl Something { fn new() -> Something { Something { x: 1 } }}...let var = Something::new();
指针
指针实际上很容易理解,即使它们来自其他高级语言。我经常错误地使用它们。
&A指向A,使用时只需要保证A存在就可以保证&A存在,因为我们不应该让指针指向一个不存在的对象。
Rust会在编译时进行静态检查,以确保不会出现上述情况。它会自动释放超出范围的变量,并且不允许指针比变量的寿命更长。另一种安全保证是只能有一个可变指针。
换句话说,下面的代码是错误的:
let a = 1;let b = &a;let c = &mut a;println!("{b}"); // Error! there can only be one mutable pointerc = 1;
我们只需要确保原始变量始终存在于指针的范围内即可。
在结构内部使用指针可能会有点问题,因为编译器不喜欢它(因为结构通常比原始变量的寿命更长)。我通常使用所有权转移或克隆(.clone(),它是 Clone 特征的一部分,可以派生)。
有时,某些函数要求只能使用指针而不能使用所有权转移。在这种情况下,只需在值前面添加 &(或 &mut)即可。
something(&a);
此外,还有双指针、三指针等,但它们很少见,而且一般只是比较难处理。
你也不需要考虑释放变量,当它们超出范围时,Rust 会自动释放它们。
名称空间
使用全名无需导入。导入只不过是别名。
std::env::args()
use std::env;env::args()
use std::env::args;args()
您可以使用 {} 选择多个“命名空间”,例如:
use std::env::{args, var};
您也可以重复使用它:
use std::env;use std::env::args;env::var();args()
另一件事是,您还可以在函数内部使用 use 。这样,如果代码没有执行,则不会导入该库(即该函数没有出现在代码路径中,例如使用了一个测试库,use只写在测试用例中) ,那么在正常构建中不会导入库)。
fn test() { use std::env; env::var();}
但我不建议在正常的代码路径中编写此代码。您应该使用全局导入。
能见度
现在我们已经讨论了命名空间,让我们讨论可见性。
本质上,默认情况下所有内容都是私有的,并且只能由其所在的文件访问。
● 特征及其方法
● 结构及其成员
● enum(成员继承了enum的可见性,这是合理的,参见Match)
● 功能
● 特征的实现取决于该特征以及实现该特征的结构。也就是说,只有当两者都是公开的时,实现才是公开的。
要使其公开(即从外部访问),请使用关键字 pub:
pub struct Something { pub letter: char}pub trait CustomTrait { ... }pub fn method() {}
使用多个文件
有时我会想念 require("./fire")。
要“导入”文件,请使用 mod 命令。通过 Cargo 下载的板条箱将自动导入。
主程序.rs
mod my;fn main() { my::function(); // or use my::function; function();}
my.rs
pub fn function() { println!("function");}
您还可以使用 pub mob 重新导出文件。大多数现有 Rust 代码对支持文件夹执行以下操作:
主程序.rs
mod my;use my::file;fn main() { file::function();}
my/mod.rs - mod.rs 这个名字很特殊,类似于index.js
pub mod file;
我的/文件.rs
pub fn function() { println!("function");}
对于 println!,请参阅“打印输出”。
编写文档
要编写文档,只需使用三个斜杠 ///。一些 IDE 使用不同的突出显示方法。
与 JSDoc 类似,只不过它的类型不会被显式标记,因为类型已经写在代码中了。
/// a description of varlet var = "something";