当前位置: 首页 > news >正文

Rust之构建命令行程序(三):重构改进模块化和错误处理

开发环境

  • Windows 10
  • Rust 1.74.1

 

  • VS Code 1.85.1

项目工程

这次创建了新的工程minigrep.

重构改进模块化和错误处理 

为了改进我们的程序,我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先,我们的main函数现在执行两项任务:解析参数和读取文件。随着我们程序的增长,main处理的独立任务的数量也会增加。随着一个功能获得更多的职责,它变得更难推理、更难测试、更难在不破坏其某个部分的情况下进行更改。最好将功能分开,每个功能负责一项任务。

这个问题还与第二个问题有关:尽管queryfile_path是程序的配置变量,但contents等变量也用于执行程序的逻辑。main越长,我们需要纳入范围的变量就越多;范围内的变量越多,就越难跟踪每个变量的用途。最好将配置变量分组到一个结构中,以使其目的明确。 

第三个问题是,我们已经使用expect在读取文件失败时打印了一条错误消息,但错误消息只是打印了Should have been able to read the file。读取文件失败的原因有很多:例如,文件可能丢失,或者我们可能没有权限打开它。现在,不管情况如何,我们都会为所有内容打印相同的错误消息,这不会给用户任何信息! 

第四,我们重复使用expect来处理不同的错误,如果用户在没有指定足够参数的情况下运行我们的程序,他们将从Rust获得一个索引越界错误,该错误无法清楚地解释问题。如果所有的错误处理代码都在一个地方是最好的,这样将来的维护人员在需要更改错误处理逻辑时就只有一个地方可以查阅代码。将所有错误处理代码放在一个地方还将确保我们打印的消息对最终用户有意义。 

让我们通过重构项目来解决这四个问题。 

二进制项目的关注点分离

将多项任务的责任分配给main功能的组织问题在许多二元项目中很常见。因此,Rust社区开发了当main开始变大时分割二进制程序的独立关注点的指导方针。该过程包括以下步骤: 

  • 将你的程序分成一个main.rs和一个lib.rs,并将你的程序逻辑转移到lib.rs。 
  • 只要您的命令行解析逻辑很小,它就可以保留在main.rs中。
  • 当命令行解析逻辑开始变得复杂时,将其从main.rs中提取出来并移动到lib.rs中。

在此流程之后,main函数的职责应仅限于以下方面: 

  • 使用参数值调用命令行解析逻辑
  • 设置其他配置
  • lib.rs中调用运行函数 
  • run返回错误时处理错误

这种模式是关于分离关注点的:main.rs处理程序的运行,lib.rs处理手头任务的所有逻辑。因为您不能直接测试main函数,所以这种结构允许您通过将程序的逻辑移入lib.rs中的函数来测试程序的所有逻辑。保留在main.rs中的代码将足够小,可以通过读取它来验证其正确性。让我们按照这个过程重新编写我们的程序。 

提取参数解析器

我们将解析参数的功能提取到一个函数中,main将调用该函数来准备将命令行解析逻辑移动到src/lib.rs .示例12-5显示了main调用新函数parse_config的新开始,我们暂时将在src/main.rs中定义该函数。 

文件名:src/main.rs

use std::env;
use std::fs;fn main() {let args: Vec<String> = env::args().collect();let (query, file_path) = parse_config(&args);// --snip--println!("With value:\n{} \n{}", query, file_path);
}fn parse_config(args: &[String]) -> (&str, &str) {let query = &args[1];let file_path = &args[2];(query, file_path)
}

 示例12-5 从main中提取parse_config函数

 我们仍然将命令行参数收集到一个向量中,但是我们没有将索引1处的参数值分配给变量query,也没有将索引2处的参数值分配给主函数中的变量file_path,而是将整个向量传递给parse_config函数。parse_config函数然后保存逻辑,该逻辑确定哪个参数进入哪个变量并将值传递回main。我们仍然在main中创建queryfile_path变量,但是main不再负责确定命令行参数和变量如何对应。

对于我们的小程序来说,这种返工似乎有些过头了,但我们正在以小而渐进的步骤进行重构。在做出这一更改后,再次运行程序以验证参数解析是否仍然有效。经常检查你的进展是有好处的,有助于在问题出现时找出问题的原因。

对配置值进行分组

