Beginng_Rust(译):定义函数(第九章)(完+1)

在本章中,您将学习:

•如何定义自己的过程(更好地称为“函数”)以及如何调用它们

•何时以及如何使用相同名称的多个功能

•如何通过值或引用将参数传递给函数

•如何从函数返回简单值和复合值

•如何从函数中提前退出

•如何操纵对象的引用

定义和调用函数

如果您多次编写相同的代码,则可以将该代码封装在一个块中,然后为该块命名。 通过这种方式,您可以定义“函数”。 然后,您可以通过按名称调用该函数来执行该代码:

fn line() {
println!("----------");
}
line();
line();
line();
This will print:
----------
----------
----------

要定义一个函数,可以编写“fn”关键字,然后是要与该函数关联的名称,后跟一对括号,后跟一个块。

该块被命名为函数的“主体”,并且在主体之前的所有内容都被命名为函数的“签名”。

这个语法看起来应该很熟悉,因为到目前为止我们编写的所有程序都只是名为“main”的函数的定义。

虽然“main”函数是一个特殊函数,因为它是由程序启动机器代码调用的,但我们的“行”函数仅在我们的代码调用它时执行。

实际上,我们的小程序的第二部分调用了三次线函数。要调用(或“调用”)它,就可以写出它的名字,然后是一对括号。

请注意,我们在main函数中定义了line函数。实际上,与C语言不同,在Rust中你可以在其他函数体内定义函数。您还可以调用main函数外部定义的函数。这是一个完整的Rust程序(不要插入主函数):

fn f1() { print!("1"); }
fn main() {
f1();
fn f2() { print!("2"); }
f2(); f1(); f2();
}
This will print: "1212".

使用后定义的功能

此代码是非法的:

a;
let a = 3;

因为它在定义之前使用了一个变量。
以下是有效的:

f();
fn f() {}

您可以在定义函数之前调用函数,只要它在当前作用域或封闭作用域中定义即可。

隐藏其他功能的功能

我们已经看到在定义变量之后,您可以定义另一个具有相同名称的变量,第二个变量将影响第一个变量。 使用函数,您无法在同一范围内执行此操作:

fn f() {}
fn f() {}

这将生成名称f被定义多次的编译错误。

但是,您可以定义多个具有相同名称的函数,只要它们位于不同的并行块中:

{
fn f() { print!("a"); }
f(); f();
} {
fn f() { print!("b"); }
f();
}

这将打印:“aab”。

每个函数仅在定义它的块中有效,因此您无法从该块的外部调用它:

{
fn f() { }
}
f();

这将在最后一行生成编译错误:“在此范围内找不到函数f”。

最后,函数可以遮蔽外部块中定义的另一个函数。 这是一个完整的程序:

fn f() { print!("1"); }
fn main() {
f(); // Prints 2
{
f(); // Prints 3
fn f() { print!("3"); }
}
f(); // Prints 2
fn f() { print!("2"); }
}

这将打印232。

实际上,在main函数外部,定义了一个打印1的函数,但在main函数内定义了另一个函数,具有相同的名称,并且打印2,因此main函数中的第一个语句调用在 主要的一个,即使它被定义为六行以下。 在嵌套块中,定义并调用另一个具有相同名称的函数,然后打印3.然后该块结束,因此函数print 2返回到活动块。 永远不会调用打印1的外部函数,因此编译器会在警告中报告该函数。

将参数传递给函数

每次调用时始终打印相同文本的函数不是很有用。 打印传递给它的任何两个数值之和的函数更有趣:

fn print_sum(addend1: f64, addend2: f64) {
println!("{} + {} = {}", addend1, addend2,
addend1 + addend2);
}
print_sum(3., 5.);
print_sum(3.2, 5.1);

这将打印:

3 + 5 = 8
3.2 + 5.1 = 8.3

现在你可以理解括号的用法了! 在函数定义中,它们包含参数定义列表; 在函数调用中,它们包含其值作为参数传递的表达式。

函数参数的定义与变量的定义非常相似。
因此,上述程序将被解释为如下所示:

{
let addend1: f64 = 3.; let addend2: f64 = 5.;
println!("{} + {} = {}", addend1, addend2,
addend1 + addend2);
} {
let addend1: f64 = 3.2; let addend2: f64 = 5.1;
println!("{} + {} = {}", addend1, addend2,
addend1 + addend2);
}

