Rust 中的奇怪表达式

Rust 拥有非常强大的类型系统,但因此也存在一些怪癖,有些人甚至称其为“可恶的表达式”。rust 存储库中有一个测试文件 weird-expr.rs,用于测试其中的一些表达式,并确保它们在更新之间保持一致。因此,我想逐一介绍这些表达式,并解释它们在 rust 中是如何有效的。

请注意,这些并不是错误,而是 rust 功能(如循环、表达式、强制转换等)的极端情况。

奇怪

fn strange() -> bool {let _x:bool = return true;}

表达式 return true 的类型为 !。never 类型可以强制转换为任何其他类型,因此我们可以将其赋值给布尔值。

有趣

fn funny(){
	fn f(_x: ()){}
	f(return);
}

函数 f 接受一个类型为 () 的参数,我们再次可以传递 return,因为 ! 将被强制转换为 ()元素周期表

什么

use std::cell::Cell;

fn what(){
	fn the(x: &Cell<bool>){
		return while !x.get() {x.set(true);};
	}
	let i = &Cell::new(false);
	let dont = {||the(i)};
	dont();
	assert!(i.get());
}

函数 the 接受一个 Cell<bool> 的引用。在函数内部,我们使用一个 while 循环

while !x.get() {x.set(true);}

来设置单元格的值为 true,如果其内容为 false,然后返回该 while 循环,其类型为 ()。接下来,我们创建一个变量 i,它是 Cell<bool> 的引用,并绑定一个闭包,该闭包调用 the 函数并传入 i 作为参数,然后调用该闭包并断言 i 为 true。

僵尸耶稣

fn zombiejesus() {  
    loop {  
        while (return) {  
            if (return) {  
                match (return) {  
                    1 => {  
                        if (return) {  
                            return  
                        } else {  
                            return  
                        }  
                    }                    
                    _ => { return }  
                };  
            } else if (return) {  
                return;  
            }  
        }        
        if (return) { break; }  
    }
}

表达式 (return) 的类型为 never,由于 never 类型可以转换为任何类型,因此可以在所有这些位置使用它。在 ifwhile 语句中,它会被转换为布尔类型,在 match 语句中,它会被转换为任何类型。

let screaming = match(return){
	"aahhh" => true,
	_ => false
};

不确定

use std::mem::swap;

fn notsure() {
    let mut _x: isize;
    let mut _y = (_x = 0) == (_x = 0);
    let mut _z = (_x = 0) < (_x = 0);
    let _a = (_x += 0) == (_x = 0);
    let _b = swap(&mut _y, &mut _z) == swap(&mut _y, &mut _z);
}

我们有一个未初始化的变量 _x,我们将 _y 赋值为 (_x = 0) == (_x = 0)(_x = 0) 评估为单位类型,因此 _y 为真。_z_a 的情况类似,但 _z 为假,因为 () 不小于它自己。_b 也为真,因为 swap 返回 ()

不能触碰这个


fn canttouchthis() -> usize {
    fn p() -> bool { true }
    let _a = (assert!(true) == (assert!(p())));
    let _c = (assert!(p()) == ());
    let _b: bool = (println!("{}", 0) == (return 0));
}

函数 p() 返回一个布尔值,assert! 宏返回 (),因此 _a_c 均为真。在最后一行,_b 被赋值为表达式

(println!("{}"),0) == (return 0))

println! 返回宏返回 (),而 (return 0)!,它会被强制转换为 (),因此表达式有效。这一行还返回 0,使函数签名有效。

愤怒的穹顶

fn angrydome() {  
    loop { if break { } }  
    let mut i = 0;  
    loop {   
		i += 1;   
		if i == 1 { 
			match (continue) { 
				1 => { }, 
				_ => panic!("wat") } 
			}  
	        break;   
		}  
}

在第一行我们立即退出循环,因为 break 是有效表达式,其类型为 !,因此可在 if 语句中使用。在下一部分,我们将 i 赋值为 0。在循环中我们递增 i,由于 i 现在为 1,因此 if 语句将在第一次迭代中执行。我们匹配 (continue),其类型为 !,循环跳转到下一次迭代,我们再次递增 i,使其现在为 2。由于 if 语句未执行,循环退出且函数返回。

联合

fn union() {
    union union<'union> { union: &'union union<'union>, }
}

Rust 有 三类 关键字:

  • 严格关键字,只能在正确的上下文中使用
  • 保留关键字,为将来使用而保留,但与严格关键字具有相同的限制
  • 弱关键字,仅在某些上下文中具有特殊含义

union 是一个弱关键字,仅在联合声明中使用时才是关键字,因此我们可以在其他上下文中使用它,例如函数名。

穿孔卡

fn punch_card() -> impl std::fmt::Debug {
    ..=..=.. ..    .. .. .. ..    .. .. .. ..    .. .. .. ..
    ..=.. ..=..    .. .. .. ..    .. .. .. ..    .. ..=.. ..
    ..=.. ..=..    ..=.. ..=..    .. ..=..=..    ..=..=..=..
    ..=..=.. ..    ..=.. ..=..    ..=.. .. ..    .. ..=.. ..
    ..=.. ..=..    ..=.. ..=..    .. ..=.. ..    .. ..=.. ..
    ..=.. ..=..    ..=.. ..=..    .. .. ..=..    .. ..=.. ..
    ..=.. ..=..    .. ..=..=..    ..=..=.. ..    .. ..=..=..
}

在 Rust 中,.. 代表一个无界范围(std::ops::RangeFull),通常用于切片。同样,..= 代表一个包括该值在内的范围(std::ops::RangeToInclusive)。所有不同的范围都有类型,您可以在 std::ops 模块文档 中看到。范围可以组合成您想要的任何组合:

use std::ops::{RangeFull, RangeTo, RangeToInclusive};

let _a: RangeToInclusive<RangeTo<RangeFull>> =  ..=.. .. ;

所有这些范围类型都实现了 Debug,这满足了 impl std::fmt::Debug 的返回类型。

猴子桶

fn monkey_barrel() {
    let val: () = ()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=()=();
    assert_eq!(val, ());
}

在 Rust 中,赋值表达式 由左侧 被赋值表达式、等号(=)和右侧的 值表达式 组成。元组模式可作为赋值表达式使用,即它可以出现在赋值表达式的左侧部分。大多数情况下,我们使用它来赋值解构值。

let (x,y) = (110.0,50.5);

