Rust之泛型、特性和生命期(四):验证有生存期的引用

news2024/11/28 4:49:33

开发环境

  • Windows 10
  • Rust 1.71.0

 

  • VS Code 1.80.1

项目工程

这里继续沿用上次工程rust-demo

验证具有生存期的引用

生存期是我们已经在使用的另一种泛型。生存期不是确保一个类型具有我们想要的行为,而是确保引用在我们需要时有效。

我们在第4章“引用和借用”一节中没有讨论的一个细节是,Rust中的每个引用都有一个生命周期,这是该引用有效的范围。大多数时候,生存期是隐式的,是推断出来的,就像大多数时候,类型是推断出来的一样。只有在可能存在多种类型时,我们才必须对类型进行注释。类似地,当引用的生存期可以以几种不同的方式关联时,我们必须注释生存期。Rust要求我们使用通用的生存期参数来注释这些关系,以确保运行时使用的实际引用绝对有效。

注释生存期甚至不是大多数其他编程语言都有的概念,所以这会让人感到陌生。虽然我们不会在这一章完整地讨论生存期,但是我们会讨论你可能遇到的生存期语法的常见方式,这样你就可以熟悉这个概念了。

用生存期防止悬空引用

生存期的主要目的是防止悬空引用,悬空引用会导致程序引用它想要引用的数据之外的数据。考虑示例16中的程序,它有一个外部作用域和一个内部作用域。

fn main() {
    let r;

    {
        let x = 5;
        r = &x;
    }

    println!("r: {}", r);
}

示例16:试图使用其值超出范围的引用

注意:示例16、示例17和示例23中的例子声明了变量,但没有给它们一个初始值,所以变量名存在于外部作用域中。乍一看,这似乎与Rust没有空值相冲突。然而,如果我们试图在给变量赋值之前使用它,我们会得到一个编译时错误,这表明Rust确实不允许空值。 

外部作用域声明了一个名为r的没有初始值的变量,内部作用域声明了一个名为x的初始值为5的变量。在内部范围内,我们试图将r的值设置为对x的引用。然后内部范围结束,我们试图打印r中的值。这段代码不会编译,因为r引用的值在我们试图使用它之前已经超出了范围。以下是错误消息: 

变量x没有“活得足够久”原因是当内部范围在第1099行结束时,x将超出范围。但是r对外作用域仍然有效;因为它的范围更大,我们说它“寿命更长”如果Rust允许这段代码工作,r将引用x超出范围时释放的内存,我们试图用r做的任何事情都不会正确工作。那么Rust是如何确定这段代码无效的呢?它使用借用检查器。 

借用检查器

Rust编译器有一个借用检查器,它比较范围以确定是否所有借用都是有效的。示例17显示了与示例16相同的代码,但是带有显示变量生命周期的注释。