变量定义与函数参数定义之间的主要区别在于,在函数参数定义中,类型规范是必需的,也就是说,您不能仅依赖于类型推断。

无论如何,编译器都使用类型推断来检查作为参数接收的值是否实际上是为该参数声明的类型。 的确,这段代码

fn f(a: i16) {}
f(3.);
f(3u16);
f(3i16);
f(3);

在第一次调用f时生成错误,因为浮点数被传递给整数参数; 并且它在第二次调用时也会生成错误,因为u16类型的值被传递给i16类型的参数。

相反,最后两次调用是允许的。 实际上,第三次调用传递的值与函数所期望的类型完全相同; 而第四次调用传递一个无约束整数类型的参数,它被调用本身约束为“i16”类型。

按值传递参数

另请注意,参数不仅仅是传递对象的新名称,也就是说,它们不是别名; 相反,它们是这些物体的副本。 调用函数时会创建此类副本,并在函数结束并且控件返回调用者代码时销毁它们。 这个例子澄清了这个概念:

fn print_double(mut x: f64) {
x *= 2.;
print!("{}", x);
}
let x = 4.;
print_double(x);
print!(" {}", x);

这将打印:“8 4”。

在这里,显然,声明并初始化了一个名为“x”的变量,并将其传递给函数“print_double”,它保持其名称“x”;更改此变量的值,正确打印其新值,函数结束,返回调用者,并打印变量的值…就像在函数调用之前一样!

实际上,它不是传递给函数的变量,而是变量的值。它是一种所谓的传值传递机制,就像在C语言中一样。 “x”变量的值用于初始化一个新变量,在这里偶然也称为“x”,这是函数的参数。然后在函数体内更改并打印新变量,并由函数结束销毁。在调用函数中,我们的变量从未改变其值。

请注意,在“print_double”的签名中,在参数“x”之前,存在“mut”关键字。这是允许函数体内的第一个语句所必需的;然而,如前所述,这样的语句只改变函数参数的值,而不是函数外部定义的变量,实际上不需要mut规范。

从函数返回值

除了能够接收要处理的值之外,函数还可以将计算结果发送回调用者:

fn double(x: f64) -> f64 { x * 2. }
print!("{}", double(17.3));

这将打印“34.6”。

函数返回的值通常是其正文的值。
我们已经看到任何函数的主体都是一个块,并且任何块的值是它的最后一个表达式的值,如果有最后一个表达式,或者是一个空元组。

上面双函数体的内容是x * 2.这个表达式的值是函数返回的值。

函数返回的值的类型,而在C语言中,在函数名称之前写入,之后写入Rust,用箭头符号“>”分隔。 根据其签名,上面的双重函数返回f64值。 如果没有指定返回值类型,则暗示为空元组类型,即“()”:

fn f1(x: i32) {}
fn f2(x: i32) -> () {}

这些函数“f1”和“f2”是相等的,因为它们都返回一个空元组。

任何函数返回的值必须与函数签名指定的返回值类型相同,或者可以约束为该类型的无约束类型。
所以,这段代码是有效的:

fn f1() -> i32 { 4.5; "abc"; 73i32 }
fn f2() -> i32 { 4.5; "abc"; 73 }
fn f3() -> i32 { 4.5; "abc"; 73 + 100 }
While this code is not:
fn f1() -> i32 { 4.5; "abc"; false }
fn f2() -> i32 { 4.5; "abc"; () }
fn f3() -> i32 { 4.5; "abc"; {} }
fn f4() -> i32 { 4.5; "abc"; }

它产生四种不匹配的类型错误:对于f1函数,错误预期i32,发现bool; 对于f2,f3和f4函数,错误预期i32,found()。

提前退出

到目前为止,为了退出一个函数,我们不得不走到它的身体的尽头。 但是,如果你编写一个包含许多语句的函数,通常会在函数中间意识到你没有更多的计算要做,所以你想从函数中快速退出。

一种可能性是从循环中突破,另一种是如果要创建一个包含该函数其余语句的大“if”语句; 第三种可能性是设置一个布尔变量,指示不再需要进行处理,并在每次必须执行某些操作时使用if语句检查此类变量的值。

