用户故事 语言不仅是工具,还是思维方式 你好,我是 Pedro,一名普普通通打工人,平平凡凡小码农。

可能你在课程留言区看到过我,也跟我讨论过问题。今天借着这篇用户故事的机会,正好能跟你再多聊几句。

我简单整理了一下自己入坑编程以来的一些思考,主要会从思维、语言和工具三个方面来聊一聊,最后也给你分享一点自己对 Rust 的看法,当然以下观点都是“主观”的,观点本身不重要,重要的是得到观点的过程。

从思维谈起

从接触编程开始,我们就已经开始与编程语言打交道,很多人学习编程的道路往往就是熟悉编程语言的过程。

在这个过程中,很多人会不适应,写出的代码往往都不能运行,更别提设计与抽象。出现这个现象最根本的原因是,代码体现的是计算机思维,而人脑思维和计算机思维差异巨大,很多人一开始无法接受两种思维差异带来的巨大冲击

那么,究竟什么是计算机思维?

计算机思维是全方位的,体现在方方面面,我以个人视角来简单概括一下:

  • 自顶向下:自顶向下是计算机思维的精髓,人脑更加适合自底向上。计算机通过自顶向下思维将大而难的问题拆解为小问题,再将小问题逐一解决,从而最终解决大问题。
  • 多维度、多任务:人脑是线性的,看问题往往是单维的,我们很难同时处理和思考多个问题,但是计算机不一样,它可以有多个 CPU 核心,在保存上下文的基础上能够并发运行成百上千的任务。
  • 全局性:人的精力、脑容量是有限的,而计算机的容量几乎是无限的;人在思考问题时,限于自己的局部性,拿到局部解就开始做了,而计算机可以在海量数据的基础上再做决策,从而逼近全局最优。
  • 协作性:计算机本身就是一件极其精细化的工程艺术品,它复杂精巧,每个部分都只会做自己最擅长的事情,比如将计算和存储剥离,计算机高效运作的背后是每个部分协作的结果,而人更擅长单体作战,只有通过大量的训练,才能发挥群体的作用。
  • 迭代快:人类进化、成长是缓慢的,直到现在,很多人的思维方式仍旧停留在上个世纪,而计算机则不同,进入信息时代后,计算机就遵循着摩尔定律,每 18 个月翻一番,十年前的手机放在今天可能连微信都无法正常运行。
  • 取舍:在长期的社会发展中,人过分喜欢强调对与错,喜欢追求绝对的公平,讽刺的是,由二进制组成的计算机却不会做出非黑即白的决策,无论是计算机本身(硬件),还是里面运行的软件,每一个部分都是性能、成本、易用性多角度权衡的结果。
  • So on…

当这些思维直接体现在代码里面,比如,自顶向下体现在编程语言中就是递归、分治;多维度、多任务的体现就是分支、跳转、上下文;迭代、协作和取舍在编程中也处处可见。

而这些恰恰是人脑思维不擅长的点,所以很多人无法短时间内做到编程入门。想要熟练掌握编程,就必须认识到人脑与计算机思维的差异,强化计算机思维的训练,这个训练的过程是不太可能短暂的,因此编程入门必须要消耗大量的时间和精力

语言

不过思维的训练和评估是需要有载体的,就好比评估你的英文水平,会考察你用英文听/说/读/写的表达能力。那我们的计算机思维怎么表达呢?

于人而言,我们可以通过肢体动作、神情、声音、文字等来表达思维。在漫长的人类史中,动作、神情、声音这几种载体很难传承和传播,直到近代,音、视频的兴起才开始慢慢解决这个问题。

文字,尤其是语言诞生后的文字,成了人类文明延续、发展的主要途径之一,直至今天,我们仍然可以通过文字来与先贤对话。当然,对话的前提是,这些文字你得看得懂。

而看得懂的前提是,我们使用了同一种或类似的语言。

回到计算机上来,现代计算机也是有通用语言的,也就是我们常说的二进制机器语言,专业一点叫指令集。二进制是计算机的灵魂,但是人类却很难理解、记忆和应用,因此为了辅助人类操纵计算机工作,上一代程序员们对机器语言做了第一次抽象,发明了汇编语言。

但伴随着硬件、软件的快速发展,程序代码越来越长,应用变得愈来愈庞大,汇编级别的抽象已经无法满足工程师对快速高效工作的需求了。历史的发展总是如此地相似,当发现语言抽象已经无法满足工作时,工程师们就会在原有层的基础上再抽象出一层,而这一层的著名佼佼者——C语言直接奠定了今天计算机系统的基石。