fn main() {
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+

示例17:分别命名为‘a’‘b’rx的生命期的注释

这里,我们用“a”标注了r的生存期,用“b”标注了x的生存期。正如您所看到的,内部的“b”块比外部的“a”生存期块小得多。在编译时,Rust比较了两个生存期的大小,发现r的生存期为“a ”,但它引用的内存的生存期为“b”。程序被拒绝,因为“b比”a短:引用的主题没有引用的生存期长。 

示例18修正了代码,所以它没有悬空引用,编译时没有任何错误。 

fn main() {
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

示例18:有效的引用,因为数据比引用有更长的生命周期

这里,x的生存期为“b ”,在这种情况下,它大于“a ”,这意味着r可以引用x,因为Rust知道,当x有效时,r中的引用将始终有效。 

既然您已经知道了引用的生命期在哪里,以及Rust如何分析生命期以确保引用总是有效的,那么让我们在函数的上下文中探索参数和返回值的一般生命期。 

函数中的泛型生存期

 我们将编写一个函数,返回两个字符串片段中较长的一个。该函数将接受两个字符串切片,并返回一个字符串切片。在我们实现了longest的函数之后,示例19中的代码应该打印出The longest string is abcd

文件名:src/main.rs

fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

 示例19:main函数,它调用最长的函数来查找两个字符串片段中较长的一个

注意,我们希望函数接受字符串片,字符串片是引用,而不是字符串,因为我们不希望longest函数接受其参数的所有权。关于为什么我们在示例19中使用的参数是我们想要的参数的更多讨论,请参考第4章中的“字符串片段作为参数”一节。 

如果我们试图实现示例20所示的最长的函数,它不会编译。

文件名:src/main.rs 

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

 示例20:最长函数的一个实现,返回两个字符串片段中较长的一个,但还没有编译

 帮助文本显示,返回类型需要一个通用的生存期参数,因为Rust无法判断返回的引用是指x还是y。实际上,我们也不知道,因为该函数体中的if块返回对x的引用,else块返回对y的引用!

当我们定义这个函数时,我们不知道将传递给这个函数的具体值,所以我们不知道是if情况还是else情况会执行。我们也不知道将要传入的引用的具体生存期,所以我们不能像在示例17和示例18中那样查看作用域来确定我们返回的引用是否总是有效。借用检查器也不能确定这一点,因为它不知道xy的生命周期如何与返回值的生命周期相关。要修复此错误,我们将添加定义引用之间关系的通用生存期参数,以便借项检查器可以执行其分析。 

生存期注释语法

 生存期注释不会改变任何引用的生存期。相反,它们描述了多个引用的生存期之间的关系,而不会影响生存期。正如当签名指定泛型类型参数时,函数可以接受任何类型一样,通过指定泛型生存期参数,函数可以接受任何生存期的引用。

生存期注释有一个稍微不同寻常的语法:生存期参数的名称必须以撇号(')开头,并且通常都是小写的,非常短,就像泛型类型一样。大多数人在第一个生命周期注释中使用名称'a。我们将生存期参数注释放在引用的&之后,使用空格将注释与引用的类型分开。

下面是一些例子:对没有寿命参数的i32的引用,对具有名为'a的寿命参数的i32的引用,以及对也具有寿命'ai32的可变引用。

&i32        // a reference
&'a i32     // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime

 一个生存期注释本身没有太多意义,因为注释是为了告诉Rust多个引用的通用生存期参数是如何相互关联的。让我们看看在longest函数的上下文中,生存期注释是如何相互关联的。

函数签名中的生存期注释

 为了在函数签名中使用生存期注释,我们需要在函数名和参数列表之间的尖括号内声明泛型生存期参数,就像我们对泛型类型参数所做的那样。

我们希望签名表达以下约束:只要两个参数都有效,返回的引用就有效。这是参数的生存期和返回值之间的关系。我们将把生存期命名为'a,然后把它添加到每个引用中,如示例21所示。

文件名:src/main.rs 

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

示例21:longest的函数定义,指定签名中的所有引用必须有相同的生命周期'a

当我们使用示例19中的main函数时,这段代码应该可以编译并产生我们想要的结果。 

函数签名现在告诉Rust,对于某个生存期'a,函数采用两个参数,这两个参数都是至少与生存期'a一样长的字符串片段。函数签名还告诉Rust,从函数返回的字符串片段将至少与生存期'a一样长。实际上,这意味着由longest函数返回的引用的生存期与由函数参数引用的值的较小生存期相同。我们希望Rust在分析这段代码时使用这些关系。 

请记住,当我们在这个函数签名中指定生存期参数时,我们不会改变任何传入或返回的值的生存期。相反,我们指定借用检查器应该拒绝任何不符合这些约束的值。请注意,longest函数不需要确切知道xy将存在多长时间,只需要用某个范围来代替满足该签名的'a

在函数中标注生存期时,标注放在函数签名中,而不是函数体中。生存期注释成为函数契约的一部分,很像签名中的类型。让函数签名包含生命周期契约意味着Rust编译器所做的分析可以更简单。如果函数的注释方式或调用方式有问题,编译器错误可以更准确地指向我们的代码部分和约束。相反,如果Rust编译器对我们想要的生存期关系做了更多的推断,编译器可能只能指出我们代码的一个用法,而不是问题的原因。

当我们将具体的引用传递给longest时,替换'a的具体生存期是x的范围与y的范围重叠的部分,换句话说,泛型生存期'a将获得等于xy的较小生存期的具体生存期,因为我们已经用相同的生存期参数'a对返回的引用进行了注释,所以返回的引用对于xy的较小生存期的长度也是有效的。 

让我们看看生存期注释如何通过传入具有不同具体生存期的引用来限制longest函数。示例22是一个简单的例子。 

文件名:src/main.rs

fn main() {
    let string1 = String::from("long string is long");

    {
        let string2 = String::from("xyz");
        let result = longest(string1.as_str(), string2.as_str());
        println!("The longest string is {}", result);
    }
}

示例22:使用引用具有不同具体生命周期的String值的longest函数

在这个例子中,string1在外部作用域结束之前有效,string2在内部作用域结束之前有效,而result引用在内部作用域结束之前有效的内容。运行这段代码,您将看到借入检查器批准了;它将编译并打印The longest string is long string is long。 

接下来,让我们尝试一个例子,它显示了result中引用的生存期必须是两个参数中较小的生存期。我们将把result变量的声明移到内部作用域之外,但是将值分配给string2作用域内的result变量。那我们就转移println!在内部范围结束后,在内部范围外使用result。示例23中的代码无法编译。

文件名:src/main.rs 

fn main() {
    let string1 = String::from("long string is long");
    let result;
    {
        let string2 = String::from("xyz");
        result = longest(string1.as_str(), string2.as_str());
    }
    println!("The longest string is {}", result);
}

 示例23:试图在string2超出范围后使用result

该错误显示,要使resultprintln!有效!语句中,string2需要在外部范围结束之前一直有效。Rust知道这一点,因为我们使用相同的生存期参数'a来注释函数参数和返回值的生存期。 

 作为人类,我们可以查看这段代码,发现string1string2长,因此result将包含对string1的引用。因为string1还没有超出范围,所以对string1的引用对于println!仍然有效!声明。但是,编译器在这种情况下看不到引用有效。我们已经告诉Rust,由longest函数返回的引用的生存期与传入的引用的较小生存期相同。因此,借用检查器拒绝示例23中的代码,因为它可能有一个无效的引用。

 尝试设计更多的实验,改变传递给longest函数的引用的值和生存期,以及如何使用返回的引用。在编译之前,假设您的实验是否会通过借用检查器;然后检查你是否正确!

从生存期的角度思考

 您需要指定生存期参数的方式取决于您的函数正在做什么。例如,如果我们将longest函数的实现改为总是返回第一个参数,而不是最长的字符串片段,我们就不需要在y参数上指定生存期。下面的代码将会编译:

文件名:src/main.rs

fn longest<'a>(x: &'a str, y: &str) -> &'a str {
    x
}

我们已经为参数x和返回类型指定了生存期参数'a,但没有为参数y指定,因为y的生存期与x的生存期或返回值没有任何关系。

从函数返回引用时,返回类型的生存期参数需要与其中一个参数的生存期参数匹配。如果返回的引用不引用参数之一,则它必须引用在此函数中创建的值。但是,这将是一个悬空引用,因为在函数结束时,该值将超出范围。考虑一下这个不会编译的longest函数的尝试实现:

文件名:src/main.rs

fn longest<'a>(x: &str, y: &str) -> &'a str {
    let result = String::from("really long string");
    result.as_str()
}

 这里,即使我们已经为返回类型指定了生存期参数'a这个实现也将无法编译,因为返回值的生存期与参数的生存期完全无关。下面是我们得到的错误消息:

 问题是result超出了范围,在longest函数结束时被清除。我们还试图返回一个对函数result的引用。我们无法指定会改变悬空引用的生存期参数,Rust也不允许我们创建悬空引用。在这种情况下,最好的解决方法是返回一个自己的数据类型而不是引用,这样调用函数就负责清理这个值。

最终,生存期语法是关于连接各种参数和函数返回值的生存期。一旦它们被连接起来,Rust就有足够的信息来允许内存安全的操作,并禁止会创建悬空指针或违反内存安全的操作。 

结构定义中的生存期注释

到目前为止,我们定义的结构都持有自己的类型。我们可以定义结构来保存引用,但在这种情况下,我们需要在结构定义中的每个引用上添加一个生存期注释。示例24有一个名为ImportantExcerpt的结构,它保存一个字符串切片。 

文件名:src/main.rs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

fn main() {
    let novel = String::from("Call me Ishmael. Some years ago...");
    let first_sentence = novel.split('.').next().expect("Could not find a '.'");
    let i = ImportantExcerpt {
        part: first_sentence,
    };
}

示例24:一个包含引用的结构,需要一个生存期注释

这个结构有一个保存字符串片段的字段part,这是一个引用。与泛型数据类型一样,我们在结构名称后面的尖括号中声明泛型生存期参数的名称,这样我们就可以在结构定义的主体中使用生存期参数。此注释意味着ImportantExcerpt的实例不能比它在part字段中保存的引用存活得更久。 

这里的main函数创建了ImportantExcerpt结构的一个实例,该实例保存了对变量novel所拥有的字符串的第一句的引用。在创建ImportantExcerpt实例之前,novel中的数据已经存在。此外,在ImportantExcerpt超出范围之前,novel不会超出范围,因此ImportantExcerpt实例中的引用是有效的。 

终生省略

您已经了解到每个引用都有一个生存期,并且您需要为使用引用的函数或结构指定生存期参数。然而,在第四章中,我们在示例4-9中有一个函数,在示例25中再次显示,它编译时没有生命期注释。

文件名:src/lib.rs 

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

示例25:我们在示例4-9中定义的一个函数,编译时没有生命期注释,即使参数和返回类型是引用

这个函数编译时没有生存期注释的原因是历史原因:在Rust的早期版本(1.0之前)中,这段代码不会编译,因为每个引用都需要一个显式的生存期。那时,函数签名应该是这样写的: 

fn first_word<'a>(s: &'a str) -> &'a str {

 在编写了大量Rust代码后,Rust团队发现Rust程序员在特定的情况下一遍又一遍地输入相同的生存期注释。这些情况是可以预测的,并且遵循一些确定的模式。开发人员将这些模式编程到编译器的代码中,因此借用检查器可以推断这些情况下的生存期,而不需要显式的注释。