但通常情况下,最方便的方法是让编译器立即从函数中退出,返回函数签名所需类型的值:

fn f(x: f64) -> f64 {
if x <= 0. { return 0.; }
x + 3.
}
print!("{} {}", f(1.), f(-1.));

它将打印:“4 0”。

“return”语句计算其后面的表达式,结果值立即返回给调用者。

return关键字与C语言相同,不同之处在于它通常不用作最后一个语句,而只是用于早期退出。 然而,你可以写:

fn f(x: f64) -> f64 {
if x <= 0. { return 0.; }
return x + 3.;
}
print!("{} {}", f(1.), f(-1.));

这个程序相当于前面的程序,但它被认为是不好的风格。 此程序也是等效的,通常认为更好:

fn f(x: f64) -> f64 {
if x <= 0. { 0. }
else { x + 3. }
}
print!("{} {}", f(1.), f(-1.));

仅当返回语句允许您减少代码行数或缩进级别时,才认为return语句很方便。

同样对于“return”语句,返回值类型必须等于函数签名中声明的类型。

如果函数签名将空元组指定为返回值类型,则可以在“return”语句中省略此值。 因此,这是一个有效的程序:

fn f(x: i32) {
if x <= 0 { return; }
if x == 4 { return (); }
if x == 7 { return {}; }
print!("{}", x);
}
f(5);

任何函数调用都可以被认为是有效的声明:

fn f() -> i32 { 3 }
f();

在这种情况下,返回的值将被忽略并立即销毁。
相反,如果使用返回的值,就像在此有效代码中一样,

fn f() -> i32 { 3 }
let _a: i32 = f();

那么它必须是正确的类型。 作为一个反例,以下程序是非法的:

fn f() -> i32 { 3 }
let _a: u32 = f();

返回多个值

如果要从函数返回多个值,可以使用元组:

fn divide(dividend: i32, divisor: i32) -> (i32, i32) {
(dividend / divisor, dividend % divisor)
}
print!("{:?}", divide(50, 11));

这将打印“(4,6)”
或者您可以返回枚举,结构,元组结构,数组或向量:

enum E { E1, E2 }
struct S { a: i32, b: bool }
struct TS (f64, char);
fn f1() -> E { E::E2 }
fn f2() -> S { S { a: 49, b: true } }
fn f3() -> TS { TS (4.7, 'w') }
fn f4() -> [i16; 4] { [7, -2, 0, 19] }
fn f5() -> Vec<i64> { vec![12000] }
print!("{} ", match f1() { E::E1 => 1, _ => -1 });
print!("{} ", f2().a);
print!("{} ", f3().0);
print!("{} ", f4()[0]);
print!("{} ", f5()[0]);

这将打印:“ - 1 49 4.7 7 12000”。

我们来解释这五个数字。
在第一次打印中调用f1! 调用返回枚举E2,它试图与E1匹配,并且,因为它不匹配,所以采用了catch-all情况,因此打印-1。

f2的调用返回包含字段a和b的struct对象,并从中提取a字段。

f3的调用返回一个包含两个字段的元组结构,并从中提取第一个字段。

f4的调用返回一个包含四个项目的数组,并从中提取第一个项目。

f5的调用返回一个只包含一个项目的向量,并从中提取第一个也是唯一的项目。

如何更改调用者的变量

假设我们有一个包含10个数字的数组,其中一些是负数,我们只想将负数的值加倍。
我们可以这样做:

let mut arr = [5, -4, 9, 0, -7, -1, 3, 5, 3, 1];
for i in 0..10 {
if arr[i] < 0 { arr[i] *= 2; }
}
print!("{:?}", arr);

这将打印:“[5,-8,9,0,-14,-2,3,5,3,1]”。

现在让我们假设我们想要将这样的操作封装到一个函数中。

我们错写成:

fn double_negatives(mut a: [i32; 10]) {
for i in 0..10 {
if a[i] < 0 { a[i] *= 2; }
}
}
let mut arr = [5, -4, 9, 0, -7, -1, 3, 5, 3, 1];
double_negatives(arr);
print!("{:?}", arr);

这将打印:[5,-4,9,0,-7,-1,3,5,3,1]。

