免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

std1.97.1——fmt模块总览

std1.97.1——fmt模块总览 目录0. 准备0.1 查看当前toolchain的std文档0.2 语法和词法结构1. std::fmt2. 格式化字符串2.1 位置参数2.2 命名参数2.3 格式化参数2.3.1 宽度2.3.2 填充/对齐2.3.3 标志2.3.4 精度2.3.5 本地化2.3.6 转义3. 语法4. Trait4.1 实现Display trait4.2 Display与Debug的区别5. 宏5.1 write!宏5.2 print!宏5.3 eprint!5.4 format_args!参考0. 准备0.1 查看当前toolchain的std文档命令rustup doc --std0.2 语法和词法结构1. std::fmt地位用于格式化与打印字符串的工具集。本模块包含了对format!语法扩展的运行时支持。该宏由编译器实现会生成对本模块的调用以便在运行时将参数格式化为字符串。2. 格式化字符串格式化字符串语法适用于这些宏format!、write!、writeln!、print!、println!、eprint!、eprintln!、format_args!。format!宏旨在让熟悉C语言的printf和fprintf函数或Python的str.format函数的开发者感到易于上手。format!宏返回一个String下面仅是为了将format!构建的String输出到控制台才调用println!宏fnmain(){println!({},format!(Hello));println!({},format!(Hello, {}!,world));println!({},format!(The number is {},1));println!({},format!({:?},(3,4)));println!({},format!({value},value4));letpeopleRustaceans;println!({},format!(Hello {people}!));println!({},format!({} {},1,2));println!({},format!({:04},42));println!({},format!({:#?},(100,200)));}format!宏的第一个参数是格式化字符串它是字符串字面量不能是传入的变量(以便编译器进行有效性检查)。随后编译器会解析格式字符串并判断所提供的参数列表是否符合传递给该格式字符串。若要将单个值转换为字符串请使用to_string方法。要使用to_string该值必须实现当前模块的Display trait。2.1 位置参数每个格式化占位符都可以指明它引用的是参数列表中的第几个参数如果省略则认为它引用参数列表中的下一个参数。例如格式化字符串{} {} {}会使用参数列表中的三个参数并按照给出的顺序格式化它们。而格式化字符串{2} {1} {0}则会按相反顺序格式化它们。一旦开始混用显式编号和隐式顺序这两种位置指定方式事情就完全不一样了可以下一个值这种指定方式看作一个在值列表上移动的迭代器。每次遇到一个{}迭代器在值列表上向后移动一步(索引增1)。具体例子如下fnmain(){println!({},format!({1} {} {0} {},1,2));}对上面这个例子的处理1. 遇到{1}时直接取参数列表索引为1处的值2。2. 遇到{}时直接获取内部隐式迭代器当前索引位置0处的值1向后移动一步内部隐式迭代器索引变为1。3. 遇到{0}时直接取参数列表索引为0处的值1。4. 遇到{}时直接获取内部隐式迭代器当前索引位置1处的值2向后移动一步内部隐式器此时已指向参数列表末尾之后无法再获取值(若无更多格式占位符则无问题)。格式化字符串必须使用参数列表中的所有参数否则编译会报错。可以在格式化字符串中多次引用参数列表中的同一个参数。2.2 命名参数Rust本身并不像Python一样函数中拥有关键字参数。format!宏作为语法扩展可以利用命名参数。参数列表末尾可以给出命名参数对应的值以identifier expression的形式。以下是format!宏使用命名参数的例子fnmain(){println!({},format!({argument},argumenttest));println!({},format!({name} {},1,name2));println!({},format!({a} {c} {b},aa,bb,c3));}如果命名参数没有出现在参数列表中format!会引用当前作用域中同名的变量fnmake_string(a:u32,b:str)-String{format!({b} {a})}fnmain(){letargument22;println!({},format!({argument}));println!({},make_string(927,label));}提供的命名参数若未被格式化字符串使用会导致编译错误。2.3 格式化参数每个被格式化的参数可以通过若干格式化参数进行转换(对应format_spec格式说明)。这些格式化参数会影响被格式化内容最终呈现的字符串形式。格式语法中的冒号:用于分隔输入数据的标识符与格式化参数冒号本身不改变任何内容仅用于引入后续的格式化参数。fnmain(){leta5;letba;println!({a:e} {b:p});}2.3.1 宽度fnmain(){println!(Hello {:5}!,x);println!(Hello {:1$}!,x,5);println!(Hello {1:0$}!,5,x);println!(Hello {:width$}!,x,width5);letwidth5;println!(Hello {:width$}!,x);}这是一个用于指定格式化内容所占最小宽度的参数。如果值的字符串长度不足以填满这么多字符则会使用下一节填充/对齐指定的填充字符来补足所需空间。宽度也可以通过引用参数列表中的另一个参数来动态提供只需在参数名后面加$后缀。使用{:N$}可以引用参数列表中第N个位置的参数N为从0开始的整数或使用{:name$}来引用参数列表中的命名参数。参数列表被宽度引用的参数类型必须是usize。使用带$的语法引用参数列表中的参数不会影响下一个参数计数器因此通常建议按位置引用参数列表中的参数或者直接使用命名参数。2.3.2 填充/对齐fnmain(){println!(Hello {:5}!,x);println!(Hello {:-5}!,x);println!(Hello {:^5}!,x);println!(Hello {:5}!,x);}可选的填充字符和对齐方式通常与宽度参数配合使用。它们必须定义在宽度之前紧跟在冒号:之后。这表示当被格式化的值宽度小于指定宽度时会在其周围额外填充一些字符。填充支持以下不同的对齐方式[fill]参数在宽度列中左对齐。[fill]^参数在宽度列中居中对齐。[fill]参数在宽度列中右对齐。对于居中对齐来说宽度N NN为奇数左右填充字符的数量均为N − 1 2 \frac{N-1}{2}2N−1​宽度N NN为偶数左边填充字符数量为N 2 − 1 \frac{N}{2}-12N​−1右边填充字符数量为N 2 \frac{N}{2}2N​。fnmain(){println!(Hello {:-^5}!,x);println!(Hello {:-^6}!,x);}非数值类型的默认填充字符为空格默认对齐方式为左对齐。数值类型的默认填充字符同样为空格默认对齐方式为右对齐。如果为数值类型指定了0标志则隐式填充字符变为0。fnmain(){println!(Hello {:5}!,x);println!(Hello {:5}!,1);}某些类型可能并未实现对齐功能尤其是Debug trait通常不支持对清。确保正确应用填充的一个好方法是先格式化输入内容然后对得到的字符串进行填充以获得最终输出fnmain(){println!(Hello {:^15}!,format!({:?},Some(hi)));}2.3.3 标志fnmain(){println!(Hello {:}!,5);println!({:#x}!,27);println!(Hello {:05}!,5);println!(Hello {:05}!,-5);println!({:#010x}!,27);}这些标志用于改变格式化器的行为。用于数值类型表示始终打印正负号。有符号数值的正号和符号都会被打印无符号数值打印正号。#此标志应使用“替代”形式的打印。替代形式包括#?美化Debug的格式化打印(添加换行和缩进)。#x在参数前加0x。#X在参数前加0x。#b在参数前加0b。#o在参数前加0o。fnmain(){println!({:#?},(100,200));println!({:#x},255);println!({:#X},255);println!({:#b},10);println!({:#o},64);}0用于指示数值格式化时应使用0字符填充至width且填充需要感知符号。格式{:08}对于整数1会产生00000001对于整数-1会产生-0000001。填充零始终放在符号(如果有)之后、数字(宽度的数字)之前。与#标志一起使用的时填充0在前缀之后、数字之前。前缀包含在总宽度中。此标志会覆盖前面的填充字符和对齐使其不产生效果。fnmain(){println!({:05},5);println!({:05},-2);println!({:06},5.1);}2.3.4 精度对于非数字类型这可以被视为最大宽度。如果字符串长于此宽度则会被截断到这么多字符并且在设置了填充、对齐和宽度参数时截断后的值会以正确的填充、对齐和宽度输出。fnmain(){println!({:10.7},Hello world from Rust);}对于整数类型精度被忽略。对于浮点类型精度指示应打印小数点后多少位数字。有三种方式可以指定所需的精度1. 整数.NN本身为精度。2. .N/name$N引用参数列表中对应位置的参数name引用参数列表中的命名参数。3. .*使用形如{:spec.*}的格式字符串其中spec指的是其他参数会消耗隐式迭代器当前及其后的两个输入当前位置的输入作为usize精度下一个输入才是要打印的值。使用形如{arg:spec.*}的格式字符串则arg显式指明要打印的值精度则取自隐式迭代器当前位置的输入。fnmain(){// 遇到{0}位置参数对应位置0处的x// 遇到{1:.5}1在冒号之前是位置参数对应位置1处的0.01.5对应精度5println!(Hello {0} is {1:.5},x,0.01);// 遇到{1}位置参数对应位置1处的x// 遇到{2:.0$}2在冒号之前是位置参数对应位置2处的0.01.0$表示精度取位置0处的5println!(Hello {1} is {2:.0$},5,x,0.01);// 遇到{0}位置参数对应位置0处的x// 遇到{2:.1$}2在冒号之前是位置参数对应位置2处的0.01.1$表示精度取位置1处的5println!(Hello {0} is {2:.1$},x,5,0.01);// 遇到{}隐式迭代器当前位置0对应x// 遇到{:.*}符合{:spec.*}隐式迭代器当前位置1精度对应5// 下一个位置2对应的0.01是打印的值println!(Hello {} is {:.*},x,5,0.01);// 遇到{1}位置参数对应位置1处的x// 遇到{2:.*}符合{arg:spec.*}2在冒号前指明打印的值为位置2处的0.01// 隐式迭代器当前位置0精度取位置0处的5println!(Hello {1} is {2:.*},5,x,0.01);// 遇到{}隐式迭代器当前位置0对应x// 遇到{2:.*}符合{arg:spec.*}2在冒号前指明打印的值为位置2处的0.01// 隐式迭代器当前位置1精度取位置1处的5println!(Hello {} is {2:.*},x,5,0.01);// 遇到{}隐式迭代器当前位置0对应x// 遇到{number:.prec$}number在冒号前是命名参数对应0.01.prec$表示精度取命名参数prec的值5println!(Hello {} is {number:.prec$},x,prec5,number0.01);}fnmain(){// 遇到{}, 隐式迭代器当前位置0对应Hello// 遇到{name:.*}匹配{arg:spec.*}name在冒号之前是命名参数对应1234.56// 隐式迭代器当前位置1精度取位置1处的3println!({}, {name:.*} has 3 fractional digits,Hello,3,name1234.56);// 遇到{}隐式迭代器当前位置0对应Hello// 遇到{name:.*}匹配{arg:spec.*}name在冒号之前是命名参数对应1234.56// 隐式迭代器当前位置1精度取位置1处的3// 精度对应打印值的类型是非数值类型产生截断截断后打印的值是123println!({}, {name:.*} has 3 characters,Hello,3,name1234.56);// 遇到{}隐式迭代器当前位置0对应Hello// 遇到{name:8.*}匹配{arg:spec.*}name在冒号之前是命名参数对应1234.56// 8指明右对齐宽度为8// 隐式迭代器当前位置1精度取位置1处的3// 精度对应打印值的类型是非数值类型产生截断截断后打印的值是123// 最终{name:8.*}为 123println!({}, {name:8.*} has 3 right-aligned characters,Hello,3,name1234.56);}在截断浮点数时Rust使用四舍六入五成双这是IEEE 754中的默认舍入模式。四舍六入五成双1. 末位5舍去。2. 末位5进一。3. 末尾5看前一位奇数进一偶数舍去。fnmain(){println!({0:.1$e},12345,3);println!({0:.1$e},12355,3);}2.3.5 本地化Rust标准库提供的格式化函数没有任何区域概念无论用户配置如何在所有系统上都会产生相同的结果。2.3.6 转义字面字符{和}可以通过在前面加上相同的字符来包含在字符串中。fnmain(){println!(Hello {{}});println!({{ Hello);}3. 语法格式化语法的文法format_string : text [ maybe_format text ] * maybe_format : { { | } } | format format : { [ argument ] [ : format_spec ] [ ws ] * } argument : integer | identifier format_spec : [[fill]align][sign][#][0][width][.precision][type] fill : character align : | ^ | sign : | - width : count precision : count | * type : ? | x? | X? | o | x | X | p | b | e | E count : parameter | integer parameter : argument $text蹦年包含任何{或}。ws是Rust定义的空白字符。integer是一个十进制整数可包含前导零且必须能放入usize类型。identifier是Rust定义的IDENTIFIER_OR_KEYWORD不能是单个下划线。fnmain(){// {{ - {// {name:-0$.pre$}name在冒号前是命名参数对应test// -是填充字符是左对齐0$是宽度对应位置0处的值10// .pre$是精度pre是精度引用的命名参数对应值3// name不是数值类型会因精度产生截断截断后的值为tes// }} - }println!({{ {name:-0$.pre$} }},10,nametest,pre3);}4. Trait格式占位符中的每个type对应一个特定的格式化Trait。type到trait的映射如下未指定 Display? Debugx? Debug(仅对整数触发十六进制小写)X? Debug(仅对整数触发十六进制大写)o Octalx LowerHexX UpperHexp Pointerb binarye LowerExpE UpperExp这意味着任何实现了fmt::Binary trait的类型都可以用{:b}格式化。标准库为许多原始类型实现了这些trait。当未指定type时使用的trait是Display。为自己的类型实现格式化trait时你需要实现以下签名的方法fnfmt(self,f:mutfmt::Formatter_)-fmt::Result(你的类型将按引用作为self传入然后函数应向实现了fmt::Write的Formatter f输出内容。每个格式化trait的实现都有责任正确遵守所请求的格式化参数。这些参数的值可以通过Formatter结构体的方法访问。为了帮助完成此工作Fromatter结构体还提供了一些辅助方法。此外此函数的返回值是fmt::Result它是Result(),std::fmt::Error的别名。格式化本身不会失败类型不匹配、精度写错在编译时就解决了。返回Result的唯一原因是最终要向底层输出流写入而写入可能失败fmt::Error就是用来传递这个失败的。所以你的实现只需要做一件事调用Formatter的方法时它返回Err就原样转发其余情况返回Ok不要自行凭空返回Err。4.1 实现Display traitusestd::fmt;#[derive(Debug)]structVector2D{x:isize,y:isize,}implfmt::DisplayforVector2D{fnfmt(self,f:mutfmt::Formatter_)-fmt::Result{// f 实现了 Write trait这正是 write! 宏所期望的。// 请注意此格式化忽略了格式字符串提供的各种标志。write!(f,({}, {}),self.x,self.y)}}// 不同的 trait 允许类型的不同输出形式。// 此格式的含义是打印向量的模。implfmt::BinaryforVector2D{fnfmt(self,f:mutfmt::Formatter_)-fmt::Result{letmagnitude(self.x*self.xself.y*self.y)asf64;letmagnitudemagnitude.sqrt();// 使用 Formatter 对象上的辅助方法 pad_integral// 来遵守格式化标志。详见方法文档// pad 函数可用于填充字符串。letdecimalsf.precision().unwrap_or(3);letstringformat!({magnitude:.decimals$});f.pad_integral(true,,string)}}fnmain(){letmyvectorVector2D{x:3,y:4};println!({myvector});// (3, 4)println!({myvector:?});// Vector2D {x: 3, y:4}println!({myvector:10.3b});// 5.000}4.2 Display与Debug的区别Display实现断言该类型始终可以忠实地表示为UTF-8字符串。并非期望所有类型都实现Display trait。Debug实现应为所有公开类型实现。输出通常应尽可能忠实地表示内部状态。Debug trait的目的是方便调试Rust代码。在大多数情况下使用#[derive(Debug)]就足够了也是推荐的做法。5. 宏5.1 write!宏write!和writeln!是两个用于将格式化字符串输出到指定流的宏。用于避免格式字符串的中间分配直接写入输出。在底层此函数实际调用的是std::io::Write和std::fmt::Write trait上定义的write_fmt函数。usestd::fmt::Write;fnmain(){letmutwString::new();write!(mutw,Hello {}!,world).unwrap();println!({:?},w);}5.2 print!宏此宏和println!将输出发送到stdout。与write!宏类似这些宏的目标是在打印输出时避免中间分配。fnmain(){print!(Hello {}!,world);println!(I have a newline {},character at the end);}5.3 eprint!eprint!和eprintln!宏分别与print!和println!相同区别在于它们将输出发送到stderr。5.4 format_args!format_args!是一个奇特的宏用于安全地传递一个描述格式字符串的不透明对象。此对象不需要任何堆分配即可创建且引用栈上的信息。在底层所有宏都是基于它实现的。usestd::fmt;usestd::io::{self,Write};fnmy_fmt_fn(args:fmt::Arguments_){write!(mutio::stdout(),{args}).unwrap();}fnmain(){letmutsome_writerio::stdout();write!(mutsome_writer,{},format_args!(print with a {},macro)).unwrap();my_fmt_fn(format_args!(, or a {} too,function));}format_args!宏的结果是fmt::Arguments类型的值。然后可以将此结构体传递给本模块的write和format函数以处理格式字符串。此宏的目标是在处理格式化字符串时进一步避免中间分配。参考1、Rust std::fmt
返回列表