我们可以采取另一个小步骤来进一步改进parse_config函数。目前,我们正在返回一个元组,但随后我们立即再次将该元组拆分为单独的部分。这表明我们可能还没有正确的抽象概念。

另一个显示还有改进空间的指标是parse_configconfig部分,这意味着我们返回的两个值是相关的,并且都是一个配置值的一部分。除了将这两个值分组到一个元组中之外,我们目前没有在数据结构中传达这种含义;相反,我们将把这两个值放入一个结构中,并为每个结构字段赋予一个有意义的名称。这样做将使该代码的未来维护者更容易理解不同值之间的关系以及它们的用途。 

示例12-6显示了对parse_config函数的改进。 

文件名:src/main.rs

use std::env;
use std::fs;fn main() {let args: Vec<String> = env::args().collect();let config = parse_config(&args);println!("Searching for {}", config.query);println!("In file {}", config.file_path);let contents = fs::read_to_string(config.file_path).expect("Should have been able to read the file");// --snip--println!("With text:\n{contents}");
}struct Config {query: String,file_path: String,
}fn parse_config(args: &[String]) -> Config {let query = args[1].clone();let file_path = args[2].clone();Config { query, file_path }
}

 示例12-6:重构parse_config以返回一个Config结构的实例

我们添加了一个名为Config的结构,该结构定义了名为queryfile_path的字段。parse_config的签名现在表明它返回一个配置值。在parse_config的主体中,我们曾经返回引用args中的字符串值的字符串片段,现在我们将Config定义为包含拥有的字符串值。main中的args变量是参数值的所有者,并且只让parse_config函数借用它们,这意味着如果Config试图获取args中的值的所有权,我们将违反Rust的借用规则。 

我们可以通过多种方式管理String数据;最简单的方法是对值调用clone方法,尽管这种方法有些低效。这将为Config实例创建数据的完整副本,这比存储对字符串数据的引用花费更多的时间和内存。然而,克隆数据也使我们的代码非常简单,因为我们不必管理引用的生存期;在这种情况下,牺牲一点性能来获得简单性是值得的。 

使用克隆的利弊

许多Rust开发者倾向于避免使用clone来解决所有权问题,因为它的运行时成本很高。在后续章节中,你将学习如何在这种情况下使用更有效的方法。但是现在,复制几个字符串以继续取得进展是可以的,因为您将只复制这些副本一次,并且您的文件路径和查询字符串非常小。最好有一个效率稍低的工作程序,而不是第一次就试图过度优化代码。随着您对Rust的经验越来越丰富,开始使用最有效的解决方案将变得更加容易,但就目前而言,调用clone是完全可以接受的。 

我们已经更新了main,因此它将parse_config返回的config实例放入名为config的变量中,并且我们更新了以前使用单独的queryfile_path变量的代码,因此它现在改为使用Config结构上的字段。 

现在我们的代码更清楚地传达了queryfile_path是相关的,它们的目的是配置程序将如何工作。任何使用这些值的代码都知道在config实例中为其目的命名的字段中找到它们。

为配置创建构造函数

到目前为止,我们已经从main中提取了负责解析命令行参数的逻辑,并将其放在parse_config函数中。这样做有助于我们看到查询和file_path值是相关的,并且这种关系应该在我们的代码中传达。然后,我们添加了一个配置结构来命名queryfile_path的相关用途,并能够从parse_config函数中将值的名称作为结构字段名返回。 

既然parse_config函数的目的是创建一个配置实例,我们可以将parse_config从一个普通函数更改为一个名为new的函数,该函数与配置结构相关联。进行这一更改将使代码更符合习惯。我们可以通过调用String::new在标准库中创建类型的实例,例如String。类似地,通过将parse_config更改为与config相关联的新函数,我们将能够通过调用Config::new来创建Config的实例。示例12-7显示了我们需要进行的更改。 

文件名:src/main.rs

use std::env;
use std::fs;fn main() {let args: Vec<String> = env::args().collect();let config = Config::new(&args);// --snip--println!("With config:\n{} \n {}", config.query, config.file_path);
}// --snip--impl Config {fn new(args: &[String]) -> Config {let query = args[1].clone();let file_path = args[2].clone();Config { query, file_path }}
}

 示例12-7:将parse_config更改为Config::new