我们的阵列根本没有变化。

实际上,当按值传递此数组时,整个数组已被复制到由函数调用创建的另一个数组中。 最后一个数组已在函数内部更改,然后在函数结束时被销毁。

原始数组从未改变,编译器知道这一点,因为它报告警告变量不需要是可变的。 也就是说,假设arr永远不会更改,您也可以从其声明中删除mut子句。

要获得我们的数组已更改,您可以使该函数返回更改的数组:

fn double_negatives(mut a: [i32; 10]) -> [i32; 10] {
for i in 0..10 {
if a[i] < 0 { a[i] *= 2; }
} a
}
let mut arr = [5, -4, 9, 0, -7, -1, 3, 5, 3, 1];
arr = double_negatives(arr);
print!("{:?}", arr);

这将打印预期结果。 但是,此解决方案有一个缺点:所有数据都被复制两次。 首先,在函数调用时,所有数组都被复制到函数中,然后函数在数据的本地副本上工作,最后是所有本地
数据被复制回来代替原始数据。 这些副本具有计算成本,可以避免。

通过引用传递参数

要优化(长)数组到函数的传递,您可以只传递该函数的数组地址,让函数直接在原始数组上工作:

fn double_negatives(a: &mut [i32; 10]) {
for i in 0..10 {
if (*a)[i] < 0 { (*a)[i] *= 2; }
}
}
let mut arr = [5, -4, 9, 0, -7, -1, 3, 5, 3, 1];
double_negatives(&mut arr);
print!("{:?}", arr);

即使这个程序打印预期的结果,但没有复制数组。

您可以通过所谓的引用参数传递获得此结果。 语法与C语言的pass by pointer技术非常相似。

让我们详细看一下。

出现了新的符号“&”和“”。 它们在Rust中的含义与在C语言中的含义相同。 “&”符号表示“对象的(存储器)地址…”,“”符号表示“存在于(存储器)地址的对象…”。

