Merge pull request #448 from AllanDowney/patch-1

Update: unified format
pull/471/head
孙飞Sunface 3 years ago committed by GitHub
commit bb4b053b4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -13,9 +13,9 @@
一箩筐的理由~~ 让我们先从第二点讲起。 一箩筐的理由~~ 让我们先从第二点讲起。
#### 为外部类型实现外部特征 #### 为外部类型实现外部特征
在之前的章节中,我们有讲过如果在外部类型上实现外部特征必须使用 `newtype` 的方式,否则你就得遵循孤儿规则:要为类型 `A` 实现特征 `T`,那么 `A` 或者 `T` 必须至少有一个在当前的作用范围内。 在之前的章节中,我们有讲过如果在外部类型上实现外部特征必须使用 `newtype` 的方式,否则你就得遵循孤儿规则:要为类型 `A` 实现特征 `T`,那么 `A` 或者 `T` 必须至少有一个在当前的作用范围内。
例如,如果想使用 `println!("{}",v)` 的方式去格式化输出一个动态数组 `Vec`,以期给用户提供更加清晰可读的内容,那么就需要为 `Vec` 实现 `Display` 特征,但是这里有一个问题: `Vec` 类型定义在标准库中,`Display` 亦然,这时就可以祭出大杀器 `newtype` 来解决: 例如,如果想使用 `println!("{}", v)` 的方式去格式化输出一个动态数组 `Vec`,以期给用户提供更加清晰可读的内容,那么就需要为 `Vec` 实现 `Display` 特征,但是这里有一个问题: `Vec` 类型定义在标准库中,`Display` 亦然,这时就可以祭出大杀器 `newtype` 来解决:
```rust ```rust
use std::fmt; use std::fmt;
@ -36,7 +36,7 @@ fn main() {
如上所示,使用元组结构体语法 `struct Wrapper(Vec<String>)` 创建了一个 `newtype` Wrapper然后为它实现 `Display` 特征,最终实现了对 `Vec` 动态数组的格式化输出。 如上所示,使用元组结构体语法 `struct Wrapper(Vec<String>)` 创建了一个 `newtype` Wrapper然后为它实现 `Display` 特征,最终实现了对 `Vec` 动态数组的格式化输出。
#### 更好的可读性及类型异化 #### 更好的可读性及类型异化
首先,更好的可读性不等于更少的代码(如果你学过Scala相信会深有体会),其次下面的例子只是一个示例,未必能体现出更好的可读性: 首先,更好的可读性不等于更少的代码如果你学过Scala相信会深有体会,其次下面的例子只是一个示例,未必能体现出更好的可读性:
```rust ```rust
use std::ops::Add; use std::ops::Add;
use std::fmt; use std::fmt;
@ -57,10 +57,10 @@ impl Add for Meters {
} }
fn main() { fn main() {
let d = calculate_distance(Meters(10), Meters(20)); let d = calculate_distance(Meters(10), Meters(20));
println!("{}",d); println!("{}", d);
} }
fn calculate_distance(d1: Meters,d2: Meters) -> Meters { fn calculate_distance(d1: Meters, d2: Meters) -> Meters {
d1 + d2 d1 + d2
} }
``` ```
@ -79,11 +79,11 @@ struct Meters(u32);
fn main() { fn main() {
let i: u32 = 2; let i: u32 = 2;
assert_eq!(i.pow(2),4); assert_eq!(i.pow(2), 4);
let n = Meters(i); let n = Meters(i);
// 下面的代码将报错,因为`Meters`类型上没有`pow`方法 // 下面的代码将报错,因为`Meters`类型上没有`pow`方法
// assert_eq!(n.pow(2),4); // assert_eq!(n.pow(2), 4);
} }
``` ```
@ -143,7 +143,7 @@ fn returns_long_type() -> Thunk {
Bang是不是立刻大幅简化了我们的使用。喝着奶茶、哼着歌、我写起代码撩起妹何其快哉 Bang是不是立刻大幅简化了我们的使用。喝着奶茶、哼着歌、我写起代码撩起妹何其快哉
在标准库中,类型别名应用最广的就是简化 `Result<T,E>` 枚举。 在标准库中,类型别名应用最广的就是简化 `Result<T, E>` 枚举。
例如在 `std::io` 库中,它定义了自己的 `Error` 类型:`std::io::Error`,那么如果要使用该 `Result` 就要用这样的语法:`std::result::Result<T, std::io::Error>;`,想象一下代码中充斥着这样的东东是一种什么感受?颤抖吧。。。 例如在 `std::io` 库中,它定义了自己的 `Error` 类型:`std::io::Error`,那么如果要使用该 `Result` 就要用这样的语法:`std::result::Result<T, std::io::Error>;`,想象一下代码中充斥着这样的东东是一种什么感受?颤抖吧。。。
@ -163,7 +163,7 @@ fn main() {
let i = 2; let i = 2;
let v = match i { let v = match i {
0..=3 => i, 0..=3 => i,
_ => println!("不合规定的值:{}",i) _ => println!("不合规定的值:{}", i)
}; };
} }
``` ```
@ -177,7 +177,7 @@ error[E0308]: `match` arms have incompatible types // match的分支类型不同
| _____________- | _____________-
4 | | 0..3 => i, 4 | | 0..3 => i,
| | - this is found to be of type `{integer}` // 该分支返回整数类型 | | - this is found to be of type `{integer}` // 该分支返回整数类型
5 | | _ => println!("不合规定的值:{}",i) 5 | | _ => println!("不合规定的值:{}", i)
| | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected integer, found `()` // 该分支返回()单元类型 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected integer, found `()` // 该分支返回()单元类型
6 | | }; 6 | | };
| |_____- `match` arms have incompatible types | |_____- `match` arms have incompatible types
@ -191,7 +191,7 @@ fn main() {
let i = 2; let i = 2;
let v = match i { let v = match i {
0..=3 => i, 0..=3 => i,
_ => panic!("不合规定的值:{}",i) _ => panic!("不合规定的值:{}", i)
}; };
} }
``` ```
@ -238,7 +238,7 @@ fn generic<T>(t: T) {
} }
``` ```
该函数很简单就一个泛型参数T那么如保证 `T` 是固定大小的类型?仔细回想下,貌似在之前的课程章节中,我们也没有做过任何事情去做相关的限制,那 `T` 怎么就成了固定大小的类型了?奥秘在于编译器自动帮我们加上了 `Sized` 特征约束: 该函数很简单就一个泛型参数T那么如保证 `T` 是固定大小的类型?仔细回想下,貌似在之前的课程章节中,我们也没有做过任何事情去做相关的限制,那 `T` 怎么就成了固定大小的类型了?奥秘在于编译器自动帮我们加上了 `Sized` 特征约束:
```rust ```rust
fn generic<T: Sized>(t: T) { fn generic<T: Sized>(t: T) {
// --snip-- // --snip--

@ -10,7 +10,7 @@
与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的,因此从性能的角度看,栈往往比对堆更高。 与栈相反,堆上内存则是从低位地址向上增长,**堆内存通常只受物理内存限制**,而且通常是不连续的,因此从性能的角度看,栈往往比对堆更高。
相比其它语言Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可),例如以下代码: 相比其它语言Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可),例如以下代码:
```rust ```rust
fn main() { fn main() {
let b = foo("world"); let b = foo("world");
@ -60,7 +60,7 @@ fn main() {
- `println!` 可以正常打印出 `a` 的值,是因为它隐式地调用了 `Deref` 对智能指针 `a` 进行了解引用 - `println!` 可以正常打印出 `a` 的值,是因为它隐式地调用了 `Deref` 对智能指针 `a` 进行了解引用
- 最后一行代码 ` let b = a + 1` 报错,是因为在表达式中,我们无法自动隐式地执行 `Deref` 解引用操作,你需要使用 `*` 操作符 `let b = *a + 1`,来显式的进行解引用 - 最后一行代码 ` let b = a + 1` 报错,是因为在表达式中,我们无法自动隐式地执行 `Deref` 解引用操作,你需要使用 `*` 操作符 `let b = *a + 1`,来显式的进行解引用
- `a` 持有的智能指针将在作用结束(`main` 函数结束)时,被释放掉,这是因为 `Box<T>` 实现了 `Drop` 特征 - `a` 持有的智能指针将在作用域结束(`main` 函数结束)时,被释放掉,这是因为 `Box<T>` 实现了 `Drop` 特征
以上的例子在实际代码中其实很少会存在因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上由于寄存器、CPU 缓存的原因,它的性能将更好,而且代码可读性也更好。 以上的例子在实际代码中其实很少会存在因为将一个简单的值分配到堆上并没有太大的意义。将其分配在栈上由于寄存器、CPU 缓存的原因,它的性能将更好,而且代码可读性也更好。
@ -76,17 +76,17 @@ fn main() {
let arr1 = arr; let arr1 = arr;
// arr 和 arr1 都拥有各自的栈上数组,因此不会报错 // arr 和 arr1 都拥有各自的栈上数组,因此不会报错
println!("{:?}",arr.len()); println!("{:?}", arr.len());
println!("{:?}",arr1.len()); println!("{:?}", arr1.len());
// 在堆上创建一个长度为1000的数组然后使用一个智能指针指向它 // 在堆上创建一个长度为1000的数组然后使用一个智能指针指向它
let arr = Box::new([0;1000]); let arr = Box::new([0;1000]);
// 将堆上数组的所有权转移给 arr1由于数据在堆上因此仅仅拷贝了智能指针的结构体底层数据并没有被拷贝 // 将堆上数组的所有权转移给 arr1由于数据在堆上因此仅仅拷贝了智能指针的结构体底层数据并没有被拷贝
// 所有权顺利转移给 arr1arr 不再拥有所有权 // 所有权顺利转移给 arr1arr 不再拥有所有权
let arr1 = arr; let arr1 = arr;
println!("{:?}",arr1.len()); println!("{:?}", arr1.len());
// 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错 // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错
// println!("{:?}",arr.len()); // println!("{:?}", arr.len());
} }
``` ```
@ -205,7 +205,7 @@ fn main() {
```rust ```rust
fn main() { fn main() {
let arr = vec![Box::new(1), Box::new(2)]; let arr = vec![Box::new(1), Box::new(2)];
let (first,second) = (&arr[0],&arr[1]); let (first, second) = (&arr[0], &arr[1]);
let sum = **first + **second; let sum = **first + **second;
} }
``` ```
@ -223,7 +223,7 @@ fn main() {
```rust ```rust
fn main() { fn main() {
let s = gen_static_str(); let s = gen_static_str();
println!("{}",s); println!("{}", s);
} }
fn gen_static_str() -> &'static str{ fn gen_static_str() -> &'static str{
@ -236,7 +236,7 @@ fn gen_static_str() -> &'static str{
在之前的代码中,如果 `String` 创建于函数中,那么返回它的唯一方法就是转移所有权给调用者 `fn move_str() -> String`,而通过 `Box::leak` 我们不仅返回了一个 `&str` 字符串切片,它还是 `'static` 类型的! 在之前的代码中,如果 `String` 创建于函数中,那么返回它的唯一方法就是转移所有权给调用者 `fn move_str() -> String`,而通过 `Box::leak` 我们不仅返回了一个 `&str` 字符串切片,它还是 `'static` 类型的!
要知道真正具有 `'static` 生命周期的往往都是编译期就创建的值,例如 `let v = "hello,world"`,这里 `v` 是直接打包到二进制可执行文件中的,因此该字符串具有 `'static` 生命周期,再比如 `const` 常量。 要知道真正具有 `'static` 生命周期的往往都是编译期就创建的值,例如 `let v = "hello, world"`,这里 `v` 是直接打包到二进制可执行文件中的,因此该字符串具有 `'static` 生命周期,再比如 `const` 常量。
又有读者要问了,我还可以手动为变量标注 `'static` 啊。其实你标注的 `'static` 只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用 `Box::leak` 就可以将一个运行期的值转为 `'static` 又有读者要问了,我还可以手动为变量标注 `'static` 啊。其实你标注的 `'static` 只是用来忽悠编译器的,但是超出作用域,一样被释放回收。而使用 `Box::leak` 就可以将一个运行期的值转为 `'static`
@ -246,7 +246,7 @@ fn gen_static_str() -> &'static str{
那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**,那么久可以使用 `Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然 `Rc/Arc` 也可以实现此功能,但是 `Box::leak` 是性能最高的。 那么我说一个简单的场景,**你需要一个在运行期初始化的值,但是可以全局有效,也就是和整个程序活得一样久**,那么久可以使用 `Box::leak`,例如有一个存储配置的结构体实例,它是在运行期动态插入内容,那么就可以将其转为全局有效,虽然 `Rc/Arc` 也可以实现此功能,但是 `Box::leak` 是性能最高的。
## 总结 ## 总结
`Box` 背后是调用 `jemalloc` 来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带 GC 的语言中的对象也是借助于 `Box` 概念来实现的,一切皆对象 = 一切皆 Box 只不过我们无需自己去 `Box` 罢了。 `Box` 背后是调用 `jemalloc` 来做内存管理,所以堆上的空间无需我们的手动管理。与此类似,带 GC 的语言中的对象也是借助于 `Box` 概念来实现的,**一切皆对象 = 一切皆 Box** 只不过我们无需自己去 `Box` 罢了。
其实很多时候编译器的鞭笞可以助我们更快的成长例如所有权规则里的借用、move、生命周期就是编译器在教我们做人哦不是是教我们深刻理解堆栈、内存布局、作用域等等你在其它 GC 语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你可以用 Java 开发的效率写出 Java 代码不可企及的性能和安全性,最终 Rust 语言所谓的开发效率低、心智负担高,对你来说终究不是个事。 其实很多时候编译器的鞭笞可以助我们更快的成长例如所有权规则里的借用、move、生命周期就是编译器在教我们做人哦不是是教我们深刻理解堆栈、内存布局、作用域等等你在其它 GC 语言无需去关注的东西。刚开始是很痛苦,但是一旦熟悉了这套规则,写代码的效率和代码本身的质量将飞速上升,直到你可以用 Java 开发的效率写出 Java 代码不可企及的性能和安全性,最终 Rust 语言所谓的开发效率低、心智负担高,对你来说终究不是个事。

@ -26,10 +26,10 @@ println!("{:04}", 42); // => "0042" with leading zeros
```rust ```rust
fn main() { fn main() {
let s = "hello"; let s = "hello";
println!("{}, world",s); println!("{}, world", s);
let s1 = format!("{}, world", s); let s1 = format!("{}, world", s);
print!("{}",s1); print!("{}", s1);
print!("{}\n","!"); print!("{}\n", "!");
} }
``` ```
@ -48,7 +48,7 @@ eprintln!("Error: Could not complete task")
它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用 `print!` 系列。 它们仅应该被用于输出错误信息和进度信息,其它场景都应该使用 `print!` 系列。
## {} 与 {:?} ## {} 与 {:?}
与其它语言常用的 `%d``%s` 不同Rust 特立独行地选择了 `{}` 作为格式化占位符(说到这个有点想吐槽下Rust 中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-),事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 `{}` 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。 与其它语言常用的 `%d``%s` 不同Rust 特立独行地选择了 `{}` 作为格式化占位符说到这个有点想吐槽下Rust 中自创的概念其实还挺多的,真不知道该夸奖还是该吐槽-,-,事实证明,这种选择非常正确,它帮助用户减少了很多使用成本,你无需再为特定的类型选择特定的占位符,统一用 `{}` 来替代即可,剩下的类型推导等细节只要交给 Rust 去做。
`{}` 类似,`{:?}` 也是占位符: `{}` 类似,`{:?}` 也是占位符:
@ -69,9 +69,9 @@ struct Person {
fn main() { fn main() {
let i = 3.1415926; let i = 3.1415926;
let s = String::from("hello"); let s = String::from("hello");
let v = vec![1,2,3]; let v = vec![1, 2, 3];
let p = Person{name: "sunface".to_string(),age: 18}; let p = Person{name: "sunface".to_string(), age: 18};
println!("{:?}, {:?}, {:?},{:?}",i,s,v,p); println!("{:?}, {:?}, {:?}, {:?}", i, s, v, p);
} }
``` ```
@ -88,7 +88,7 @@ let p = Person {
name: "sunface".to_string(), name: "sunface".to_string(),
age: 18, age: 18,
}; };
println!("{}, {}, {},{}", i, s, v, p); println!("{}, {}, {}, {}", i, s, v, p);
``` ```
运行后可以看到 `v``p` 都无法通过编译,因为没有实现 `Display` 特征,但是你又不能像派生 `Debug` 一般派生 `Display`,只能另寻他法: 运行后可以看到 `v``p` 都无法通过编译,因为没有实现 `Display` 特征,但是你又不能像派生 `Debug` 一般派生 `Display`,只能另寻他法:
@ -103,14 +103,14 @@ println!("{}, {}, {},{}", i, s, v, p);
`{:#?}``{:?}` 几乎一样,唯一的区别在于它能更优美地输出内容: `{:#?}``{:?}` 几乎一样,唯一的区别在于它能更优美地输出内容:
```console ```console
// {:?} // {:?}
[1, 2, 3],Person { name: "sunface", age: 18 } [1, 2, 3], Person { name: "sunface", age: 18 }
// {:#?} // {:#?}
[ [
1, 1,
2, 2,
3, 3,
],Person { ], Person {
name: "sunface", name: "sunface",
} }
``` ```
@ -177,11 +177,11 @@ fn main() {
除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从0开始) 除了按照依次顺序使用值去替换占位符之外,还能让指定位置的参数去替换某个占位符,例如 `{1}`,表示用第二个参数替换该占位符(索引从0开始)
```rust ```rust
fn main() { fn main() {
println!("{}{}",1,2); // =>"12" println!("{}{}", 1, 2); // =>"12"
println!("{1}{0}",1,2); // =>"21" println!("{1}{0}", 1, 2); // =>"21"
// => Alice, this is Bob. Bob, this is Alice // => Alice, this is Bob. Bob, this is Alice
println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob"); println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");
println!("{1}{}{0}{}",1,2); // => 2112 println!("{1}{}{0}{}", 1, 2); // => 2112
} }
``` ```
@ -200,15 +200,25 @@ fn main() {
println!("{abc} {1}", abc = "def", 2); println!("{abc} {1}", abc = "def", 2);
``` ```
```rust
error: positional arguments cannot follow named arguments
--> src/main.rs:4:36
|
4 | println!("{abc} {1}", abc = "def", 2);
| ----- ^ positional arguments must be before named arguments
| |
| named argument
```
## 格式化参数 ## 格式化参数
格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位: 格式化输出,意味着对输出格式会有更多的要求,例如只输出浮点数的小数点后两位:
```rust ```rust
fn main() { fn main() {
let v = 3.1415926; let v = 3.1415926;
// Display => 3.14 // Display => 3.14
println!("{:.2}",v); println!("{:.2}", v);
// Debug => 3.14 // Debug => 3.14
println!("{:.2?}",v); println!("{:.2?}", v);
} }
``` ```
@ -260,11 +270,11 @@ fn main() {
fn main() { fn main() {
// 以下全部都会补齐5个字符的长度 // 以下全部都会补齐5个字符的长度
// 左对齐 => Hello x ! // 左对齐 => Hello x !
println!("Hello {:<5}!","x"); println!("Hello {:<5}!", "x");
// 右对齐 => Hello x // 右对齐 => Hello x
println!("Hello {:>5}!","x"); println!("Hello {:>5}!", "x");
// 居中对齐 => Hello x ! // 居中对齐 => Hello x !
println!("Hello {:^5}!","x"); println!("Hello {:^5}!", "x");
// 对齐并使用指定符号填充 => Hello x&&&&! // 对齐并使用指定符号填充 => Hello x&&&&!
// 指定符号填充的前提条件是必须有对齐字符 // 指定符号填充的前提条件是必须有对齐字符
@ -278,12 +288,12 @@ fn main() {
fn main() { fn main() {
let v = 3.1415926; let v = 3.1415926;
// 保留小数点后两位 => 3.14 // 保留小数点后两位 => 3.14
println!("{:.2}",v); println!("{:.2}", v);
// 带符号保留小数点后两位 => +3.14 // 带符号保留小数点后两位 => +3.14
println!("{:+.2}",v); println!("{:+.2}", v);
// 不带小数 => 3 // 不带小数 => 3
println!("{:.0}",v); println!("{:.0}", v);
// 通过参数来设定精度 => 3.1416,相当于{:.4} // 通过参数来设定精度 => 3.1416相当于{:.4}
println!("{:.1$}", v, 4); println!("{:.1$}", v, 4);
let s = "hi我是Sunface孙飞"; let s = "hi我是Sunface孙飞";
@ -334,8 +344,8 @@ fn main() {
#### 指针地址 #### 指针地址
```rust ```rust
let v= vec![1,2,3]; let v= vec![1, 2, 3];
println!("{:p}",v.as_ptr()) // => 0x600002324050 println!("{:p}", v.as_ptr()) // => 0x600002324050
``` ```
#### 转义 #### 转义
@ -350,7 +360,7 @@ fn main() {
} }
``` ```
## 在格式化字符串时捕获环境中的值Rust1.58新增) ## 在格式化字符串时捕获环境中的值Rust 1.58 新增)
在以前,想要输出一个函数的返回值,你需要这么做: 在以前,想要输出一个函数的返回值,你需要这么做:
```rust ```rust
@ -364,7 +374,7 @@ fn main() {
println!("Hello, {person}!", person = p); println!("Hello, {person}!", person = p);
} }
``` ```
问题倒也不大但是一旦格式化字符串长了后就会非常冗余而在1.58后,我们可以这么写: 问题倒也不大,但是一旦格式化字符串长了后,就会非常冗余,而在 1.58 后,我们可以这么写:
```rust ```rust
fn get_person() -> String { fn get_person() -> String {
String::from("sunface") String::from("sunface")
@ -381,8 +391,8 @@ for (name, score) in get_scores() {
println!("{name}: {score:width$.precision$}"); println!("{name}: {score:width$.precision$}");
} }
``` ```
但也有局限,它只能捕获普通的变量,对于更复杂的类型(例如表达式),可以先将它赋值给一个变量或使用以前的`name = expression`形式的格式化参数。 但也有局限,它只能捕获普通的变量,对于更复杂的类型(例如表达式),可以先将它赋值给一个变量或使用以前的 `name = expression` 形式的格式化参数。
目前除了`panic!`外,其它接收格式化参数的宏,都可以使用新的特性。对于`panic!` 而言,如果还在使用`Rust2015`或`2018`大版本 ,那`panic!("{ident}")`依然会被当成 正常的字符串来处理,同时编译器会给予`warn`提示。而对于`2021版本`,则可以正常使用: 目前除了 `panic!` 外,其它接收格式化参数的宏,都可以使用新的特性。对于 `panic!` 而言,如果还在使用 `2015版本``2018版本`,那 `panic!("{ident}")` 依然会被当成 正常的字符串来处理,同时编译器会给予 `warn` 提示。而对于 `2021版本` ,则可以正常使用:
```rust ```rust
fn get_person() -> String { fn get_person() -> String {
String::from("sunface") String::from("sunface")

Loading…
Cancel
Save