这段Rust历史是相关的,因为更确定的模式可能会出现并被添加到编译器中。将来,可能需要更少的生存期注释。 

编程到Rust的引用分析中的模式被称为生存期省略规则。这些不是程序员要遵守的规则;它们是编译器将考虑的一组特殊情况,如果您的代码符合这些情况,您就不需要显式地编写生存期。

省略规则没有提供完整的推论。如果Rust确定性地应用了这些规则,但是对于引用的生命周期仍然不明确,编译器不会猜测其余引用的生命周期应该是多少。编译器不会猜测,而是会给出一个错误,您可以通过添加生存期注释来解决这个错误。 

函数或方法参数的生存期称为输入生存期,返回值的生存期称为输出生存期。 

当没有显式注释时,编译器使用三个规则来计算引用的生存期。第一个规则适用于输入生存期,第二个和第三个规则适用于输出生存期。如果编译器到达了三个规则的末尾,但仍然有一些引用无法确定它们的生存期,编译器将会出错停止。这些规则适用于fn定义以及impl块。 

第一条规则是编译器给每个引用的参数分配一个生存期参数。换句话说,有一个参数的函数得到一个生存期参数:fn foo < ' a >(x:& ' a i32);有两个参数的函数得到两个独立的生存期参数:fn foo<'a,' b>(x: &'a i32,y:& ' b i32);诸如此类。 