但元组也可以为空,这意味着我们将其赋值给 () 类型。

let () = ();

由于赋值返回 (),我们可以链式调用它们

let () = ()=()=();

分号

fn semisemisemisemisemi() {
    ;;;;;;; ;;;;;;; ;;;    ;;; ;;
    ;;      ;;      ;;;;  ;;;; ;;
    ;;;;;;; ;;;;;   ;; ;;;; ;; ;;
         ;; ;;      ;;  ;;  ;; ;;
    ;;;;;;; ;;;;;;; ;;      ;; ;;
}

您可以在块中的任何位置添加分号,这会创建一个空语句并返回空值 ()。因此这些分号只是创建了一系列空语句。

有用的语法

fn useful_syntax() {  
    use {{std::{{collections::{{HashMap}}}}}};  
    use ::{{{{core}, {std}}}};  
    use {{::{{core as core2}}}};  
}

Rust 允许使用分组的 use 语句来减少冗余代码。这些大括号也可以用于语句的根部,且使用大括号的数量没有限制。

use {std::sync::Arc};
use core::{mem::{{transmute}}};

无限模块

fn infcx() {
    pub mod cx {
        pub mod cx {
            pub use super::cx;
            pub struct Cx;
        }
    }
    let _cx: cx::cx::Cx = cx::cx::cx::cx::cx::Cx;
}

我们声明一个模块 cx,然后创建另一个同名子模块 cx。以下代码行

pub use super::cx;

实际上是从自身重新导出该模块,这意味着我们可以递归调用它。如果更改名称会更直观。

pub mod outer{  
    pub mod inner{  
        pub use super::inner;  
        pub struct Item;  
    }  
}  
  
let _item: outer::inner::Item = outer::inner::inner::inner::Item;

鱼之争

fn fish_fight() {
    trait Rope {
        fn _____________<U>(_: Self, _: U) where Self: Sized {}
    }

    struct T;

    impl Rope for T {}

    fn tug_o_war(_: impl Fn(T, T)) {}

    tug_o_war(<T>::_____________::<T>);
}

Rope 特性提供了一个带有通用 U 的方法,它接受两个参数,一个是 Self 类型,另一个是 U 类型。我们创建了一个结构体 T,并为其实现了 Ropetug_of_war 函数接受任何实现 Fn(T,T) 的函数或闭包。表达式 <T>::_____________::<T> 是带通用类型 T 的完全限定函数指针(fn(T,T))。由于两个参数类型相同,我们可以将其传递给 tug_of_war

fn dots() {
    assert_eq!(String::from(".................................................."),
               format!("{:?}", .. .. .. .. .. .. .. .. .. .. .. .. ..
                               .. .. .. .. .. .. .. .. .. .. .. ..));
}

范围语法(std::ops::RangeFull)实现了 Debug 并以 “..” 格式化。因此我们可以将它们链式调用以生成点字符串。

u8

fn u8(u8: u8) {  
    if u8 != 0u8 {  
        assert_eq!(8u8, {  
            macro_rules! u8 {  
                (u8) => {  
                    mod u8 {  
                        pub fn u8<'u8: 'u8 + 'u8>(u8: &'u8 u8) -> &'u8 u8 {  
                            "u8";  
                            u8  
                        }  
                    }                
                };  
            }
            u8!(u8);  
            let &u8: &u8 = u8::u8(&8u8);  
            crate::u8(0u8);  
            u8  
        });  
    }  
}

让我们分解一下,我们有一个宏 u8!,它声明了一个模块 u8,该模块声明了一个函数 u8,该函数接受一个名为 u8 的参数,类型为 u8,并返回对 u8 的引用。

macro_rules! u8 {  
    (u8) => {  
        mod u8 {  
            pub fn u8<'u8: 'u8 + 'u8>(u8: &'u8 u8) -> &'u8 u8 {  
                "u8";  
                u8  
	        }  
        }                
    };  
}

接下来,我们调用 u8::u8(&8u8),并将其赋值给一个变量(u8)。下一行调用 crate::u8(0u8),最后,我们从整个表达式中返回 u8 变量。

继续

fn 𝚌𝚘𝚗𝚝𝚒𝚗𝚞𝚎() {  
    type 𝚕𝚘𝚘𝚙 = i32;  
    fn 𝚋𝚛𝚎𝚊𝚔() -> 𝚕𝚘𝚘𝚙 {  
        let 𝚛𝚎𝚝𝚞𝚛𝚗 = 42;  
        return 𝚛𝚎𝚝𝚞𝚛𝚗;  
    }  
    assert_eq!(loop {  
        break 𝚋𝚛𝚎𝚊𝚔 ();  
    }, 42);  
}

这些使用的是 Unicode 等宽字符,而不是普通的 ASCII 字符作为标识符,这不会违反 Rust 使用关键字作为标识符的规则。

Fishy

fn fishy() {
    assert_eq!(
	    String::from("><>"),
        String::<>::from::<>("><>").chars::<>().rev::<>().collect::<String>()
    );
}

Rust 在添加泛型和生命周期时使用涡轮鱼语法。我们可以使用空尖括号来明确指定空泛型。

特殊字符