double_negatives函数的参数现在是&mut [i32;10。 通过在类型规范之前放置一个&符号,指定它是之后指定类型的对象的地址(也称为指针或引用)。 因此,在这种情况下,a是“十个32位有符号整数的可变数组的地址”类型。

在函数体中,我们对处理地址本身并不感兴趣,而是由这样的地址引用的对象,因此我们使用符号来访问这样的对象。 通常,给定参考a, a表达式表示由这种参考引用的对象。

在函数的第二行中,通过使用* a表达式,访问位于作为参数接收的地址的对象两次。这样的对象是一个数组,因此您可以访问其i索引项。

  • a表达式周围的括号是必需的,因为方括号优先于星号运算符,因此* a [i]表达式将被视为*(a [i]),这意味着“取第i个项目对象a,然后,考虑这样的项目作为地址,取具有这样的地址的对象“。这不是我们想要做的,但是它会产生编译错误类型i32不能被解除引用,也就是说,“你不能得到其内存地址包含在i32类型值中的对象”。

使用这种参数传递,double_negatives函数只接收数组的地址,通过它可以读取和写入这种数组的项。

声明此功能后,我们可以使用它。必须声明数组并将其初始化为可变数,因为必须更改其内容。然后调用该函数而不期望返回值,但将参数作为参数传递给数组的地址。
请注意,在传递参数的地方也需要重复mut关键字,以明确表示该函数需要更改引用的对象。实际上,此功能可以简化为以下等效版本:

fn double_negatives(a: &mut [i32; 10]) {
for i in 0..10 {
if a[i] < 0 { a[i] *= 2; }
}
}

我们删除了两个星号,因此它们的括号已经变得无用了。 我们说这里a不是数组,而是数组的地址,因此a [i]表达式应该是非法的。 然而,Rust对这样的地址进行了以下简化:每次使用引用时,就好像它是一个非引用值,Rust试图假装它前面有一个星号,也就是说,它试图取消引用它, 因此它将引用的对象视为引用本身。

结果语法是C ++引用的语法,不同之处在于,在Rust中也允许应用显式的解引用,即在引用之前,允许您编写或省略星号,而在C ++中,您必须使用它 在指针之前,不能将它放在引用之前。

使用引用

引用主要用作函数参数,但您也可以在其他地方使用它们:

let a = 15;
let ref_a = &a;
print!("{} {} {}", a, *ref_a, ref_a);

这将打印:“15 15 15”。

实际上,同一个对象被打印三次。

a变量只包含一个32位对象,其值为15。

ref_a变量包含此类对象的内存地址,即变量的内存地址。 因此,它是对数字的引用。

在最后一个陈述中,首先打印a的值; 然后,采用并打印ref_a引用的对象; 最后,编译器尝试直接打印ref_a变量,但由于不允许以这种方式直接打印引用,所以引用和打印引用的对象。
使用引用,你可以做一些精湛的技巧:

let a = &&&7;
print!("{} {} {} {}", ***a, **a, *a, a);

这将打印:“7 7 7 7”。
在第一个语句中,获取7值,并将其放入无名对象的内存中。 然后,获取这种对象的地址,并将这样的地址放在第二无名对象的存储器中。 然后,获取该对象的地址,并将其放入第三无名对象的存储器中。 然后,获取该对象的地址,并将其放入第四对象的存储器中,并且名称a与该对象相关联。 该第四个对象是变量,因此它是对对数字的引用的引用的引用。

在第二个语句中,首先打印***表达式。 这里,考虑到a是一个参考,通过使用三个最右边的星号,取a所引用的对象; 然后,考虑到这个对象也是一个引用,通过使用中间的星号来获取它引用的对象; 然后,考虑到这个对象也是一个引用,通过使用最左边的星号来获取它引用的对象; 最后,考虑到这个对象是一个数字,它被打印成一个数字。

如果添加一个或多个附加星号,将导致编译错误,因为不允许取消引用数字而不是引用的对象。

在使用完全显式的语法打印前7个之后,使用分别表示一个,两个或三个星号的表达式,相同的对象被打印三次。

引用的可变性

让我们看看如何将mut关键字与引用一起使用:

let mut a: i32 = 10;
let mut b: i32 = 20;
let mut p: &mut i32 = &mut a; // line 3
print!("{} ", *p);
*p += 1; // line 5
print!("{} ", *p);
p = &mut b; // line 7
print!("{} ", *p);
*p += 1; // line 9
print!("{} ", *p);

这将打印:“10 11 20 21”。

这里,我们有两个数字变量a和b,以及参考p,最初是指a。

暂时忽略所有那些可怜的话。 最初,a的值是10,p是指a,因此,通过打印* p,打印10。

然后,在第5行,由p引用的对象递增,变为11,因此,通过打印* p,打印11。

然后,在第7行,使p参考b,其值为20,实际上,通过打印* p,打印20。

然后,在第9行,现在由p引用的对象也增加,变为21,因此,通过打印* p,打印21。

请注意,在第5行,p间接递增a变量,因此a必须是可变的;在第9行,p间接递增b变量,因此b必须是可变的;在第7行,p本身发生了变化,因此p也必须是可变的。但编译器的推理是不同的。

实际推理如下。

在第5行和第9行,p引用的对象递增,即它们被读取和写入,并且仅当* p是可变的时才允许这样做。所以p不能是&i32;它必须是&mut i32类型,这是“对可变i32的引用”。

表达式&a的类型为&i32,因此不能将其分配给p,即类型为&mut i32。相反,表达式&mut a具有正确的类型,因为它具有相同的p可变性。因此,第3行中的初始化是正确的。

但是表达式&mut a,可以读作“引用可变的a”,允许我们改变a,而Rust只有在a可变时才允许这样做。因此,仅当a变量声明为可变时才允许第3行的初始化。类似地,仅当b变量是可变的时,才允许第7行的初始化。

然后,注意在第7行p本身被改变,也就是说,它被引用到某个其他对象。仅当p变量是可变的时才允许这样做。

因此,已经解释了在该程序中对mut的任何使用。

但是,最好坚持在第3行,第一个mut字与另外两个字具有不同的含义。

第一个mut字意味着p变量可以被改变,这意味着它可以被重新分配,使它引用另一个对象,就像它在第7行所做的那样。没有这样的mut字,p总是指同一个目的。

第二个和第三个mut字意味着p的类型允许它改变被引用对象的值。如果没有这样的mut字,p将无法更改其引用对象的值。

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82948871