被诅咒的 Rust:用错误的方式打印
原文由 Matthias Endler 于 发布,订阅该博客

在哥本哈根大学的一次考试中,流传着一个关于物理学家的著名故事。考生被要求描述如何用气压计来测定摩天大楼的高度。那名学生建议用一根绳子把气压计从楼顶吊下去,然后测量绳子的长度再加上气压计本身的高度。虽然在技术上完全正确,考官们却不以为然。
在申诉并重新考核后,这名学生又提出了多种基于物理学的解法,既有让气压计自由落体、通过下落时间来计算楼高的,也有利用大楼影长与气压计影长的比例,结合气压计的高度来推算大楼高度的。他甚至还幽默地提议,直接把气压计送给看门人来换取答案。
据传说,这位物理学家就是尼尔斯·玻尔,他后来在 1922 年获得了诺贝尔奖。这个故事也被称为气压计问题。
这个故事为什么有趣?
这个问题及其各种可能的答案具有重要的教学意义:它向学习者传达了一个道理——也可以用非常规的方法找到答案——而这些方法往往比标准答案更有意思,因为它们揭示了问题本身的一些本质。
从对常规问题的非常规回答中学习是大有裨益的。在某种程度上,这能培养新的思考和解决问题的方式,而这正是创新的关键所在。
把同样的原则应用到学习 Rust 上
任何一本 Rust 入门书里的第一个例子几乎都是“Hello, world!”程序。
fn main() {
println!("Hello, world!");
}这是检验 Rust 安装是否正常的简便方法。
不过,我们也可以换个角度找点乐子:想想不用 println! 来打印“Hello, world!”的各种办法。
我们来尽可能多地想出一些非常规的解法。越古怪越好!在浏览下面的每一种解法时,试着去理解它们为什么能奏效,以及你能从中学到什么。
这起初只是个梗,但在帖子引发大量关注后,我决定把它写成一篇完整的文章。
不用说,这些解法都不该用在生产代码中。不如去看看这个企业级就绪的 hello world 版本。
解法 1:剥开 println! 的语法糖
use std::io::Write;
write!(std::io::stdout().lock(), "Hello, world!");这个解法很有意思,因为它表明 println! 不过是一个宏,展开后就是对 write! 的调用,只是在字符串末尾追加了一个换行符。
真正的代码则要古怪得多。如果你想大开眼界,可以在这个文件中搜索 print。write! 本身则会脱糖为对 write_fmt 的调用,而 write_fmt 是 Write trait 的一个方法。
这其实有一个实际用例:如果你想极快地打印,可以先锁定 stdout,然后再使用 write!。这样可以避免每次调用 println! 时都要锁定 stdout 的开销。可以看看这篇关于如何用这个技巧写出超快版 yes 的文章。
解法 2:逐字符迭代
"Hello, world!".chars().for_each(|c| print!("{}", c));这说明你可以用 Rust 强大的迭代器来实现 println!。在这里,我们遍历字符串中的每个字符并逐一打印。
chars() 返回的是一个遍历 Unicode 标量值的迭代器。
在这里了解更多关于迭代器的知识。
解法 3:实现 Display
struct HelloWorld;
impl std::fmt::Display for HelloWorld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Hello, world!")
}
}
println!("{HelloWorld}");这让我们对 Rust 中 trait 的工作方式有了些许了解:我们定义了一个实现了 Display trait 的结构体,这样就可以用 print! 来打印它。一般来说,Display 是为了让更复杂的类型变得可打印,但为一个写死的字符串实现它也是完全可行的!
解法 4:谁还需要 Display?
如果我们不用 Display,而是自己创建一个 trait 呢?
trait Println {
fn println(&self);
}
impl Println for &str {
fn println(&self) {
print!("{}", self);
}
}
"Hello, world!".println();我们可以利用 trait 方法可以随意命名这一点。在这个例子中,我们选择了 println 这个名字,让它看起来像是标准库的一部分。
这完全颠覆了 println! 宏的用法。我们不再把字符串当作参数传进去,而是直接在字符串本身上调用方法!
解法 5:有了 panic! 还要什么 println!?
panic!("Hello, world!");在 Rust 中除了 println! 还有其他打印方式。在这里,我们用了 panic!,它会(作为副作用)打印字符串并立即终止程序。只要我们只想打印一个字符串,这招就管用……
解法 6:我 ♥︎️ 闭包
(|s: &str| print!("{}", s))("hello");Rust 允许你在定义闭包后直接调用它。这个闭包被定义为一个匿名函数,接受一个字符串切片作为参数并将其打印出来。字符串切片则作为参数传给了闭包。
在实践中,这对于定义只使用一次、又不想费心命名的闭包很有用。
解法 7:C 风格
extern crate libc;
use libc::{c_char, c_int};
use core::ffi::CStr;
extern "C" {
fn printf(fmt: *const c_char, ...) -> c_int;
}
fn main() {
const HI: &CStr = match CStr::from_bytes_until_nul(b"hello\n\0") {
Ok(x) => x,
Err(_) => panic!(),
};
unsafe {
printf(HI.as_ptr());
}
}你甚至不需要用 Rust 标准库来打印!这个例子展示了如何在 Rust 中调用 C 标准库的 printf 函数。这是不安全的,因为我们使用裸指针来向函数传递字符串。这让我们对 Rust 中的 FFI 工作方式有了初步了解。
感谢 Reddit 上的 /u/pinespear 和 @[email protected]。
解法 8:C++ 风格
我们现在已经完全进入了“变态”领域……所以别停下来。如果你足够努力,完全可以让 Rust 屈服于你的意志,让它看起来像 C++。
use std::fmt::Display;
use std::ops::Shl;
#[allow(non_camel_case_types)]
struct cout;
#[allow(non_camel_case_types)]
struct endl;
impl<T: Display> Shl<T> for cout {
type Output = cout;
fn shl(self, data: T) -> Self::Output {
print!("{}", data);
cout
}
}
impl Shl<endl> for cout {
type Output = ();
fn shl(self, _: endl) -> Self::Output {
println!();
}
}
cout << "Hello World" << endl;Shl trait 用于实现 << 运算符。cout 结构体为所有实现了 Display 的类型实现了 Shl,因此我们可以打印任何可打印的类型。endl 结构体则为 cout 实现了 Shl,用于在最后打印换行符。
这个解法归功于 Wisha Wanichwecharungruang。
解法 9:用汇编实现纯粹的控制
所有这些高级抽象都阻碍了高效打印。我们必须夺回对 CPU 的控制权。汇编才是正道。不再浪费周期。没有隐藏指令。纯粹、极致的性能。
use std::arch::asm;
const SYS_WRITE: usize = 1;
const STDOUT: usize = 1;
fn main() {
#[cfg(not(target_arch = "x86_64"))]
panic!("This only works on x86_64 machines!");
let phrase = "Hello, world!";
let bytes_written: usize;
unsafe {
asm! {
"syscall",
inout("rax") SYS_WRITE => bytes_written,
inout("rdi") STDOUT => _,
in("rsi") phrase.as_ptr(),
in("rdx") phrase.len(),
// syscall clobbers these
out("rcx") _,
out("r11") _,
}
}
assert_eq!(bytes_written, phrase.len());
}如果你在想,既然全是调汇编代码,那当初为什么还要用 Rust,那你就没抓住重点!这远不止是打印。它关乎自由!别来告诉我该怎么用我的 CPU。
好吧,它只能在 x86_64 机器上运行,但为了自由,这点牺牲不算什么。
由 isaacthefallenapple 提交。
解法 10:“极速”
如果我们根本不用那些 CPU 核心,那当初花大价钱买它们干什么?无畏并发不正是 Rust 的承诺之一吗?让我们把这些核心好好利用起来!
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let phrase = "hello world";
let phrase = Arc::new(Mutex::new(phrase.chars().collect::<Vec<_>>()));
let mut handles = vec![];
for i in 0..phrase.lock().unwrap().len() {
let phrase = Arc::clone(&phrase);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(((i + 1) * 100) as u64));
print!("{}", phrase.lock().unwrap()[i]);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!();
}在这里,每个字符都在单独的线程中打印。线程在循环中创建,每个线程在打印自己的字符前会休眠一定的毫秒数。这可是动用了 CPU 的全部力量来打印一个字符串!它可能并不总能保证按正确顺序打印字符(嘿,调度可不是件容易事!),但为了那原始的性能提升,这点代价是值得的。
轮到你了!
如果你还有更多解法,欢迎给我留言。
另外,如果你喜欢这篇文章,你可能也会喜欢一年一度的国际 C 语言混乱代码大赛。可以在这里查看历届获奖作品。
如果你其实对气压计的故事更感兴趣,可以读一读理查德·费曼的书《别闹了,费曼先生!》,他是另一位以思维不拘一格而著称的著名物理学家和诺贝尔奖得主。
我们都应该努力跳出思维定式,为问题想出非常规的解法。谁知道呢,也许这正是更深刻理解问题本身的关键?
随机一篇博客
评论
登录后参与讨论