从此以后,不计其数的编程语言走向计算机的舞台,它们如同满天繁星,吸引了无数的编程爱好者,比如说迈向中年的 Java 和新生代的 Julia。虽然学习计算机最正确的途径不是从语言开始,但学习编程最好、最容易获取成就感的路径确实是应该从语言入手。因此编程语言的重要性不言而喻,它是我们走向编程世界的大门。

C 语言是一种命令式编程语言,命令式是一种编程范式;使用 C 写代码时,我们更多是在思考如何描述程序的运行,通过编程语言来告诉计算机如何执行。

举个例子,使用 C 语言来筛选出一个数组中大于 100 的数字。对应代码如下: int main() { int arr[5] = { 100, 105, 110, 99, 0 }; for (int i = 0; i < 5; ++i) { if (arr[i] > 100) { // do something } } return 0; }

在这个例子中,代码撰写者需要使用数组、循环、分支判断等逻辑来告诉计算机如何去筛选数字,写代码的过程往往就是计算机的执行过程。

而对于另一种语言而言,比如 JavaScript,筛选出大于 100 的数字的代码大概是这样的: let arr = [ 100, 105, 110, 99, 0 ] let result = arr.filter(n => n > 100)

相较于 C 来说,JavaScript 做出了更加高级的抽象,代码撰写者无需关心数组容量、数组遍历,只需将数字丢进容器里面,并在合适的地方加上筛选函数即可,这种编程方式被称为声明式编程

可以看到的是,相较于命令式编程,声明式编程更倾向于表达在解决问题时应该做什么,而不是具体怎么做。这种更高级的抽象不仅能够给开发者带来更加良好的体验,也能让更多非专业人士进入编程这个领域。

不过命令式编程和声明式编程其实并没有优劣之分,主要区别体现在两者的语言特性相较于计算机指令集的抽象程度

其中,命令式编程语言的抽象程度更低,这意味着该类语言的语法结构可以直接由相应的机器指令来实现,适合对性能极度敏感的场景。而声明式编程语言的抽象程度更高,这类语言更倾向于以叙事的方式来描述程序逻辑,开发者无需关心语言背后在机器指令层面的实现细节,适合于业务快速迭代的场景。

不过语言不是一成不变的。编程语言一直在进化,它的进化速度绝对超过了自然语言的进化速度。

在抽象层面上,编程语言一直都停留在机器码 -> 汇编 -> 高级语言这三层上。而对于我们广大开发者来说,我们的目光一直聚焦在高级语言这一层上,所以,高级编程语言也慢慢成为了狭隘的编程语言(当然,这是一件好事,每一类人都应该各司其职做好自己的事情,不用过多担心指令架构、指令集差异带来的麻烦)。

谈到这里,不知你是否发现了一个规律:抽象越低的编程语言越接近计算机思维,而抽象越高越接近人脑思维。

是的。现代层出不穷的编程语言,往往都是在人脑、计算机思维之间的平衡做取舍。那些设计语言的专家们似乎在这个毫无硝烟的战场上博弈,彼此对立却又彼此借鉴。不过哪怕再博弈,按照人类自然语言的趋势来看,也几乎不可能出现一家独大的可能,就像人类目前也是汉语、英语等多种语言共存,即使世界语于 1887 年就被发明,但我们似乎从未见过谁说世界语。

既然高级编程语言那么多,对于有选择困难症的我们,又该做出何种选择呢?

工具

一提到选语言,估计你常听这么一句话,语言是工具。很长一段时间里,我也这么告诫自己,无所谓一门语言的优劣,它仅仅只是一门工具,而我需要做的就是将这门工具用好。语言是表达思想的载体,只要有了思想,无论是何种语言,都能表达。

可当我接触了越来越多的编程语言,对代码、指令、抽象有了更深入的理解之后,我推翻了这个想法,认识到了“语言只是工具”这个说法的狭隘性。

编程语言,显然不仅只是工具,它一定程度上桎梏了我们的思维。

举例来说,使用 Java 或者 C/# 的人能够很轻易地想到对象的设计与封装,那是因为 Java 和 C/# 就是以类作为基本的组织单位,无论你是否有意识地去做这件事,你都已经做了。而对于 C 和 JavaScript 的使用者来说,大家似乎更倾向于使用函数来进行封装。

抛开语言本身的优劣,这是一种思维的惯性,恰恰也印证了上面我谈到的,语言一定程度上桎梏了我们的思维。其实如果从人类语言的角度出发,一个人说中文和说英文的思维方式是大相径庭的,甚至一个人分别说方言和普通话给别人的感觉也像是两个人一样。

Rust

所以如果说思维是我们创造的出发点,那么编程语言,在表达思维的同时,也在一定程度上桎梏了我们的思维。聊到这里,终于到我们今天的主角——Rust这门编程语言出场了。

Rust 是什么?

Rust 是一门高度抽象、性能与安全并重的现代化高级编程语言。我学习、推崇它的主要原因有三点:

  • 高度抽象、表达能力强,支持命令式、声明式、元编程、范型等多种编程范式;
  • 强大的工程能力,安全与性能并重;
  • 良好的底层能力,天然适合内核、数据库、网络。

Rust 很好地迎合了人类思维,对指令集进行了高度抽象,抽象后的表达力能让我们以更接近人类思维的视角去写代码,而 Rust 负责将我们的思维翻译为计算机语言,并且性能和安全得到了极大的保证。简单说就是,完美兼顾了一门语言的思想性和工具性。

仍以前面“选出一个数组中大于 100 的数字”为例,如果使用 Rust,那么代码是这样的: let arr = vec![ 100, 105, 110, 99, 0 ] let result = arr.iter().filter(n => n > 100).collect();

如此简洁的代码会不会带来性能损耗,Rust 的答案是不会,甚至可以比 C 做到更快。

我们对应看三个小例子的实现思路/要点,来感受一下 Rust 的语言表达能力、工程能力和底层能力。

简单协程

Rust 可以无缝衔接到 C、汇编代码,这样我们就可以跟下层的硬件打交道从而实现协程。

实现也很清晰。首先,定义出协程的上下文: /#[derive(Debug, Default)] /#[repr(C)] struct Context { rsp: u64, // rsp 寄存器 r15: u64, r14: u64, r13: u64, r12: u64, rbx: u64, rbp: u64, } /#[naked] unsafe fn ctx_switch() { // 注意:16 进制 llvm_asm!( “ mov %rsp, 0x00(%rdi) mov %r15, 0x08(%rdi) mov %r14, 0x10(%rdi) mov %r13, 0x18(%rdi) mov %r12, 0x20(%rdi) mov %rbx, 0x28(%rdi) mov %rbp, 0x30(%rdi) mov 0x00(%rsi), %rsp mov 0x08(%rsi), %r15 mov 0x10(%rsi), %r14 mov 0x18(%rsi), %r13 mov 0x20(%rsi), %r12 mov 0x28(%rsi), %rbx mov 0x30(%rsi), %rbp “ ); }

结构体 Context 保存了协程的运行上下文信息(寄存器数据),通过函数 ctx_switch,当前协程就可以交出 CPU 使用权,下一个协程接管 CPU 并进入执行流。

然后我们给出协程的定义: /#[derive(Debug)] struct Routine { id: usize, stack: Vec, state: State, ctx: Context, }

协程 Routine 有自己唯一的 id、栈 stack、状态 state,以及上下文 ctx。Routine 通过 spawn 函数创建一个就绪协程,yield 函数会交出 CPU 执行权:

pub fn spawn(&mut self, f: fn()) { // 找到一个可用的 // let avaliable = …. let sz = avaliable.stack.len(); unsafe { let stack_bottom = avaliable.stack.as_mut_ptr().offset(sz as isize); // 高地址内存是栈顶 let stack_aligned = (stack_bottom as usize & !15) as /mut u8; std::ptr::write(stack_aligned.offset(-16) as /mut u64, guard as u64); std::ptr::write(stack_aligned.offset(-24) as /mut u64, hello as u64); std::ptr::write(stack_aligned.offset(-32) as /mut u64, f as u64); avaliable.ctx.rsp = stack_aligned.offset(-32) as u64; // 16 字节对齐 } avaliable.state = State::Ready; } pub fn r/#yield(&mut self) -> bool { // 找到一个 ready 的,然后让其运行 let mut pos = self.current; //….. self.routines[pos].state = State::Running; let old_pos = self.current; self.current = pos; unsafe { let old: /mut Context = &mut self.routines[old_pos].ctx; let new: /const Context = &self.routines[pos].ctx; llvm_asm!( “mov $0, %rdi mov $1, %rsi”::”r”(old), “r”(new) ); ctx_switch(); } self.routines.len() > 0 }

运行结果如下:

1 STARTING routine: 1 counter: 0 2 STARTING routine: 2 counter: 0 routine: 1 counter: 1 routine: 2 counter: 1 routine: 1 counter: 2 routine: 2 counter: 2 routine: 1 counter: 3 routine: 2 counter: 3 routine: 1 counter: 4 routine: 2 counter: 4 routine: 1 counter: 5 routine: 2 counter: 5 routine: 1 counter: 6 routine: 2 counter: 6 routine: 1 counter: 7 routine: 2 counter: 7 routine: 1 counter: 8 routine: 2 counter: 8 routine: 1 counter: 9 routine: 2 counter: 9 1 FINISHED

具体代码实现参考协程

简单内核

操作系统内核是一个极为庞大的工程,但是如果只是写个简单内核输出 Hello World,那么 Rust 就能很快完成这个任务。你可以自己体验一下。

首先,添加依赖工具: rustup component add llvm-tools-preview cargo install bootimage

然后编辑 main.rs 文件输出一个 Hello World:

/#![no_std] /#![no_main] use core::panic::PanicInfo; static HELLO:&[u8] = b”Hello World!”; /#[no_mangle] pub extern “C” fn _start() -> ! { let vga_buffer = 0xb8000 as /mut u8; for (i, &byte) in HELLO.iter().enumerate() { unsafe { /vga_buffer.offset(i as isize /* 2) = byte; /vga_buffer.offset(i as isize / 2 + 1) = 0xb; } } loop{} } /#[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} }

然后编译、打包运行:

cargo bootimage cargo run

运行结果如下:-

具体代码实现参考内核

简单网络协议栈

同操作系统一样,网络协议栈也是一个庞大的工程系统。但是借助 Rust 和其完备的生态,我们可以迅速完成一个小巧的 HTTP 协议栈。

首先,在数据链路层,我们定义 Mac 地址结构体: /#[derive(Debug)] pub struct MacAddress([u8; 6]); impl MacAddress { pub fn new() -> MacAddress { let mut octets: [u8; 6] = [0; 6]; rand::thread_rng().fill_bytes(&mut octets); // 1. 随机生成 octets[0] |= 0b_0000_0010; // 2 octets[1] &= 0b_1111_1110; // 3 MacAddress { 0: octets } } }

MacAddress 用来表示网卡的物理地址,此处的 new 函数通过随机数来生成随机的物理地址。

然后实现 DNS 域名解析函数,通过 IP 地址获取 MAC 地址,如下: pub fn resolve( dns_server_address: &str, domain_name: &str, ) -> Result<Option<std::net::IpAddr>, Box> { let domain_name = Name::from_ascii(domain_name).map_err(DnsError::ParseDomainName)?; let dns_server_address = format!("{}:53", dns_server_address); let dns_server: SocketAddr = dns_server_address .parse() .map_err(DnsError::ParseDnsServerAddress)?; // .... let mut encoder = BinEncoder::new(&mut request_buffer); request.emit(&mut encoder).map_err(DnsError::Encoding)?; let _n_bytes_sent = localhost .send_to(&request_buffer, dns_server) .map_err(DnsError::Sending)?; loop { let (_b_bytes_recv, remote_port) = localhost .recv_from(&mut response_buffer) .map_err(DnsError::Receiving)?; if remote_port == dns_server { break; } } let response = Message::from_vec(&response_buffer).map_err(DnsError::Decoding)?; for answer in response.answers() { if answer.record_type() == RecordType::A { let resource = answer.rdata(); let server_ip = resource.to_ip_addr().expect("invalid IP address received"); return Ok(Some(server_ip)); } } Ok(None) }

接着实现 HTTP 协议的 GET 方法:

pub fn get( tap: TapInterface, mac: EthernetAddress, addr: IpAddr, url: Url, ) -> Result<(), UpstreamError> { let domain_name = url.host_str().ok_or(UpstreamError::InvalidUrl)?; let neighbor_cache = NeighborCache::new(BTreeMap::new()); // TCP 缓冲区 let tcp_rx_buffer = TcpSocketBuffer::new(vec![0; 1024]); let tcp_tx_buffer = TcpSocketBuffer::new(vec![0; 1024]); let tcp_socket = TcpSocket::new(tcp_rx_buffer, tcp_tx_buffer); let ip_addrs = [IpCidr::new(IpAddress::v4(192, 168, 42, 1), 24)]; let fd = tap.as_raw_fd(); let mut routes = Routes::new(BTreeMap::new()); let default_gateway = Ipv4Address::new(192, 168, 42, 100); routes.add_default_ipv4route(default_gateway).unwrap(); let mut iface = EthernetInterfaceBuilder::new(tap) .ethernet_addr(mac) .neighbor_cache(neighbor_cache) .ip_addrs(ip_addrs) .routes(routes) .finalize(); let mut sockets = SocketSet::new(vec![]); let tcp_handle = sockets.add(tcp_socket); // HTTP 请求 let http_header = format!( “GET {} HTTP/1.0\r\nHost: {}\r\nConnection: close\r\n\r\n”, url.path(), domain_name, ); let mut state = HttpState::Connect; ‘http: loop { let timestamp = Instant::now(); match iface.poll(&mut sockets, timestamp) { Ok() => {} Err(smoltcp::Error::Unrecognized) => {} Err(e) => { eprintln!(“error: {:?}”, e); } } { let mut socket = sockets.get::(tcp_handle); state = match state { HttpState::Connect if !socket.is_active() => { eprintln!("connecting"); socket.connect((addr, 80), random_port())?; HttpState::Request } HttpState::Request if socket.may_send() => { eprintln!("sending request"); socket.send_slice(http_header.as_ref())?; HttpState::Response } HttpState::Response if socket.can_recv() => { socket.recv(|raw_data| { let output = String::from_utf8_lossy(raw_data); println!("{}", output); (raw_data.len(), ()) })?; HttpState::Response } HttpState::Response if !socket.may_recv() => { eprintln!("received complete response"); break 'http; } _ => state, } } phy_wait(fd, iface.poll_delay(&sockets, timestamp)).expect("wait error"); } Ok(()) }

最后在 main 函数中使用 HTTP GET 方法:

fn main() { // … let tap = TapInterface::new(&tap_text).expect( “error: unable to use as a \ network interface", ); let domain_name = url.host_str().expect("domain name required"); let _dns_server: std::net::Ipv4Addr = dns_server_text.parse().expect( "error: unable to parse as an \ IPv4 address", ); let addr = dns::resolve(dns_server_text, domain_name).unwrap().unwrap(); let mac = ethernet::MacAddress::new().into(); http::get(tap, mac, addr, url).unwrap(); }

运行程序,结果如下:

$ ./target/debug/rget http://www.baidu.com tap-rust HTTP/1.0 200 OK Accept-Ranges: bytes Cache-Control: no-cache Content-Length: 9508 Content-Type: text/html

具体代码实现参考协议栈

通过这三个简单的小例子,无论是协程、内核还是协议栈,这些听上去都很高大上的技术,在 Rust 强大的表现力、生态和底层能力面前显得如此简单和方便。

思维是出发点,语言是表达体,工具是媒介,而 Rust 完美兼顾了一门语言的思想性和工具性,赋予了我们极强的工程表达能力和完成能力。

总结

作为极其现代的语言,Rust 集百家之长而成,将性能、安全、语言表达力都做到了极致,但同时也带来了巨大的学习曲线。

初学时,每天都要和编译器做斗争,每次编译都是满屏的错误信息;攻克一个陡坡后,发现后面有更大的陡坡,学习的道路似乎无穷无尽。那我们为什么要学习 Rust ?

这里引用左耳朵耗子的一句话: 如果你对 Rust 的概念认识得不完整,你完全写不出程序,那怕就是很简单的一段代码。这逼着程序员必须了解所有的概念才能编码。

Rust 是一个对开发者极其严格的语言,严格到你学的不扎实,就不能写程序,但这无疑也是一个巨大的机会,改掉你不好的编码习惯,锻炼你的思维,让你成为真正的大师

聊到这里,你是否已经对 Rust 有了更深的认识和更多的激情,那么放手去做吧!期待你与 Rust 擦出更加明亮的火花!

参考资料

参考资料

https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e9%99%88%e5%a4%a9%20%c2%b7%20Rust%20%e7%bc%96%e7%a8%8b%e7%ac%ac%e4%b8%80%e8%af%be/%e7%94%a8%e6%88%b7%e6%95%85%e4%ba%8b%20%e8%af%ad%e8%a8%80%e4%b8%8d%e4%bb%85%e6%98%af%e5%b7%a5%e5%85%b7%ef%bc%8c%e8%bf%98%e6%98%af%e6%80%9d%e7%bb%b4%e6%96%b9%e5%bc%8f.md