第二个规则是,如果恰好有一个输入寿命参数,则该寿命被分配给所有输出寿命参数:fn foo<'a>(x: &'a i32) -> &'a i32

第三个规则是,如果有多个输入生存期参数,但其中一个是&self&mut self,因为这是一个方法,那么self的生存期被分配给所有输出生存期参数。第三条规则使得方法更易于读写,因为需要的符号更少。 

假设我们是编译器。我们将应用这些规则来计算示例25中first_word函数签名中引用的生存期。签名开始时没有任何与引用相关联的生存期:

fn first_word(s: &str) -> &str {

 然后编译器应用第一条规则,规定每个参数都有自己的生存期。我们像往常一样称之为‘a,所以现在的签名是这样的:

fn first_word<'a>(s: &'a str) -> &str {

 第二个规则适用,因为只有一个输入生命周期。第二个规则指定将一个输入参数的生存期分配给输出生存期,因此签名现在是这样的:

fn first_word<'a>(s: &'a str) -> &'a str {

现在,这个函数签名中的所有引用都有生存期,编译器可以继续分析,而不需要程序员在这个函数签名中注释生存期。

让我们看另一个例子,这一次使用longest函数,当我们在示例20中开始使用它时,它没有生存期参数:

fn longest(x: &str, y: &str) -> &str {

 让我们应用第一条规则:每个参数都有自己的生存期。这次我们有两个参数,而不是一个,所以我们有两个生存期:

fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {

 您可以看到第二条规则不适用,因为有多个输入生存期。第三条规则也不适用,因为longest是函数而不是方法,所以没有一个参数是self。在研究了所有三个规则之后,我们仍然没有弄清楚返回类型的生存期是多少。这就是为什么我们在试图编译示例20中的代码时出现错误:编译器通过了生存期省略规则,但仍然无法计算出签名中引用的所有生存期。

因为第三条规则实际上只适用于方法签名,我们将在接下来的上下文中查看生存期,以了解为什么第三条规则意味着我们不必经常在方法签名中注释生存期。

方法定义中的生存期注释

当我们在具有生存期的结构上实现方法时,我们使用与示例11所示的泛型类型参数相同的语法。我们在哪里声明和使用生存期参数取决于它们是与结构字段相关还是与方法参数和返回值相关。

结构字段的生存期名称总是需要在impl关键字之后声明,然后在结构名称之后使用,因为这些生存期是结构类型的一部分。 

impl块内部的方法签名中,引用可能与结构的字段中的引用的生存期相关联,或者它们可能是独立的。此外,生存期省略规则通常使得方法签名中不需要生存期注释。让我们看一些例子,使用我们在示例24中定义的名为ImportantExcerpt的结构。

首先,我们将使用一个名为level的方法,其唯一的参数是对self的引用,其返回值是i32,而不是对任何东西的引用:

impl<'a> ImportantExcerpt<'a> {
    fn level(&self) -> i32 {
        3
    }
}

 impl后的生存期参数声明及其在类型名后的使用是必需的,但由于第一个省略规则,我们不需要注释self引用的生存期。

下面是一个应用第三生命周期省略规则的示例:

impl<'a> ImportantExcerpt<'a> {
    fn announce_and_return_part(&self, announcement: &str) -> &str {
        println!("Attention please: {}", announcement);
        self.part
    }
}

有两个输入生命周期,所以Rust应用第一个生命周期省略规则,给&selfannouncement它们自己的生命周期。然后,因为参数之一是&self,返回类型获得&self的生存期,并且所有生存期都已被考虑。

静态生存期

我们需要讨论的一个特殊的生命周期是'static,这意味着受影响的引用可以存在于整个程序期间。所有字符串文字都有'static生存期,我们可以对其进行如下注释:

let s: &'static str = "I have a static lifetime.";

该字符串的文本直接存储在程序的二进制文件中,该文件总是可用的。因此,所有字符串文字的生存期都是'static

您可能会在错误消息中看到使用“'static生存期的建议。但是在指定'static作为引用的生存期之前,请考虑您拥有的引用是否实际上存在于您的程序的整个生存期中,以及您是否希望它存在。大多数情况下,提示'static生存期的错误信息是由于试图创建悬空引用或可用生存期不匹配而导致的。在这种情况下,解决方案是修复这些问题,而不是指定'static生存期。

泛型类型参数、特征界限和生存期

让我们简单地看一下在一个函数中指定泛型类型参数、特征界限和生存期的语法! 

use std::fmt::Display;

fn longest_with_an_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: Display,
{
    println!("Announcement! {}", ann);
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

这是示例21中longest的函数,返回两个字符串片段中较长的一个。但是现在它有了一个名为ann的泛型类型T的额外参数,该参数可以由实现where子句指定的Display特征的任何类型填充。这个额外的参数将使用{}打印,这就是为什么Display特征界限是必要的。因为生存期是泛型的一种类型,所以生存期参数'a和泛型类型参数T的声明放在函数名后面的尖括号内的同一列表中。

总结

这一章我们讲了很多!现在,您已经了解了泛型类型参数、特征和特征界限,以及泛型生存期参数,您已经准备好编写在许多不同情况下都可以工作的代码了。泛型类型参数允许您将代码应用于不同的类型。特征和特征界限确保了即使类型是泛型的,它们也会有代码需要的行为。您了解了如何使用生存期注释来确保这个灵活的代码不会有任何悬空引用。所有这些分析都发生在编译时,不会影响运行时性能! 

信不信由你,关于我们在本章讨论的主题,还有很多东西要学:第17章讨论了trait对象,这是使用trait的另一种方式。还有一些更复杂的场景涉及生存期注释,您只需要在非常高级的场景中使用它们;对于这些,你应该阅读生锈的参考。但是接下来,您将学习如何在Rust中编写测试,这样您就可以确保您的代码按照它应该的方式运行。

本章重点

  • 验证生存期引用
  • 防止悬空引用
  • 借用检查器的概念
  • 函数中泛型生存期
  • 掌握生存期的注释语法
  • 函数签名生存期注释
  • 结构定义生存期注释
  • 方法定义中的生存期注释
  • 静态生存期
  • 泛型类型参数,trait界限及生存期

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/777906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java将数据集合转换为PDF

这里写自定义目录标题 将数据集合转换为pdf引入包工具类测试代码导出效果 将数据集合转换为pdf 依赖itext7包将数据集合转换导出为PDF文件 引入包 <properties><itext.version>7.1.11</itext.version> </properties><dependency><groupId&…

Spring、Springboot、SpringMVC之间的关系

他们之间没有明确的区分。一个项目&#xff0c;可以说是SpringMVC,又是Sprigboot,又是Spring项目。 首先简单看一下他们的定义&#xff1a; Spring是包含众多容器的IOC(控制反转)容器&#xff0c;是一个分层的轻量级框架&#xff0c;为了简化Java程序的开发。Springboot在Spr…

实例022 非矩形窗体

实例说明 大部分Windows窗体都是一个矩形区域&#xff0c;读者是否已经厌倦了这种中规中矩的矩形窗体&#xff1f;本例中的窗体是一个打破传统矩形的异型窗体&#xff0c;运行该例会看到一个非常可爱的窗体&#xff0c;单击【X】按钮就会使窗口关闭。实例效果如图1.22所示。 …

基于MATLAB的无人机遥感数据预处理与农林植被性状估算实践

遥感技术作为一种空间大数据手段&#xff0c;能够从多时、多维、多地等角度&#xff0c;获取大量的农情数据。数据具有面状、实时、非接触、无伤检测等显著优势&#xff0c;是智慧农业必须采用的重要技术之一。本内容主要针对农业、林业、生态、遥感背景的对无人机遥感有兴趣的…

低代码平台协同OA升级,促进金融企业信息化建设

编者按&#xff1a;数字化办公是信息化时代每个企业不可避免的&#xff0c;OA系统是数字化办公的关键环节。如何与时俱进&#xff0c;保持企业的活力&#xff0c;增强企业综合竞争力&#xff1f;本文分析了企业OA系统为什么需要升级&#xff0c;并进一步指出如何实现升级。 关…

git stash 内容丢失找回【亲测好用】

直接将下列代码复制到 终端 会出现所有列表 也包括你删除/丢失的stash git log --graph --oneline --decorate $( git fsck --no-reflog | awk /dangling commit/ {print $3} ) 前面的黄色就是他的编号 例如我想回复 自己编辑修改项目 将编号复制重链即可 git stash apply …

MySQL使用

目录 1 MySQL的登录 1.1 服务的启动和终止 1.2 自带客户端的登录与退出 2 MySQL演示使用 2.1 MySQL的使用演示 2.2 MySQL的编码设置 1 MySQL的登录 1.1 服务的启动和终止 MySQL安装完毕以后&#xff0c;需要启动服务器进程&#xff0c;不然客户端无法连接数据库。 在前面…

用html+javascript打造公文一键排版系统7:落款排版

一、公文落款的格式 公文落款包括单位署名和成文日期两个部分&#xff0c;其中成文日期中的数字 用阿拉伯数字将年、月、日标全&#xff0c;年份应标全称&#xff0c;月、日不编虚位&#xff08;即 1 不编为 01&#xff09;。 在实际应用工作中分为三种情况&#xff1a; &am…

(36)转速传感器

文章目录 前言 36.1 RPM库如何工作(TYPE AUXPIN) 36.2 霍尔效应传感器 36.3 电调遥测 - 电机平均转速 36.4 电气换向传感器 36.5 光学传感器 36.6 谐波陷波中心频率 前言 ArduPilot 支持使用众多类型的转速传感器。它们通常用于传统的直升机&#xff0c;测量主旋翼速度…

java Spring Boot上线运维 启动jar时控制台调整零时变量

前面的文章 java 打包Spring Boot项目&#xff0c;并运行在windows系统中和将Spring Boot项目打包部署到阿里云linux服务器讲述了Spring Boot项目打包部署的过程 但是 这里 我们可能会遇到一种情况 此时 我们服务器 java项目占用了 80端口 但我们需要放上去一个更重要的东西&am…

M1安装服务一条龙Mysql (解决PID的不存在的方法)

遇到的各种奇葩离谱问题 dev.mysql.com/downloads/mysql/&#xff0c;登入下载就行&#xff0c;下载这块最简单&#xff0c;就不详细说明了 首先注意一个点M1可以下载ARM&#xff0c;也可以X86&#xff0c;目前暂时不用考虑效率能用就行&#xff0c;5.7也可以用哈 然后下载完&…

【公考-判断推理】定义判断04

【国考判断推理】定义判断04 1.读得准1.1找主客体1.2 句式1.3解释说明 2.读得快2.1 优先看概念2.2多定义先看问题 3.小技巧3.1拆词3.2 同构 解题思维 在这里插入代码片 1.读得准 1.1找主客体 看主体 1.2 句式 出现多个的时候要就注意多个主体。 定义题&#xff0c;出现或…

Mac系统下配置环境变量:Javajdk、maven、tomcat 环境变量配置及对应配置文件

文章目录 前言一、配置环境变量路径问题1、macOS 下环境变量的配置文件2、解决环境变量在 zsh shell 无效的问题3、查看 macOS 全部环境变量4、设置系统环境变量 二、JDK下载&配置环境变量1、下载2、配置环境变量3、测试 三、maven下载&配置环境变量1、下载2、环境变量…

day28-Github Profiles(获取Github用户概要)

50 天学习 50 个项目 - HTMLCSS and JavaScript day28-Github Profiles&#xff08;获取Github用户概要&#xff09; 效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewp…

第三章:C语言的循环控制结构

文章目录 1.While2.do...while...3.for循环 1.While 标准格式&#xff1a;while() 括号后面如果直接写1则表示是死循环&#xff1a;while(1) 括号后面也可以是执行条件&#xff0c;比如下面的代码是只有当i<10时才会进入循环执行&#xff0c;执行完毕后自动退出 运行结果 …

Qgis二次开发-QgsMapTool地图交互工具详解

1.简介 QgsMapTool地图工具是用于操作地图画布的用户交互式工具。例如&#xff0c;地图平移和缩放功能被实现为地图工具。 QgsMapTool是抽象基类&#xff0c;以下是类的继承关系&#xff1a; 2.常用接口 virtual void canvasDoubleClickEvent (QgsMapMouseEvent *e)重写鼠标…

【Matlab】基于遗传算法优化 BP 神经网络的数据回归预测(Excel可直接替换数据)

【Matlab】基于遗传算法优化 BP 神经网络的数据回归预测&#xff08;Excel可直接替换数据&#xff09; 1.模型原理2.文件结构3.Excel数据4.分块代码4.1 arithXover.m4.2 delta.m4.3 ga.m4.4 gabpEval.m4.5 initializega.m4.6 maxGenTerm.m4.7 nonUnifMutation.m4.8 normGeomSel…

Hadoop简介以及集群搭建详细过程

Hadoop简介以及集群搭建详细过程 hadoop集群简介hadoop部署模式Hadoop集群安装1.集群角色规划2.服务器基础环境准备3.上传安装包hadoop安装包目录结构5.编辑hadoop配置文件6.分发安装包7.配置hadoop环境变量8.NameNode format(格式化操作) hadoop集群启动关闭-手动逐个进程启停…

数字孪生in电力终端:高效虚拟环境实现测试“左移”

电力资源是现代社会发展必不可少的清洁型可再生资源&#xff0c;在清洁性、高效性、便捷性和适用性等方面优于传统化石能源&#xff0c;是如期实现2030年前碳达峰、2060年前碳中和的“双碳”目标的关键。2006至2019年前&#xff0c;电力行业累计为全社会减少了约159.4亿吨的碳排…

VSCode_常用插件_最新推荐

本文介绍前端开发领域常用的一些VSCode插件&#xff0c;插件是VSCode最重要的组成部分之一&#xff0c;本文列出了个人觉得是有用或有趣的一些插件。 一、代码管理相关插件 1、GitLens — Git supercharged 该插件增强了 VS Code 中的 Git&#xff0c;通过丰富的可视化和强…