我们更新了main中调用parse_config的地方,改为调用Config::new。我们已经将parse_config的名称改为new,并将其移动到impl块中,该块将新函数与config相关联。请再次尝试编译此代码以确保它正常工作。

修复错误处理

现在我们将修复我们的错误处理。回想一下,如果args向量包含的项少于三项,则尝试访问索引1或索引2处的args向量中的值将导致程序崩溃。尝试不带任何参数运行程序;它将看起来像这样:

$ cargo runCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.0sRunning `target/debug/minigrep`
thread 'main' panicked at 'index out of bounds: the len is 1 but the index is 1', src/main.rs:27:21
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

 index out of bounds: the len is 1 but the index is 1,是一条针对程序员的错误消息。它不会帮助我们的最终用户理解他们应该做什么。让我们现在解决这个问题。

改进错误信息

在示例12-8中,我们在new函数中添加了一个检查,它将在访问索引1和2之前验证切片是否足够长。如果切片不够长,程序会惊慌并显示更好的错误消息。 

文件名:src/main.rs

    // --snip--fn new(args: &[String]) -> Config {if args.len() < 3 {panic!("not enough arguments");}// --snip--

 示例12-8:添加对参数数量的检查

value参数超出有效值范围时。在这里,我们不是检查值的范围,而是检查args的长度至少为3,并且函数的其余部分可以在满足该条件的假设下运行。如果args的项目少于三个,则该条件将为真,我们称之为panic!宏立即结束程序。 

有了new中的这几行额外代码,让我们再次不带任何参数运行程序,看看错误现在是什么样子的: 

$ cargo runCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.0sRunning `target/debug/minigrep`
thread 'main' panicked at 'not enough arguments', src/main.rs:26:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

 这个输出更好:我们现在有了一个合理的错误消息。然而,我们也有不想给用户的无关信息。也许使用我们在之前的示例9-13中使用的技术在这里不是最好的:恐慌的呼唤!更适合于编程问题而不是使用问题。

返回结果而不是调用panic!

相反,我们可以返回一个Result,该值在成功的情况下将包含一个Config实例,并在错误的情况下描述问题。我们还将把函数名从new改为build,因为许多程序员希望新函数永远不会失败。当Config::buildmain通信时,我们可以使用结果类型来表示有问题。然后,我们可以更改main,将Err变体转换为更实用的错误,这样用户就不用担心周围的关于线程“main”和RUST_BACKTRACEpanic!原因。

示例12-9显示了我们需要对我们现在调用的Config::build函数的返回值以及返回结果所需的函数体进行的更改。请注意,直到我们更新main之后才会编译,我们将在下一个清单中更新main。 

文件名:src/main.rs

impl Config {fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config { query, file_path })}
}

 示例12-9:从Config::build返回Result

我们的build函数在成功情况下返回一个带有Config实例的Result,在错误情况下返回一个&‘static str。我们的错误值将总是具有“static生存期”的字符串文字。 我们在函数体中做了两处修改:不再调用panic!当用户没有传递足够的参数时,我们现在返回一个Err值,并且我们已经将配置返回值包装在Ok中。这些更改使函数符合其新的类型签名。 

Config::build返回一个Err值允许main函数处理从build函数返回的Result,并在出错时更干净地退出进程。 

调用Config::build并处理错误

为了处理错误情况并打印用户友好的消息,我们需要更新main来处理Config::build返回的Result,如示例12-10所示。我们还将负责退出命令行工具,并给出一个非零错误代码,以免引起panic!而是手动实现。非零退出状态是一种惯例,它向调用我们程序的进程发出信号,表明程序以错误状态退出。

文件名:src/main.rs

use std::process;fn main() {let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {err}");process::exit(1);});// --snip--// 全部代码
use std::env;
use std::process;fn main() {let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {err}");process::exit(1);});
}struct Config {query: String,file_path: String,
}impl Config {fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config { query, file_path })}
}

示例12-10:如果构建Config失败,退出并显示错误代码

