被诅咒的 Rust:用错误的方式打印

关于哥本哈根大学考试中的一位物理学家,有一个著名的故事。考生被要求描述如何使用气压计来测定摩天大楼的高度。该学生建议用一根绳子将气压计从楼顶吊下去,然后测量绳子的长度加上气压计本身的高度。虽然在技术上是正确的,但考官们并不觉得有趣。
在提出申诉并获得重新评估的机会后,该学生提供了多种基于物理学的解法,范围从扔下气压计并利用下落时间来计算大楼高度,到利用大楼阴影与气压计阴影长度之间的比例,通过气压计的高度来计算大楼的高度。他甚至幽默地建议,直接以气压计为报酬去询问管理员就能得到答案。
据传说,这位物理学家就是Niels Bohr(尼尔斯·玻尔),他后来在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! 只是一个 macro(宏),会展开为对 write! 的调用,并在字符串末尾追加一个换行符。
真正的代码要奇怪得多。如果你想大开眼界,就在这个文件中搜索 print。write! 本身会解糖为对 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! macro。我们不再将字符串作为参数传入,而是直接在字符串本身上调用方法!
解法 5:有了 panic! 还要什么 println!?
panic!("Hello, world!");在 Rust 中,除了使用 println! 还有其他打印方式。在这种情况下,我们使用 panic!,它会打印字符串(作为副作用)并立即终止程序。只要我们只想打印单个字符串,它就能奏效……
解法 6:我 ♥︎️ 闭包
(|s: &str| print!("{}", s))("hello");Rust 允许你在定义 closure(闭包) 后直接调用它。该 closure 被定义为一个匿名函数,它接受一个字符串切片作为参数并将其打印出来。字符串切片作为参数被传递给该 closure。
在实践中,这对于定义仅使用一次且你不想为其命名的 closure 很有用。
解法 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 函数。由于我们使用原始指针将字符串传递给函数,所以它是不安全的。这让我们对 FFI(外部函数接口) 在 Rust 中的工作原理有了一些了解。
鸣谢 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 语言混乱代码大赛。请在这里查看往届获奖者。
如果你其实对气压计的故事更感兴趣,可以读一读Surely You’re Joking, Mr. Feynman!(《别闹了,费曼先生!》),这是一本由 Richard Feynman(理查德·费曼)所著的书,他是另一位著名的物理学家和诺贝尔奖得主,以其非传统的思维方式而闻名。
我们都应该努力跳出思维定式,为问题想出非常规的解决方案。谁知道呢,也许这正是更深入理解问题本身的关键?
随机一篇博客