fn special_characters() {
    let val = !((|(..):(_,_),(|__@_|__)|__)((&*"\",'🤔')/**/,{})=={&[..=..][..];})//
    ;
    assert!(!val);
}

让我们解析右侧表达式:

let val = &[..=..][..];

我们创建一个指向包含范围 &[..=..] 的切片的引用,然后从中获取整个切片。现在解析左侧表达式:

let val = (|(..):(_,_),(|__@_|__)|__)((&*"\",'🤔')/**/,{});

我们有一个带两个参数的闭包,第一个参数是一个元组,类型为自动推断。

let val = |(..):(_,_)|{};

第二个参数是一个具有 绑定 的闭包,变量 __ 绑定到通配符模式(_),该模式将匹配任何内容。

let val = |(..):(_,_),(|__@_|__)|{};

然后我们立即调用该闭包,传入一个包含字符串和字符的元组,以及一个空块。

let val = (|(..):(_,_),(|__@_|__)|)((&*"\",'🤔'),{})

匹配

fn r#match() {
    let val: () = match match match match match () {
        () => ()
    } {
        () => ()
    } {
        () => ()
    } {
        () => ()
    } {
        () => ()
    };
    assert_eq!(val, ());
}

这只是匹配嵌套的 match 语句。

嵌套的 if 匹配

fn match_nested_if() {
    let val = match () {
        () if if if if true {true} else {false} {true} else {false} {true} else {false} => true,
        _ => false,
    };
    assert!(val);
}

这是带有嵌套 if 语句的 匹配守卫

函数


fn function() {
    struct foo;
    impl Deref for foo {
        type Target = fn() -> Self;
        fn deref(&self) -> &Self::Target {
            &((|| foo) as _)
        }
    }
    let foo = foo () ()() ()()() ()()()() ()()()()();
}

当一种类型可以隐式转换为另一种类型时,会使用 Deref 特性,它通常由智能指针使用,以便它们可以隐式地用于底层类型。我们将 foo 的 Deref 实现为一个返回 foo 的函数指针,这意味着我们可以再次递归地调用该 foo。

厕所隔间

fn bathroom_stall() {
    let mut i = 1;
    matches!(2, _|_|_|_|_|_ if (i+=1) != (i+=1));
    assert_eq!(i, 13);
}

在匹配臂中,多个模式可以匹配到一个臂中,用 | 分隔。

let foo = 'a';  
match foo {   
	'a'..'c'|'x'..'z' => {}  
    _ => {}  
}

matches! 宏与匹配语句具有相同的语法,因此我们也可以链式连接多个模式,即使这些模式是通配符模式。

matches!((),_|_|_|_|_|_)
matches!(2, _|_|_|_|_|_ if (i+=1) != (i+=1));

这里有六个不同的模式,它们都做同样的事情:我们检查 i +=1 != i += 1,这会将 i 增加两次,因此每次迭代都会将 i 增加 2。6 x 2 = 12 加上 1(初始值),最终值为 13,因此断言 assert_eq!(i,13) 为真。match!(2,..) 不会引发 panic,因为这是一个通配符模式,因此可以使用任何值。if 语句始终为假,因为右侧表达式始终比左侧多 1,因此它将运行直到所有模式都被尝试。

闭包匹配

fn closure_matching() {
    let x = |_| Some(1);
    let (|x| x) = match x(..) {
        |_| Some(2) => |_| Some(3),
        |_| _ => unreachable!(),
    };
    assert!(matches!(x(..), |_| Some(4)));
}

x 是一个闭包,它接受一个类型未指定的参数,该类型将通过其使用情况推断出来。接下来我们使用 match x(..),这使得闭包的类型为 RangeFull。看起来我们是在匹配闭包,但实际上只是多个通配符模式。数字也不重要,尽管看起来函数每次都在递增,因为它是通配符,任何值都会匹配。

已经返回

fn return_already() -> impl std::fmt::Debug {
    loop {
        return !!!!!!!
        break !!!!!!1111
    }
}

break 表达式对整数反复应用 not 操作,而 return 表达式 也对 break 表达式反复应用 not 操作。

伪宏

fn fake_macros() -> impl std::fmt::Debug {
    loop {
        if! {
            match! (
                break! {
                    return! {
                        1337
                    }
                }
            ) {
            }
        } {
        }
    }
}

让我们孤立返回语句:

fn fake_macros() -> impl std::fmt::Debug{
	return! { 1337 }
}

这在对内部表达式应用 not 操作。接下来我们将该表达式包裹在循环中。

fn fake_macros() -> impl std::fmt::Debug{
	loop {
		break! {
			return! {
				1337
			}
		}
	}
}

break!{ } 也在对 return! { 1337 } 执行 not 操作,其类型为 !。现在函数的返回类型是从循环和返回语句两者推断出来的。分支函数?接下来我们将循环中的所有内容包裹在 match 语句中

fn fake_macros() -> impl std::fmt::Debug{
	loop {
		match!(
			break! {
				return! {
					1337
				}
			}
		){
		
		}
	}
}

由于我们匹配的是 never,因此无需在匹配语句中添加任何模式。最后我们将此包裹在一个 if 语句中。

fn fake_macros() -> impl std::fmt::Debug{
	loop {
		if! {
			match! (
				break! {
					return! {
						1337
					}
				}
			)
		} {
		}
	}
}

总结如下:

  • return! { 1337 } 使函数的返回类型为 i32,该类型实现了 Debug
  • break! { ... } 使循环的返回类型为 !,因为内部有 return,它也实现了 Debug
  • 我们匹配 break 语句并省略模式,因为它是 !
  • match 语句包裹在 if 语句中

111 Responses to Rust 中的奇怪表达式

  1. steveklabnik says:

    这篇帖子缺少我最喜欢的那部分!

        fn evil_lincoln() { let _evil = println!(“lincoln”); }
    

    这有什么奇怪的呢?

    要理解 evil_lincoln 的作用,你必须了解非常古老的 Rust。以下是引入它的提交:https://github.com/rust-lang/rust/commit/664b0ad3fcead4fe4d2

        fn evil_lincoln() {
            let evil <- log “lincoln”;
        }
    

    log 是一个用于将内容打印到屏幕上的关键字。因此有了这个笑话,https://en.wikipedia.org/wiki/Lincoln_Logs现在 log 变成了 println! 宏,这个笑话就失效了。

    它没有明确说明为什么这很“奇怪”,但考虑到文件中的其他注释,

        // FIXME: 无法编译
        //let _x = log true == (ret 0);
    

    我推测使用log的返回值存在问题,因此这段代码测试了是否能将其保存到变量中。我不记得log的具体语义,但如果它像println!一样返回(),那毫无用处,因此将它绑定到变量是你在实际代码中绝不会写的东西,所以从这个意义上说它是“奇怪的”。

  2. b0a04gl says:

    它们存在是因为整个语言设计时就将表达式视为一等公民:块、条件语句、模式匹配,甚至宏都被视为返回值的表达式。一旦你理解了这一点,所有这些奇怪的一行代码都是系统特性的产物。只是一个系统中表达式可以无限组合的产物。语法树的深度超出了大多数人的习惯所允许的范围。当你达到那个深度时,大脑会觉得这不对,但编译器允许。

    • AIPedant says:

      嗯,我的理解是,这有点夸张——Rust 一直都是以表达式作为第一公民的理念构建的,但实用性和性能要求使用“return”这样的破坏表达式的关键字,这些关键字并不适合 ML 式的语言,而且与它们的实现有关的一些老式黑客技巧(不是指缺乏健壮性的“黑客”,而是指理论上/形式上不优雅)。我指的是理论上/形式上不优雅)。同样,有些东西(u8)只是简单的语法怪癖。但许多这些 return 等怪癖是因为 Rust 最终是一种受函数式编程强烈影响的命令式语言。

      • steveklabnik says:

        return 是 Rust 中的一个表达式,非常适合。

        语句非常少:https://doc.rust-lang.org/stable/reference/statements.html

        和许多表达式:https://doc.rust-lang.org/stable/reference/expressions.html

        • AIPedant says:

          我们正在各说各话,因为Rust规范中定义的“表达式”与普通计算机科学中的“表达式”有所不同,而Rust中对return的使用显然不属于后者意义上的“表达式”。它被硬塞进“表达式”的范畴,但实际上它没有语义上意义的类型,它是一种效果。类型是(谨慎但有些任意地)分配给它的,这就是为什么涉及“return”的一些示例特别荒谬。对于大多数程序来说,它并不重要,因为它只在有意滥用关键字时才会出现。但在具有真正一等表达式的函数式语言中,“return”没有意义——函数不会“返回”值,它们会被“评估”,而帧销毁等操作都被抽象掉了。在 Rust 中,这很有意义,因为从计算机科学的角度来看,表达式最终并不是第一类。

          • steveklabnik says:

            我认为我们是在各说各话。我并不完全同意你的“从计算机科学的角度来看”的说法,因为 Rust 确实有一个语义上意义重大的类型:!。这都是非常标准的东西。Rust 在这里并没有做任何奇怪或新奇的事情。

            • rtpg says:

              我确实想知道有多少语言明确提供了“永远不会返回”类型。Typescript 和 Rust……Haskell 有底值,但我好奇在语义上,底值和“永远不会返回”之间到底有多大差距。显然,懒惰会让事情变得奇怪。

              不过,这正是我对这一代语言感兴趣的地方。任何 C 程序员都理解无限循环的概念,以及条件表达式(如三元运算符)的价值。但现在语言开始意识到,当你将越来越多的东西视为表达式时,你真的需要开始给过去不会命名的东西命名。

          • int_19h says:

            纯函数式语言有“never”的等价物——它是底类型。事实上,Haskell中error的返回类型就是这样,但也包括可预测的无限递归等情况。但这种语义对“return”和其他形式的控制转移非常有效——它们出现的表达式也“永远不会完成”(但包含该表达式作为子表达式的其他表达式会完成)。

            理想情况下,你希望在类型系统中引入效果,以便更精确地表达此类内容。但如果你将此限制在 return/break/continue 等场景,其中目标是静态已知且可验证的,你可以将这些效果类型视为已存在,只需推断所有表达式并禁止其跨越函数边界。

            对于异常而言,此技巧不再适用,因为异常的本质就是跨越该边界。但这些东西给打字带来的复杂性,即使是对简单的通用代码来说,也显然太大了,不适合实际使用(参见:Java 中的检查异常)。无论如何,在 Rust 中,你可以使用 Result 类型来代替,这样这些异常就会产生常规值。虽然可以处理恐慌,但它们肯定不是用来作为控制转移的通用机制的,所以只为它们添加效果类型是不值得的。

          • octachron says:

            返回(或其他效果)作为函数式语言中的表达式是有意义的。通常,OCaml有raise Exception,这也是一个表达式,与return或任何不返回的函数具有相同的类型。异常也可以用于实现用户定义的return函数。

        • missinglugnut says:

          史蒂夫,我知道你是该语言的权威,但你没有认真对待这里提出的问题就直接否定了它。

          在程序员的思维中,return 是一个语句,但在语言中它是一个表达式。这是一个非常务实的决定,需要一个反直觉的实现。结果,我们得到了这篇帖子中满是代码的内容,这些代码对编译器来说是有效的,但对大多数阅读它的程序员来说毫无意义。

          • steveklabnik says:

            > 返回语句在程序员的思维中是一个语句

            我对此有异议,当然,对于很多人来说,他们可能从其他语言中带入了假设,认为赋值是一个语句。这并不意味着他们是正确的。

            > 需要一个反直觉的实现

            对某些人来说,确实如此。对其他人来说,它并不反直觉。它非常常规,而习惯于“一切都是表达式”语言的人往往更喜欢它,我发现。

            • hansvm says:

              > 习惯于“一切都是表达式”语言的人往往更喜欢它,我发现

              也就是说,如果我们偏向于选择支持我们观点的数据点,那么我们的观点就被证明了。这就像那个关于“每家汽车保险公司都可以同时声称‘换了保险的人平均节省了数百美元’”的俏皮话。

              我也喜欢“一切都是表达式”的语言,但我认为这不是一个很好的论点。

          • int_19h says:

            我们在这条路上已经走了很长时间了。例如,在C++中,“throw”已经是一个(无类型)表达式,原因类似,尽管它没有一个合适的底层类型,因此不够完善。C#更进一步,添加了类型,这样你就可以写出类似的代码,例如x = y ?? throw new Error(...)。没有明显理由认为“return”在概念上应与之不同。

            此时更值得探讨的问题或许是:为何要区分表达式与语句?所有命令式语句均可合理地表示为产生()或“never”的表达式。分号 merely 作为序列化运算符,类似于C++中的逗号。

      • efnx says:

        我发现 Rust 是一个穿着 C 服装的 ML。

        它与其他 ML 的主要区别在于缺乏高阶类型,因此难以表达 Functor、Monad、Arrow 等概念。

      • GrantMoyer says:

        Haskell 有 `bottom`[1](另见 [2]),从类型检查的角度来看,它与 Rust 的 `return` 作用相似。

        我不会说在返回表达式的类型中使用无人居住类型在理论上是不雅观的。相反,我认为这非常令人满意。

        [1]: https://wiki.haskell.org/Bottom

        [2]: https://en.wikipedia.org/wiki/Bottom_type

    • derriz says:

      这听起来表面上合理,我支持编程语言语义的一致性,但进一步思考后,我认为这实际上是一个设计缺陷。

      对我来说,“return ”有类型与“if ”、“break”、“{”或任何其他关键字有类型一样,都是毫无意义的。这些都是语法元素。

      Rust 的类型系统显然受到 Hindley-Milner 的启发,而大多数使用这种类型系统的语言甚至都没有 return 关键字。

      即使你不同意这个论点,这个设计决策导致了所有这些奇怪/令人困惑但完全无用的代码示例,而我无法看到这个决策在语言易用性方面有什么好处。允许“return ”本身成为一个表达式对用户有什么实际价值?你可以将这样的“表达式”作为函数调用的参数,导致令人费解的后果?这只是语法糖。

      • steveklabnik says:

        恕我直言,“对我来说这毫无意义”并不是一个论点。if 和 break 在 Rust 中也有类型。

        > 甚至没有 return 关键字。

        这是因为它们不是过程化语言,与类型系统无关。

        > 就语言的人体工学而言,我无法看到这个决定有什么好处。

        它有巨大的优点!这就是为什么许多语言都选择这样做。例如,Rust 中没有必要使用三元运算符:if 可以做到这一点。

        > 允许“return ”本身成为表达式对用户有什么实际价值?

        这样的代码就可以正常运行:

                let guess: u32 = match guess.trim().parse() {
                    Ok(num) => num,
                    Err(_) => return,
                };
        

        也就是说,如果 return 不是表达式,我们会遇到类型错误:两个分支的类型不兼容。

        • derriz says:

          参见我上面的评论,你的示例仅在包含函数具有适当的返回类型(在此示例中为无返回类型)时“正常工作”。

          因此,语法元素“return”不仅仅是一个表达式——与其他子表达式不同,它涉及远程操作——即它不仅必须与作为表达式一部分的上下文一致,还必须与包含函数的签名一致。

      • bobbylarrybobby says:

        `return expr`没有类型的问题在于,你失去了编写类似以下内容的能力

        let y = match option { Some(x) => x, None => return Err(“whoops!”), };

        没有类型,None 分支就失去了与 Some 分支统一的能力。现在,你可以说 Rust 应该只要求所有分支都有类型时才能统一,但 ! never 类型完全可以实现这个目标。

        • derriz says:

          我在此回复是因为许多回复都在强调同一点。

          在你的具体示例中,让我们将你的示例置于上下文中。以下代码:

            fn foo(option: Option<i32>) -> i32 {
               let y = match option { Some(x) => x, None => return Err(“whoops!”), };
               return 1;
            }
          

          是类型正确的?如果我们相信“return ”是一个类型为()的表达式,那么它应该是正确的——但显然,它会导致编译错误,因为编译器会将“return ”与其他表达式区别对待。因此,这种方法并没有提高一致性,反而允许各种难以理解的“谜题”存在。

          我看不出来,如果你去掉“return ”本身是一个表达式的说法,为什么会失去这种能力。大多数/许多语言都有机制允许表达式影响流程控制——例如通过异常、yield 等——而这些构造(例如“throw x”)并不需要类型。

          Rust 可以轻松支持你上面使用的语法,而无需将“return ”作为可录制表达式。

          • steveklabnik says:

            > 是…类型正确的吗?

            不是,但不是因为返回,而是因为你试图从返回 i32 的函数中返回一个 Result。以下代码可以正常运行:

              fn foo(option: Option<i32>) -> Result<i32, &'static str> {
                 let y = match option { Some(x) => x, None => return Err(“whoops!”), };
                 return Ok(1);
              }
            

            > 如果我们认为“return ”是一个类型为()的表达式,那么它应该成立

            它不是,它是一个类型为!的表达式。这种类型与其他所有类型统一,因此y的整体类型是i32。return没有被特殊处理。

            > 如果移除“return ”本身是表达式的说法

            这段代码将不再工作,因为以表达式结尾的代码块会评估为 (),因此你会得到一个发散的、类型不正确的错误,因为一个分支是 i32,另一个是 ()。

            • derriz says:

              抱歉造成混淆——我本意是使用 ! 而不是 ()。

              “不是,但不是因为 return,而是因为你试图从一个返回 i32 的函数中返回一个 Result。”

              这正是我的观点。“return ”不仅仅是一个可以被类型化的表达式。如果你告诉我所有使用的标识符的类型,我可以查看 Rust 中不包含 return 的任何表达式,并告诉你它是否符合类型要求。如果表达式包含 return,那么我无法告诉你该表达式是否符合类型要求。

              • steveklabnik says:

                > “return ”不仅仅是一个可以输入的表达式。

                是的,它是,而且可以。它的类型是 !,无论 的类型是什么。

                • derriz says:

                  它只有类型 !,如果包含它的函数声明的返回类型与 的类型相同,否则它是不合法的。

                  对于不涉及 “return” 的任何表达式,我可以写,例如:

                  const Z =

                  但如果 中嵌入了某个地方的 “return”,我就不能这样写。表达式中某个地方存在 “return” 会改变整个表达式的性质。

                  也就是说,有两类 “表达式”。一种是不包含 return 的表达式(相当于 Rust 借鉴的语言中的“表达式”概念),另一种是在某处包含 return 的表达式,后者需要遵守关于语法正确性的进一步规则。

                  我的观点是,这些规则完全没必要——你无需为语言的每个词法特征提供类型规则,就能拥有一个具有强大表达能力的类型系统(如 Rust 的类型系统)。

                  • int_19h says:

                    如果你将 `const Z` 嵌套在函数定义内部,就可以正常编写。

                    实际上,这与变量引用并无本质区别。如果你有一个表达式 (x + 1),你只能在有 `x` 在作用域内的位置使用它。同样地,你只能在作用域内存在要返回的函数时使用 `return`。事实上,在设计语言时甚至可以明确规定这一点!函数定义本身已隐式引入了所有参数的 let 定义。假设我们重新定义函数,使其同时引入 “return” 作为局部变量,即给定:

                       fn foo(x: i32, y: i32) -> i32 {
                         ...
                       }
                    

                    函数主体的编写方式将被视为已预先添加了以下行:

                       let x = ...;
                       let y = ...;
                       let return = ...;
                       ...
                    

                    其中“return”是一个与语句功能相同的函数。break/continue 和循环也是如此。

                    这些与真实变量不同的地方在于,它们不能作为第一类值传递(例如,函数将“return”传递给它调用的另一个函数)。虽然实际上可以做到这一点,而且使用 Rust 生命周期注释甚至可以进行静态验证。

                  • steveklabnik says:

                    好的,我认为我们确实存在沟通障碍,我明白你的意思。我不确定是否完全同意,但感谢你的观点。我需要再思考一下。

          • rtpg says:

            在讨论良好类型性时,对于那些包含奇怪的不可判定组件的复杂语言,你可以将“良好类型性”定义为:

            e: T 是良好类型的 _如果_ e 的最终结果是类型 T

            (最终结果是一个模糊的概念)

            这并非保证e是某种类型的值,而是保证如果e本身是一个值,那么它将属于某种类型。这样就无需证明e的停机性质。

            这为无法完成的计算留出了空间!

                let y = return 1
                f(y)
            

            y 可以是任何类型,而且它是类型正确的,因为你永远不会遇到 f(y) 被赋予错误类型值的情况。

            在我对更复杂类型系统的理解中,类型正确性并非对控制流的保证,而是对表达式评估的保证——即如果我们评估某个表达式,那么它将是安全的。

            因此……你可以将 ! 作为类型引入系统,将 return 视为表达式,从而获得一个更简单的语义模型,而不会真正失去任何东西。减少了复杂性,等等……这就是我对它的理解。

          • bobbylarrybobby says:

            返回值的类型是 !,而不是 ()。这意味着这种类型的实例为零(而 () 类型有一个实例)。! 可以转换为任何类型。

            此外,返回值的类型与被返回对象的类型是两个独立的问题。显然,你不能从返回 i32 的函数中返回 Result。类型转换的意义在于,你可以在匹配的某一分支中yield `return Err(…)`,同时与其他分支的类型检查兼容。

          • efnx says:

            现在我们只是在讨论个人偏好了。

      • NobodyNada says:

        实际上,这是一个非常有用的功能,因为你可以这样写:

            let day_number = match name {
                “Sunday” => 0,
                “Monday” => 1,
                “Tuesday” => 2,
                “Wednesday” => 3,
                “Thursday” => 4,
                “Friday” => 5,
                “Saturday” => 6,
                _ => return Err(“invalid day”)
            };
        
      • pornel says:

        这使得语言更加统一。无需在表达式位置使用特殊的 ternary ?: 运算符来实现 if-else 逻辑,而是采用统一的语法。

        这使得泛型代码能够正常工作,无需为“语法元素”添加例外情况。你可以使用类似 `map(callback)` 的方法,该方法接受一个通用类型 `fn() -> T`,并传递 `T`。这对于返回值的函数以及仅有 `return;` 的函数都可统一适用。将空值作为真实类型,使得只需使用一套类型规则即可正常工作,而非同时使用真实类型的规则加上对“语法元素”的例外处理。

      • deathanatos says:

        我们可以使用 match 进行模式匹配:

          let name = match color_code {
            0 => “red”,
            1 => “blue”,
            2 => “green”,
            _ => “unknown”,
          };
        

        `=>` 右侧必须是一个表达式,因为我们将其赋值给变量。在这里,你应该已经看到你所称的“语法元素”(我或许会称它们为“块语句”,这更贴近你所表达的意图)的一个“有用”的副作用。示例中整个`match … {}`是一个表达式(我们将其评估结果赋值给变量)。

        > 允许“return ”本身成为表达式对用户有何实际价值?

        如果需要返回错误怎么办?

          let name = match color_code {
            0 => “red”,
            1 => “blue”,
            2 => “green”,
            _ => return Err(“unknown color”),
          };
        

        表达式分支必须是相同类型(或`name`的类型是什么?)。因此,最后一个分支的类型是 !。(正如你从 TFA 中学到的,它可以转换为任何类型,这里转换为 &str。)

        “块语句实际上是表达式”这一特性还有更多用途。它不必是三元运算符/关键字(如 C、C++、Python、JS 等):

          let x = if cond { a } else { b };
        

        事实上,如果你熟悉 JavaScript,你可能会希望使用这种模式,但它并不存在:

          const x;  // 但 x 的值将取决于一个计算:
          // 这是非法的。
          if(foo) {
            x = 3;
          } else {
            x = 4;
          }
          // 虽然可行,但代码很丑陋:
          const x = (function() { if(foo) { return 3; } else { return 4; }})();
          // (是的,你可以用三元运算符实现这个示例。
          // 假设 if 分支比三元运算符更复杂,
          // 例如包含两条语句。)
        

        同样,循环也可以返回值,这在某些情况下是个有用的模式:

          let x = loop {
            // 例如,在数据结构中查找值。进行计算。等等。
            if all_done {
              break result;
            }
          };
        

        以及代码块:

          let x = {
            // 计算 x;中间变量在代码块关闭时会被正确作用域化
            // 并清理。
            //
            // 这里还有一个视觉上的好处,即“这里计算 x”
            // 非常明确地标注出来。
          };
        

        > 即使您不同意这个论点,但这个设计决策也导致了所有这些奇怪/令人困惑但完全无用的代码示例_

        我认为,任何语言都可以编写出奇怪的代码示例。

      • dathinab says:

        它不需要在更高层次的逻辑/语义抽象上说得通

        我的意思是,你在任何现实的PR中都不会看到博客文章中的那些胡说八道(所以它们不重要),

        但如果你让某些表达式比其他表达式更特殊,你就会遇到微妙的边界情况问题(所以这很重要),

        尤其是在宏/过程宏或部分“进行中”的代码更改的上下文中(这也是为什么use允许一些“奇怪”的{-大括号用法,或者为什么很多东西允许可选的尾随,,所有这些都使自动代码生成更简单)。

    • throwawaymaths says:

      这不仅仅是因为一些你通常认为是控制流的东西是表达式,还因为强制执行 `noreturn` 类型有一些不寻常的规则。

      • tialaramex says:

        这里唯一“不寻常”的规则是,Rust 提供了零类型加法,但没有提供(更复杂的)其他类型加法。

        因此,Rust 确实有:String + ! = String

        但 Rust 没有:String + i32 = Either

        请注意,never 类型 ! 在这里并不特殊,Rust 也会愉快地执行:String + Infallible = String或者,如果你要定义自己的空类型,如下所示:

            enum MyEmptyType {} // MyEmptyType 没有可能的值
        

        现在,在类型算术中,String + MyEmptyType = String,这确实可以在 Rust 中运行。

        编辑:语法修正

    • gmueckl says:

      如果一种声称注重安全的语言能够轻松表达出人类难以理解甚至更糟糕的结构,那么这本身就可能是一个安全问题:无法检查难以理解的逻辑的正确性。

      • pornel says:

        威胁模型是什么?如果你在审查不可信或安全关键的代码,而它因任何原因难以理解,那么它就是被拒绝的。

        仅凭语法无法阻止那些足够顽固的傻瓜。Lisp 以语法简单而著称,但很容易以难以理解的方式编写。汇编语言的语法非常严格,但这并不意味着它们容易理解。

        Rust 已经拥有非常强大的类型系统和大量 lint,比许多其他语言更能阻止不良程序。

        • gmueckl says:

          许多现代语言设计师专注于塑造表达力而非提供最大可能的灵活性,因为他们的设计师从C、Lisp等犯过错误的语言中吸取了教训。例如Java、C#、D、Go等语言,其中一些可能比其他语言更成功。但将最终表达力交给程序员的语言设计已是过去的遗物。

          • pornel??? says:

            “表达能力”和“表达力”是模糊且主观的,因此不清楚你的具体含义。

            你是否反对语法中的正交性?Go语言和Java确实缺乏这一点。

            但你提到C语言时又提到“最大可能的灵活性”?C语言中几乎不存在这种灵活性。我只能同意它有供他人学习的错误。

            你列出的语言之间几乎没有共同点。C#一直在添加巧妙的语法糖,而Go则正式放弃了去除其最冗余的 boilerplate 代码。

            D 有 UFCS、模板元编程、字符串混合、lambda 等有趣的东西——足够让你创建“难以理解”的代码,如果你想的话。

            你谈论的是现代语言与过去的遗物,但你提到的所有语言都比 Rust 更老。

            • gmueckl says:

              你见过 IOCCC 或 Underhanded C Code Contest 的投稿吗?这就是语法灵活性过强(如果走极端的话)的样子。

              如果你希望你的代码安全,你就需要它正确。为了确保正确性,首先需要确保可理解性。这需要语法和语义没有奇怪的意外。

      • steveklabnik says:

        > 如果一种语言声称注重安全性

        Rust 并不声称特别注重安全性,只是注重内存安全性。

        此外,这意味着你会认为任何基于表达式的语言都存在固有的安全问题。

        • gmueckl says:

          “内存安全”是计算机安全的一个方面。而安全是 Rust 使命宣言中列出的首要价值。

          Rust 并不是一种纯粹的基于表达式的语言。正如我们从 C 和 JS 的经验中非常清楚地知道的那样,任何出乎意料且看起来奇怪的代码都可能隐藏着巨大的危害。允许程序员偏离预期的惯用语太远是危险的。

          • steveklabnik says:

            这是其中一个方面,但 Rust 并不能保证你的核心是安全的。

            它并不是纯粹的表达式语言,但非常接近,只有几种语句,绝大多数都是表达式。

          • keybored says:

            我们需要一个令人费解的代码示例,这种代码很容易隐藏(安全)漏洞。

            提交的示例展示了奇怪的程序片段。我认为这些片段并不一定能轻松隐藏漏洞?

      • int_19h says:

        “永远不会”是一个一旦你开始提出正确问题就容易理解的概念。

        但同时,TFA中的所有示例都是非常人为且复杂的代码。也就是说,你可以像这样编写代码,就像你可以编写类似&&&…x的代码一样——但为什么你要这样做?实际世界中对这一特性的使用都相当易读。

      • PaulHoule says:

        这是一篇对宏的批判。500行Common Lisp代码可以替代50,000行C++代码,但当你第一次看到这500行代码时,它们完全没有意义。

  3. ramon156 says:

    请注意,对于 Rust 开发人员来说,这些也是奇怪的语法。我觉得有些人认为经验丰富的开发人员可以读懂这些语法,但要理解它们需要一段时间。

  4. xg15 says:

    我是 Rust 新手。

    那个 ‘!’ 类型在前几个例子中看起来很奇怪,但后来开始有意义了。

    它本质上是所有在语法上是表达式但永远不会返回任何内容的“伪类型”,因为评估它会导致整个语句被取消。

    这样理解对吗?

    • NobodyNada says:

      没错,这就是所谓的 Never 类型。

      它在更多场景下都有用处,而不仅仅是返回表达式——例如,你可以让函数返回 ! 来表示这是一个不返回的函数,这对于表达必须让程序崩溃的错误处理程序,或必须永远不返回的主循环非常有用。它还可以帮助编译器生成更紧凑的代码,当函数已知不会返回时。

      目前正在进行的工作允许你在任何地方指定 ! 作为类型,而不仅仅是函数返回值。这在一些通用代码中很有用,因为这些代码期望函数返回一个带有实现指定错误类型的 Result,而一个无错误的实现可以指定 ! 作为错误类型。然后,类型检查器可以允许程序员在不检查错误的情况下解包 Result,优化器可以从通用代码中删除错误检查分支:https://doc.rust-lang.org/std/primitive.never.html

      这一功能的实现耗时极长,因为类型推断中存在一些非常微妙的影响,使得在不破坏兼容性的情况下难以稳定——但 2024 版终于找到了实现这一功能的方法。

    • int_19h says:

      不一定是整个语句,只是某些外部表达式。

      当您记住 Rust 中的唯一语句是各种声明(`let`、`type`、`fn` 等)和宏调用时,这可能会更合理。其他一切都是“表达式语句”,包括块和循环。因此,您可以执行以下操作:

          // 计算大于10的第一个斐波那契数
          let n = {
              let mut x1 = 0;
              let mut x2 = 1;
              loop {
                  let x = x1 + x2;
                  if x > 10 { break x }
                  x1 = x2;
                  x2 = x;
              }
          };
      

      注意,break 不会离开 let 语句——它只是终止循环表达式并强制其返回一个值(break 没有参数时返回 (),循环没有 break 时也是如此)。

      你也可以从标记的块中跳出,只要它们被标记并且你使用标记形式的 break

         let x = 'label: { ... break 'label 42 ... }
      

      如果不谨慎使用,这很容易导致复杂的代码,但有时将可变数据封装在循环中并使用 break 在计算完成后返回值,确实是最直接的编写方式。

    • Analemma_ says:

      是的。如果你查看 steveklabnik 在评论中其他地方的 match 语句示例,就会明白 ‘!’ 是“从未”或“不可达”类型,不是因为返回表达式未执行,而是因为其值永远不会被赋值给变量,因为它会导致函数无条件退出。

  5. lacker says:

    我认为对“bathroom_stall”的解释有误。在描述此表达式中的条件判断时:

      if (i+=1) != (i+=1)
    

    帖子中写道:“if 语句始终为假,因为右侧表达式始终比左侧大 1。” 但这是不等于。if 语句总是为假,因为在 Rust 中,“i += 1”不会返回整数值,而是返回 ()。因此,比较任何两个 += 语句,它们总是相等的。由于守卫是 != 比较,因此 if 语句总是为假。

  6. arjvik says:

    我意识到“返回值”的概念只有在理解以下代码时才变得有意义:

        fn funny(){
            fn f(_x: ()){}
            f(return);
        }
    

    f() 函数从未被调用,因为 funny() 在 f() 被调用之前就已经返回了。

    你希望 return 能够强制转换为任何类型,这样你就可以编写类似以下代码:

        let x: i32 = if y {
            4
        } else {
            return; // 类型 ! 被强制转换为 i32
        }
    

    你选择返回值为 !,因为 return 实际上从未在运行时产生一个被传递的值,它会立即退出函数。

    (请注意,即使返回值,这一切也仍然有效)

  7. munificent says:

    有人知道为什么 `union` 不是 Rust 中的保留字吗?

    其他语言中的大多数上下文关键字来自以下两种情况:

    1. 语言被广泛使用后添加的功能,无法添加关键字而不破坏现有代码。

    2. 该词在其他地方特别有用,因此保留起来会很麻烦(如 Dart 中的 `get` 和 `set`)。

    但这些似乎都不适用于 Rust。据我所知,它一直都有 ML 风格的联合,而且“union”似乎并不是一个特别有用的标识符。

    为什么不完全保留 `union` 呢?

  8. armchairhacker says:

    相关:https://dtolnay.github.io/rust-quiz

    Rust 程序会给出不直观的输出或编译错误。

  9. pluto_modadic says:

    我确实见过一些/可恶的/ Rust 一行代码。

    如果你把它扩展到最诅咒的~6行代码,你真的可以以一种极其难以调试的方式混淆你在做什么。

  10. kzrdude says:

    其中许多都围绕同一个主题——主题是`return -> !`。

    这是我最喜欢的这个主题的例子,我之前在列表中漏掉了:

        return return return return return return return return return 1
    
  11. sureglymop says:

    随着昨天的发布,链式调用功能也一并发布了。这是一个非常需要的功能,但有时看起来非常糟糕:https://blog.rust-lang.org/2025/06/26/Rust-1.88.0/#let-chain

  12. nfrmatk says:

    几年前有一场关于这个主题的有趣的RustConf演讲。https://youtu.be/tNVDjMxCz-c

  13. Sniffnoy says:

    我不明白那个使用dont()的例子。为什么i.get()在最后会变成false?难道它在被dont()设置后不应该是true吗?

  14. bensons1 says:

    我有点觉得有趣,一个内存安全的语言居然会用这种花哨的写法

  15. IshKebab says:

    说实话,我没想到这些写法居然不那么奇怪。比 JavaScript、PHP、C 或 C++ 少多了。

  16. behnamoh says:

    太好了,现在这些东西被输入到大语言模型(LLM)的训练中,我们将在下一代模型输出中看到它们。

    说真的,我喜欢以意想不到的方式“滥用”编程语言。到目前为止,我最喜欢的是:https://evuez.net/posts/cursed-elixir.html。读完这篇文章,我意识到Elixir本质上就是从头到尾都是宏,而且它是一种Lisp语言!

  17. JoeOfTexas says:

    老兄,我昨天刚开始学习 Rust。你为什么对我这样做呢?现在我学的东西都忘了。

  18. npalli says:

    是的,这很好,但现在加上生命周期注释会更有趣。

  19. Thaxll says:

    为什么将变量赋值给一个返回空值的函数不是编译错误?

    • tialaramex says:

      你的意思是返回“无”的函数,即它确实返回了,但没有特定的值可返回,比如 Vec::clear,它删除了,但保留了容器的容量?

      在 Rust 中,此函数的返回类型是单位类型,即空元组 ()。因此,该变量具有这种类型,在 Rust 中没有问题,尽管一些较差的语言无法处理如此小的类型。

      或者你的意思是像 std::process::exit 这样的永远不会返回的函数?在 Rust 中,该函数的返回类型是 !Never 类型,这是一种空类型,在稳定的 Rust 中通常无法命名。

      由于该类型为空,该类型的变量将消失。编译器知道如果不存在该类型的值,我们就无法创建值,因此包含该变量的代码路径将永远不会被执行,因此无需生成机器码。

      在像 Rust 这样的泛型编程语言中,这并不是一个错误,实际上是一种便利。我们可以编写泛型错误处理代码,对于永远不会出现错误的情况,我们的错误处理代码甚至不会编译,它会完全消失,但对于可能出现实际错误的情况,错误处理代码会输出。

    • dathinab says:

      假设你指的是返回 ()(空元组/空类型)

      因为它与泛型、宏、过程宏等配合不好。

      例如,如果你有这种包裹 clone 的方式:`fn foo(v: T) -> (T, T) { let new = v.clone(); (v, new) }`它将隐式地无法与 T = () 配合使用,因为此时 `v.clone()` 将是一个“返回空值的函数”。

      在孤立情况下这可能看似无碍,但当你组合抽象时,迟早会遇到一个边界案例,此时它就不再适用。

      而当涉及宏/过程宏时,这个问题会变得更加严重。

      它还使修改代码更容易,例如,如果你有类似 `let x = foo(); dbg!(x);` 的代码,即使你将返回类型改为 `()`(即返回空值),只要该类型实现了 `Debug`,代码仍能编译通过。对于普通代码,这只是一个小问题,但对于宏、过程宏或足够复杂的泛型代码,迟早会遇到一些边界情况,此时允许这种行为确实很重要。虽然不常见,但足够常见。

      最后也是最重要的一点,将 () 赋值给变量不会伤害任何人,你不会在正常的 PR 中看到这样的代码。

      因此,它不会伤害任何人,但在边界情况下可能非常有用。

      最后,静态分析工具(主要是 clippy)会对某些无意义的代码发出警告或错误,具体取决于你启用的静态分析规则。

    • steveklabnik says:

      你指的是哪个示例?

    • assbuttbuttass says:

      在 Rust 中,没有“返回值为空的函数”这种东西。单元(用 () 表示)是一流值,可以赋给变量,尽管这并没有多大意义。

  20. keybored says:

    我曾经尝试过需要多少个 `{{{…}}}` 才能让 javac 崩溃。其实并不多。

  21. xyst says:

    这些“奇怪的表达式”可能在代码高尔夫中被使用。

  22. nikolayasdf123 says:

    这就是我喜欢Go的原因

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注