在这个示例中,我们使用了一个尚未详细介绍的方法:unwrap_or_else,它是由标准库在Result<T, E>上定义的。使用unwrap_or_else允许我们定义一些自定义的、非紧急的!错误处理。如果结果是一个Ok值,该方法的行为类似于unwrap:它返回Ok正在换行的内部值。但是,如果该值是一个Err值,该方法将调用闭包中的代码,这是一个我们定义的匿名函数,并作为参数传递给unwrap_or_else。我们将在后续章节更详细地讨论闭包。现在,您只需要知道unwrap_or_else将把err的内部值传递给出现在竖线之间的参数Err中的闭包,在本例中,该值是我们在示例12-9中添加的静态字符串“not follow arguments”。闭包中的代码可以在运行时使用err值。 

我们添加了一个新的use行,将标准库中的process纳入范围。在错误情况下运行的闭包中的代码只有两行:我们打印err值,然后调用process::exitprocess::exit函数将立即停止程序并返回作为退出状态代码传递的数字。这类似于panic!我们在示例12-8中使用的基于的处理,但是我们不再得到所有额外的输出。让我们来试试:

$ cargo runCompiling minigrep v0.1.0 (file:///projects/minigrep)Finished dev [unoptimized + debuginfo] target(s) in 0.48sRunning `target/debug/minigrep`
Problem parsing arguments: not enough arguments

太好了!这个输出对我们的用户更友好。

从main提取逻辑

现在我们已经完成了配置解析的重构,让我们转向程序的逻辑。正如我们在“二进制项目的关注点分离”中所述,我们将提取一个名为run的函数,该函数将保存main函数中当前不涉及设置配置或处理错误的所有逻辑。当我们完成时,main将简洁并易于检查验证,我们将能够为所有其他逻辑编写测试。

示例12-11显示了提取的run函数。目前,我们只是对提取函数进行了微小的、渐进的改进。我们仍在定义src/main.rs中的函数。

文件名:src/main.rs

fn main() {// --snip--println!("Searching for {}", config.query);println!("In file {}", config.file_path);run(config);
}fn run(config: Config) {let contents = fs::read_to_string(config.file_path).expect("Should have been able to read the file");println!("With text:\n{contents}");
}// --snip--
// 全部代码
use std::env;
use std::process;
use std::fs;fn main() {// --snip--let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {err}");process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.file_path);run(config);
}struct Config {query: String,file_path: String,
}fn run(config: Config) {let contents = fs::read_to_string(config.file_path).expect("Should have been able to read the file");println!("With text:\n{contents}");
}impl Config {fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config { query, file_path })}
}

示例12-11:提取包含剩余程序逻辑的run函数

run函数现在包含main中所有剩余的逻辑,从读取文件开始。run函数将Config实例作为参数。 

从运行函数返回错误

通过将剩余的程序逻辑分离到run函数中,我们可以改进错误处理,就像我们在示例12-9中对Config::build所做的那样。当出现问题时,run函数将返回Result<T, E>,而不是通过调用expect让程序崩溃。这将让我们以用户友好的方式进一步巩固main中处理错误的逻辑。示例12-12显示了我们需要对run的签名和主体进行的更改。 

文件名:src/main.rs

use std::error::Error;// --snip--fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.file_path)?;println!("With text:\n{contents}");Ok(())
}// 全部代码
use std::error::Error;
use std::env;
use std::process;
use std::fs;fn main() {    let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {err}");process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.file_path);run(config);
}struct Config {query: String,file_path: String,
}fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.file_path)?;println!("With text:\n{contents}");Ok(())
}impl Config {fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config { query, file_path })}
}

示例12-12:将run函数更改为返回Result

我们在这里做了三个重大改变。首先,我们将run函数的返回类型更改为Result<(),Box<dyn Error>>。该函数先前返回了单元类型(),我们将其作为Ok情况下的返回值。 

对于错误类型,我们使用了trait对象Box<dyn Error>并且我们将std::error::Error纳入了范围,并在顶部使用了一条use语句)。我们将在后续章节讨论特征对象。现在,只需知道Box<dyn Error>意味着函数将返回实现错误特征的类型,但我们不必指定返回值将是什么特定类型。这给了我们在不同错误情况下返回不同类型的错误值的灵活性。dyn关键字是“dynamic”的缩写 。

其次,我们删除了对expect的调用,转而使用运算符,正如我们在前面章节中讨论的那样。而不是panic!出错时将从当前函数返回错误值供调用方处理。

第三,run函数现在在成功的情况下返回一个Ok值。我们已经在签名中将run函数的成功类型声明为(),这意味着我们需要将单元类型值包装在Ok值中。这个Ok(())语法一开始看起来可能有点奇怪,但是像这样使用()是惯用的方式,表明我们调用run只是为了它的副作用;它没有返回我们需要的值。

当您运行此代码时,它会编译,但会显示一条警告: 

$ cargo run the poem.txtCompiling minigrep v0.1.0 (file:///projects/minigrep)
warning: unused `Result` that must be used--> src/main.rs:19:5|
19 |     run(config);|     ^^^^^^^^^^^|= note: this `Result` may be an `Err` variant, which should be handled= note: `#[warn(unused_must_use)]` on by defaultwarning: `minigrep` (bin "minigrep") generated 1 warningFinished dev [unoptimized + debuginfo] target(s) in 0.71sRunning `target/debug/minigrep the poem.txt`
Searching for the
In file poem.txt
With text:
I'm nobody! Who are you?
Are you nobody, too?
Then there's a pair of us - don't tell!
They'd banish us, you know.How dreary to be somebody!
How public, like a frog
To tell your name the livelong day
To an admiring bog!

Rust告诉我们,我们的代码忽略了ResultResult可能表明发生了错误。但是我们没有检查是否有错误,编译器提醒我们这里可能有一些错误处理代码!让我们现在纠正这个问题。

处理运行main返回的错误

我们将使用与示例12-10中的Config::build类似的技术来检查错误并处理它们,但略有不同: 

文件名:src/main.rs

fn main() {// --snip--println!("Searching for {}", config.query);println!("In file {}", config.file_path);if let Err(e) = run(config) {println!("Application error: {e}");process::exit(1);}
}// 全部代码
use std::error::Error;
use std::env;
use std::process;
use std::fs;fn main() {    let args: Vec<String> = env::args().collect();let config = Config::build(&args).unwrap_or_else(|err| {println!("Problem parsing arguments: {err}");process::exit(1);});println!("Searching for {}", config.query);println!("In file {}", config.file_path);if let Err(e) = run(config) {println!("Application error: {e}");process::exit(1);} 
}struct Config {query: String,file_path: String,
}fn run(config: Config) -> Result<(), Box<dyn Error>> {let contents = fs::read_to_string(config.file_path)?;println!("With text:\n{contents}");Ok(())
}impl Config {fn build(args: &[String]) -> Result<Config, &'static str> {if args.len() < 3 {return Err("not enough arguments");}let query = args[1].clone();let file_path = args[2].clone();Ok(Config { query, file_path })}
}

 

 我们使用if let而不是unwrap_or_else来检查run是否返回Err值,如果返回则调用process::exit(1)run函数不会像Config::build返回Config实例那样返回我们想要解开的值。因为run在成功的情况下返回(),所以我们只关心检测到错误,所以我们不需要unwrap_or_else来返回未包装的值,它只会是()

if letunwrap_or_else函数的主体在两种情况下是相同的:我们打印错误并退出。

将代码拆分到一个Carte中 

我们的minigrep项目到目前为止看起来不错!现在我们将拆分src/main.rs文件,并将一些代码放入src/lib.rs文件中。这样我们就可以测试代码,并得到一个责任更少的src/main.rs文件。 

让我们将所有不是main函数的代码从src/main.rs移到src/lib.rs: 

  • run函数定义
  • 相关的use声明
  • Config的定义
  • Config::build函数定义

src/lib.rs的内容应该具有示例12-13所示的签名(为了简洁起见,我们省略了函数体)。注意,在我们修改示例12-14中的src/main.rs之前,这不会编译。 

文件名:src/lib.rs

use std::error::Error;
use std::fs;pub struct Config {pub query: String,pub file_path: String,
}impl Config {pub fn build(args: &[String]) -> Result<Config, &'static str> {// --snip--}
}pub fn run(config: Config) -> Result<(), Box<dyn Error>> {// --snip--
}

 示例12-13:将Configrun移入src/lib.rs

我们已经自由地使用了pub关键字:在Config、它的字段和build方法以及run函数上。我们现在有一个库箱,它有一个我们可以测试的公共API!

现在我们需要将移动到src/lib.rs的代码放入src/main.rs中的二进制crate的范围内,如示例12-14所示。

文件名:src/main.rs

use std::env;
use std::process;use minigrep::Config;fn main() {// --snip--if let Err(e) = minigrep::run(config) {// --snip--}
}

示例12-14:在src/main.rs中使用minigrep crate

我们添加一个use minigrep::Config行,将库crate中的配置类型引入二进制crate的范围,并在run函数前面加上crate名称。现在,所有功能都应该连接起来并正常工作。用cargo run运行程序,并确保一切正常。 

咻!这是一项艰巨的任务,但我们已经为未来的成功做好了准备。现在处理错误要容易得多,我们已经使代码更加模块化。从现在开始,我们几乎所有的工作都将在src/lib.rs中完成。

让我们利用这一新发现的模块性,做一些在旧代码中很难但在新代码中很容易做到的事情:我们将编写一些测试!

本章重点

  • 重构项目的意义:模块化,错误处理

  • 项目分离的意义和操作步骤

  • 如何提取参数解析器

  • 对配置值分组的操作

  • 修复和改进错误处理

  • 错误处理的主要方法

相关文章:

Rust之构建命令行程序(三):重构改进模块化和错误处理

开发环境 Windows 10Rust 1.74.1 VS Code 1.85.1 项目工程 这次创建了新的工程minigrep. 重构改进模块化和错误处理 为了改进我们的程序&#xff0c;我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先&#xff0c;我们的main函数现在执行两项任务:解析参数和…...

广和通AI解决方案“智”赋室外机器人迈向新天地!

大模型趋势下&#xff0c;行业机器人将具备更完善的交互与自主能力&#xff0c;逐步迈向AI 2.0时代&#xff0c;成为人工智能技术全面爆发的重要基础。随着行业智能化&#xff0c;更多机器人应用将从“室内”走向“室外”&#xff0c;承担更多高风险、高智能工作。复杂的室外环…...

C++I/O流——(4)格式化输入/输出(第二节)

归纳编程学习的感悟&#xff0c; 记录奋斗路上的点滴&#xff0c; 希望能帮到一样刻苦的你&#xff01; 如有不足欢迎指正&#xff01; 共同学习交流&#xff01; &#x1f30e;欢迎各位→点赞 &#x1f44d; 收藏⭐ 留言​&#x1f4dd; 含泪播种的人一定能含笑收获&#xff…...

九、K8S-label和label Selector

label和label selector 标签和标签选择器 1、label 标签&#xff1a; 一个label就是一个key/value对 label 特性&#xff1a; label可以被附加到各种资源对象上一个资源对象可以定义任意数量的label同一个label可以被添加到任意数量的资源上 2、label selector 标签选择器 L…...

【.NET Core】 多线程之(Thread)详解

【.NET Core】 多线程之&#xff08;Thread&#xff09;详解 文章目录 【.NET Core】 多线程之&#xff08;Thread&#xff09;详解一、概述二、线程的创建和使用2.1 ThreadStart用于无返回值&#xff0c;无参数的方法2.2 ParameterizedThreadStart:用于带参数的方法 三、线程的…...

苹果笔记本 macbook 在 office word 中使用 mathtype 的方法

前言 想在 MacBook 中使用 mathtype&#xff0c;去搜索&#xff0c;去 Apple Store 下载也发现没有 解决方法 打开 office Word 的「插入」中的「获取加载项」、「我的加载项」。 在应用商店中下载&#xff0c;需要登录自己的微软账号。 加载成功后就可以使用了。 注意 和…...

课表排课小程序怎么制作?多少钱?

在当今的数字化时代&#xff0c;无论是购物、支付、点餐&#xff0c;还是工作、学习&#xff0c;都离不开各种各样的微信小程序。其中&#xff0c;课表排课小程序就是许多教育机构和学校必不可少的工具。那么课表排课小程序怎么制作呢&#xff1f;又需要多少钱呢&#xff1f; …...

C语言总结十三:程序环境和预处理详细总结

了解程序的运行环境可以让我们更加清楚的程序的底层运行的每一个步骤和过程&#xff0c;做到心中有数&#xff0c;预处理阶段是在预编译阶段完成&#xff0c;掌握常用的预处理命令语法&#xff0c;可以让我们正确的使用预处理命令&#xff0c;从而提高代码的开发能力和阅读别人…...

tinyxml2

使用tinyxml2&#xff0c;得知道一些xml基础 xml tutorial--菜鸟 tinyxml2类对象 链接 结构 XMLNode 什么是节点 节点&#xff1a;元素、声明、文本、注释等。 XMLDocument xml文档(文件)对象。 作用&#xff1a; 加载xml文件&#xff0c; tinyxml2作用 先定义两个宏 …...

What is `@Controller` does?

Controller 是SpringMVC注解&#xff0c;标记一个类作为Web控制器&#xff08;Controller&#xff09;&#xff0c;负责处理HTTP请求并返回响应结果 在SpringMVC中&#xff0c;控制器类的主要职责是&#xff1a; 1、接收来自客户端的HTTP请求 2、调用服务层或其他业务逻辑组件…...

新版AndroidStudio dependencyResolutionManagement出错

在新版AndroidStudio中想像使用4.2版本或者4.3版本的AndroidStudio来构造项目&#xff1f;那下面这些坑我们就需要来避免了&#xff0c;否则会出各种各样的问题。 一.我们先来看看新旧两个版本的不同。 1.jdk版本的不同 新版默认是jdk17 旧版默认是jdk8 所以在新版AndroidSt…...

第三天业务题

3-1 你们的项目是如何进行参数校验的 在我们的项目中&#xff0c;通常使用以下2种方式进行参数校验&#xff1a; 1.手动校验&#xff1a;在方法内部&#xff0c;我们可以手动编写代码来对参数进行校验。例如&#xff0c;使用条件判断语句&#xff08;if-else&#xff09;来检…...

nestjs 装饰器

1、装饰器定义 装饰器是一种特殊的类型声明&#xff0c;它可以附加在类、方法、属性、参数上边 需开启tsconfig.json中 "experimentalDecorators":true 生成tsconfig.json文件 tsc -init 2、类装饰器 // 类装饰器 主要是通过符号添加装饰器 // 装饰器会自动把cl…...

一款开源且不限制大小可以设置过期时间的支持分享的的开源文件共享系统picoshare 部署教程

1.拉取镜像 2.部署 创建目录 mkdir -p /opt/picoshare/data 部署 其中:"somesecretpass"是密码 docker run \--env "PORT4001" \--env "PS_SHARED_SECRETsomesecretpass" \--publish 10005:4001/tcp \--volume "/opt/picoshare/data:…...

eBPF运行时安全

引言 eBPF作为当前linux系统上最为炙手可热的技术&#xff0c;通常被用于网络流量过滤和分析、系统调用跟踪、性能优化、安全监控&#xff0c;当下比较知名的项目有Cilium、Falco等。 Cilium 是一个开源的容器网络和安全性项目&#xff0c;致力于提供高效的容器通信和强大的安…...

Linux 系统中常见的命令,它们用于执行各种任务,包括文件和目录管理、系统信息查看、用户管理等

以下是一些在 Linux 系统中常见的命令&#xff0c;它们用于执行各种任务&#xff0c;包括文件和目录管理、系统信息查看、用户管理等。这里列举了一些基础的命令&#xff1a; 文件和目录管理&#xff1a; ls: 列出目录内容。 ls cd: 切换当前目录。 cd /path/to/directory …...

AutoEventWireup详解

AutoEventWireup详解 大家好&#xff0c;我是免费搭建查券返利机器人赚佣金就用微赚淘客系统3.0的小编&#xff0c;也是冬天不穿秋裤&#xff0c;天冷也要风度的程序猿&#xff01;今天&#xff0c;让我们深入探讨.NET开发中一个神奇而强大的特性——AutoEventWireup&#xff…...

SAP ABAP 自定义流水号 编号范围

前言 在开发中经常会遇到生成编号的需求(如接口报文ID&#xff0c;自建表数据主键等)&#xff1b;为此&#xff0c;SAP提供了自动编号工具&#xff0c;能根用户需求设定并自动生成一组唯一的编号。 编号范围对象的创建 1.进入事务代码SNRO&#xff0c;创建一个编号范围对象。…...

安卓、ios系统详解

一、安卓 安卓系统架构:从上至下,依次是应用层、应用框架层、系统运行库层和Linux内核层 应用层(system app):系统内置的应用程序及非系统级的应用程序都属于应用层,负责与用于进行交互,一般都用java或者kotlin来开发应用框架层(java api framework):为应用层提供所需…...

含并行连结的网络(GoogLeNet)

目录 1.GoogLeNet 2.代码 1.GoogLeNet inception不改变高宽&#xff0c;只改变通道数。GoogLeNet也大量使用1*1卷积&#xff0c;把它当作全连接用。 V3耗内存比较多&#xff0c;计算比较慢&#xff0c;但是精度比较准确。 2.代码 import torch from torch import nn from t…...

计算机网络(第六版)复习提纲3

2.3 物理层下面的传输媒体 传输媒体是数据传输系统中在发送器和接收器之间的物理通道&#xff0c;有导引型传输媒体&#xff08;有线&#xff09;和非导引型传输媒体&#xff08;无线&#xff09; 1.双绞线&#xff1a;两条铜线绞合&#xff0c;以减少干扰&#xff0c;绞合度越…...

怿星科技测试实验室获CNAS实验室认可,汽车以太网检测能力达国际标准

2023年12月27日&#xff0c;上海怿星电子科技有限公司测试实验室&#xff08;下称&#xff1a;EPT LABS&#xff09;通过CNAS实验室认可批准&#xff0c;并于2024年1月5日正式取得CNAS实验室认可证书&#xff08;注册号CNAS L19826&#xff09;&#xff0c;标志着怿星科技的实验…...

GORM 介绍及快速入门

GORM 介绍及快速入门 前言 GORM 是一个用 GoLang 语言编写的 ORM&#xff08;对象关系映射&#xff09;库。它被设计为开发者友好的方式来进行数据库操作。GORM 提供了一种高级的 API 来处理数据库的 CRUD&#xff08;创建、读取、更新、删除&#xff09;操作&#xff0c;它支…...

Scrcpy:掌握你的Android设备

Scrcpy&#xff1a;掌握你的Android设备 本文将介绍Scrcpy工具&#xff0c;它是一种强大的安卓设备控制工具&#xff0c;可以实现屏幕镜像、操作控制等功能。我们将探讨Scrcpy的基本原理和工作方式&#xff0c;并介绍如何使用Scrcpy连接和控制安卓设备。此外&#xff0c;我们还…...

[9, 8, 7, 6][1,2] = ?

当我们运行这段代码时,控制台中会记录什么值? const arr [9, 8, 7, 6]; const res arr[1, 2]; console.log(res);当我们运行这段代码时,res 的值将是 7。并且控制台中会打印出 7。 让我们来详细分析一下。 第一步:[1, 2] 会被转换成 [2]。 为什么? 后续的元素 [1, 2] …...

docker部署Jira+配置MySQL8数据库

写在前面&#xff1a;如果你通过docker安装Jira且启动过&#xff0c;然后你现在又想使用mysql数据库&#xff0c;需要注意 你除了停掉原有容器&#xff0c;还需要删除&#xff1a;/var/lib/docker/volumes/jiraVolume/_data下的文件&#xff0c;否则启动后会无法正常使用。注意…...

YOLOv5全网独家首发:DCNv4更快收敛、更高速度、更高性能,效果秒杀DCNv3、DCNv2等 ,助力检测实现暴力涨点

💡💡💡本文独家改进:DCNv4更快收敛、更高速度、更高性能,完美和YOLOv5结合,助力涨点 DCNv4优势:(1) 去除空间聚合中的softmax归一化,以增强其动态性和表达能力;(2) 优化存储器访问以最小化冗余操作以加速。这些改进显著加快了收敛速度,并大幅提高了处理速度,DCN…...

HTML中常用标签--详解

目录 1.b/strong标签 2.i/em 标签 3.u标签 4.del删除线 5.br换行 6.p标签 * 7.pre 预处理标签 8.span标签** 9.div标签*** 10.sub标签 11.sup标签 12.hr标签 13.hn标签 14.HTML5中语义标签 特殊字符 15.多媒体标签 img*** a 标签*** 第一种用法&#xff1a;…...

Vue实现字符串首字母大写、翻转字符串、获取用户选定的文本

目录 Vue2实现字符串首字母大写Vue3实现字符串首字母大写Vue2实现翻转字符串Vue3实现翻转字符串Vue2获取用户选定的文本Vue3获取用户选定的文本 Vue2实现字符串首字母大写 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"…...

基于springboot+vue的旅游网站系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目背景…...