Beginng_Rust(译):定义通用函数和结构(第十章)(完+1)

在本章中,您将学习:
•如何编写单个函数定义,其调用可以有效地处理不同的数据类型

•如何使用类型推断来避免指定使用的类型
通用功能

•如何编写单个struct,tuple-struct或enum类型,其实例可以包含有效的不同数据类型

•如何使用两个重要的标准通用枚举(选项和结果)来表示可选数据或错误功能结果

•某些标准功能如何简化Option和Result的处理

需要通用函数

Rust执行严格的数据类型检查,因此当你定义一个使用某种类型的参数的函数时,比如fn square_root(x:f32) - > f32,调用这样一个函数的代码必须传递给它一个完全正确的表达式 该类型,如square_root(45.2f32),或者每次使用该函数时都必须执行显式转换,例如square_root(45.2f64 as f32)。 您无法传递其他类型,例如square_root(45.2f64)。

这对于编写调用函数的代码的人来说是不方便的,但对于编写函数本身的人也是如此。 由于Rust有许多不同的数字类型,因此在编写函数时,必须处理选择哪种类型的问题。 例如,如果您决定指定函数的参数必须是i16类型,但是对于每次调用,首选i32类型,则最好更改该函数定义。

如果您的功能将由多个模块或甚至多个应用程序使用,据我们所见,没有办法满足您的每个功能用户。
例如:

// Library code
fn f(ch: char, num1: i16, num2: i16) -> i16 {
if ch == 'a' { num1 }
else { num2 }
}
// Application code
print!("{}", f('a', 37, 41));

这将打印:“37”。

在应用程序代码中,如果用37.2替换37,用41替换41,则会出现编译错误; 而且,如果你在每个数字之后添加为i16,则获取语句print!(“{}”,f(‘a’,37.2 as i16,41 as i16)); 该程序仍将打印37,而不是所需的37.2。

在您决定更改库代码,用f32或f64替换i16时,程序将在上述所有情况下正常工作,但会强制所有调用者使用浮点数。

定义和使用通用函数

在Rust中解决此问题的惯用方法是编写以下代码:

// Library code
fn f<T>(ch: char, num1: T, num2: T) -> T {
if ch == 'a' { num1 }
else { num2 }
}
// Application code
let a: i16 = f::<i16>('a', 37, 41);
let b: f64 = f::<f64>('b', 37.2, 41.1);
print!("{} {}", a, b);

这将打印37 41.1。

在函数定义中,在函数名称后面,有一个用尖括号括起来的T字。 此符号是函数声明的类型参数。

这意味着声明的内容不是具体函数,而是泛型函数,它由T类型参数参数化。 只有在编译时,才会为这样的T参数指定具体类型,这样的函数才会成为具体的函数。

这样的T参数仅在函数定义的范围内定义。 实际上,它仅在函数的签名中使用了三次,并且它也可以在函数体中使用,但不能在其他地方使用。

虽然ch参数是char类型,但num1和num2参数以及函数返回值都是T泛型类型。 当使用这样的函数时,将需要用具体类型替换这样的T参数,从而获得具体的功能。

应用程序代码的第一行,而不是使用f泛型函数,使用f:函数,即通过用i16类型替换T参数获得的具体函数。类似地,应用程序代码的第二行调用f :: 函数,即通过用f64类型替换T参数获得的具体函数。

请注意,在使用i16类型的情况下,可以约束为i16类型的两个整数值作为f generic函数的第二个和第三个参数传递,并且函数返回的值将分配给类型为i16的变量。

相反,在使用f64类型的情况下,可以约束为f64类型的两个浮点值作为f泛型函数的第二个和第三个参数传递,并且函数返回的值被赋值给变量有类型f64。

如果交换了函数参数的类型或接收返回值或两者的变量类型,则会获得一些不匹配的类型编译错误。

通过这种方式,通过编写库代码而无需无用的重复,可以编写使用两种不同类型的应用程序代码,并且可以轻松添加其他类型,而无需更改现有的库代码。

C语言不允许泛型函数,但C ++语言允许它们:它们是函数模板。

推断参数类型

但是,上述应用程序代码可以进一步简化:

// Library code
fn f<T>(ch: char, num1: T, num2: T) -> T {
if ch == 'a' { num1 }
else { num2 }
}
// Application code
let a: i16 = f('a', 37, 41);
let b: f64 = f('b', 37.2, 41.1);
print!("{} {}", a, b);

如图所示,删除了:: 和:: 子句,无论如何都获得了一个等效的程序。 实际上,编译器在解析泛型函数的调用时,使用作为参数传递的值的类型来确定类型参数。

据说,参数类型是从包含泛型函数调用的表达式中使用的值的类型推断出来的。

当然,使用的各种类型必须一致:

fn f<T>(a: T, _b: T) -> T { a }
let _a = f(12u8, 13u8);
let _b = f(12i64, 13i64);
let _c = f(12i16, 13u16);
let _d: i32 = f(12i16, 13i16);

这会在last-but-one语句中生成编译错误,在最后一个语句中生成另外两个错误。 实际上,第一次和第二次调用传递两个相同类型的数字,而第三次调用传递两个不同类型的值,即使它们必须是相同的类型,由T泛型类型表示。

在最后一个语句中,两个参数具有相同的类型,但返回的值被分配给不同类型的变量。

如果需要使用多个不同类型的值来参数化函数,可以通过指定多个类型参数来实现:

fn f<Param1, Param2>(_a: Param1, _b: Param2) {}
f('a', true);
f(12.56, "Hello");
f((3, 'a'), [5, 6, 7]);

即使它什么都不做,该程序也是有效的。

定义和使用通用结构

参数类型也可用于声明泛型结构和通用元组结构:

struct S<T1, T2> {
c: char,
n1: T1,
n2: T1,
n3: T2,
}
let _s = S { c: 'a', n1: 34, n2: 782, n3: 0.02 };
struct SE<T1, T2> (char, T1, T1, T2);
let _se = SE ('a', 34, 782, 0.02);

第一个语句声明了通用结构S,由两个类型T1和T2参数化。 第一个这样的泛型类型由两个字段使用,而第二个字段仅由一个字段使用。

第二个语句创建一个具有此类泛型类型的具体版本的对象。 参数T1被i32隐式替换,因为两个无约束整数32和782用于初始化两个字段n1和n2,参数T2被f64隐式替换,因为无约束浮点数0.02用于初始化 字段n3。

第三和第四个语句类似,但它们使用元组结构而不是结构。

对于结构体,类型参数的具体化可以明确:

struct S<T1, T2> {
c: char,
n1: T1,
n2: T1,
n3: T2,
}
let _s = S::<u16, f32> { c: 'a', n1: 34, n2: 782, n3: 0.02 };
struct SE<T1, T2> (char, T1, T1, T2);
let _se = SE::<u16, f32> ('a', 34, 782, 0.02);

C语言不允许使用泛型结构,但C ++语言允许它们:它们是类模板和结构模板。

通用力学

为了更好地理解泛型的工作原理,您应该充当编译器的角色来遵循编译过程。 实际上,从概念上讲,通用代码编译分几个阶段进行。

让我们遵循编译的概念机制,应用于以下代码:

fn swap<T1, T2>(a: T1, b: T2) -> (T2, T1) { (b, a) }
let x = swap(3i16, 4u16);
let y = swap(5f32, true);
print!("{:?} {:?}", x, y);

在第一阶段,扫描源代码,并且每次编译器找到泛型函数声明(在示例中,交换函数的声明)时,它在其数据结构中加载此函数的内部表示,在其所有 通用性,仅检查通用代码中是否存在语法错误。

在第二阶段,再次扫描源代码,并且每次编译器遇到泛型函数的调用时,它在其数据结构中加载这种用法与通用声明的相应内部表示之间的关联,当然 检查此类通信是否有效。

因此,在我们的示例中的前两个阶段之后,编译器具有通用交换函数和具体的main函数,并且最后一个函数包含对通用交换函数的两个引用。

在第三阶段,扫描所有泛型函数的调用(在本例中,两次调用swap)。 对于每种这样的用法,并且对于相应定义的每个通用参数,确定具体类型。 这种具体类型在使用中可以是明确的,或者(如在示例中)可以从用作函数的自变量的表达式的类型推断出它。 在该示例中,对于第一次调用swap,参数T1与i16类型相关联,参数T2与u16类型相关联; 相反,在第二次调用swap时,参数T1与f32类型相关联,参数T2与bool类型相关联。

在确定了要替换通用参数的具体类型之后,生成通用函数的具体版本。 在这样的具体版本中,每个通用参数都由为特定函数调用确定的具体类型替换。 并且通用函数的调用被刚刚生成的具体函数的调用所取代。

例如,生成的内部表示对应于以下Rust代码:

fn swap_i16_u16(a: i16, b: u16) -> (u16, i16) { (b, a) }
fn swap_f32_bool(a: f32, b: bool) -> (bool, f32) { (b, a) }
let x = swap_i16_u16(3i16, 4u16);
let y = swap_f32_bool(5f32, true);
print!("{:?} {:?}", x, y);

如您所见,没有更多通用定义或通用函数调用。 泛型函数定义已转换为两个具体的函数定义,两个函数调用现在调用每个函数定义一个不同的具体函数。

第四阶段包括编译此代码。

请注意,需要生成两个不同的具体函数,因为通用交换函数的两个调用指定了不同的类型。

但是这段代码:

fn swap<T1, T2>(a: T1, b: T2) -> (T2, T1) { (b, a) }
let x = swap('A', 4.5);
let y = swap('g', -6.);
print!("{:?} {:?}", x, y);

内部翻译为此代码:

fn swap_char_f64(a: char, b: f64) -> (f64, char) { (b, a) }
let x = swap_char_f64('A', 4.5);
let y = swap_char_f64('g', -6.);
print!("{:?} {:?}", x, y);

即使有多个泛型函数交换调用,也只生成一个具体版本,因为所有调用都需要相同的参数类型。

通常,当多个调用指定完全相同的类型参数时,始终应用优化生成通用函数声明的一个具体版本。

编译器可以在单个程序中生成与单个函数对应的几个具体机器代码版本,这一事实会产生以下结果:

•关于编译非泛型代码,这种多阶段编译速度稍慢。
•为每个特定的调用高度优化了生成的代码,因为它完全使用调用者使用的类型,而无需转换或决策。 在优化每次调用的运行时性能之前。
•如果对通用函数执行了许多具有不同数据类型的调用,则会生成大量机器代码。 我们已经讨论过这种现象,称“代码膨胀”,关于这样一个事实,即为了优化性能,最好不要在单个处理中使用许多不同的类型,因为不同的代码用于不同的类型,这会给CPU带来负担缓存。

本节中关于泛型函数的所有内容也适用于泛型结构和元组结构。

通用数组和向量

关于数组和向量,没有新闻。 我们从一开始就把它们视为泛型类型。

实际上,虽然数组是Rust语言的一部分,但是向量是Rust标准库中定义的结构。

通用枚举

在Rust中,枚举也可以是通用的。

enum Result1<SuccessCode, FailureCode> {
Success(SuccessCode),
Failure(FailureCode, char),
Uncertainty,
}
let mut _res = Result1::Success::<u32,u16>(12u32);
_res = Result1::Uncertainty;
_res = Result1::Failure(0u16, 'd');

该计划有效。 相反,下面的一个导致在最后一行进行编译,因为Failure的第一个参数是u32类型,而它应该是u16类型,根据_res的初始化,前面两行。

enum Result1<SuccessCode, FailureCode> {
Success(SuccessCode),
Failure(FailureCode, char),
Uncertainty,
}
let mut _res = Result1::Success::<u32,u16>(12u32);
_res = Result1::Uncertainty;
_res = Result1::Failure(0u32, 'd');

通用枚举在Rust标准库中经常使用。

Rust标准库中定义的最常用枚举之一解决了以下常见问题。 如果一个函数失败了,它应该怎么办?

例如,函数pop从向量中删除最后一项,如果该向量包含某些项,则返回已删除的项。 但是表达式应该是什么![0; 0] .pop()吗? 它正在从空矢量中删除一个项目!

有些语言会将此行为保留为未定义,从而可能导致崩溃或不可预测的结果。 Rust尽可能避免未定义的行为。

某些语言引发异常,由封闭块或当前函数的调用者处理,或导致崩溃。 Rust不使用异常的概念。

某些语言返回特定的空值。 但是矢量几乎可以包含任何可能的类型,并且许多类型没有空值。

这是Rust解决方案:

let mut v = vec![11, 22, 33];
for _ in 0..5 {
let item: Option<i32> = v.pop();
match item {
Some(number) => print!("{}, ", number),
None => print!("#, "),
}
}

这将打印:“33,22,11,#,#,”。

v变量是最初包含三个数字的向量。

循环执行五次迭代。 他们每个人都试图从v中删除一个项目。如果删除成功,则打印删除的项目,否则打印#字符。 应用于Vec 类型的对象的pop函数返回Option 类型的值。

这种泛型类型由Rust标准库定义为:

enum Option<T> {
Some(T),
None,
}

此枚举意味着:“这是T类型的可选值。 它可以选择成为T,也可以选择什么也不做。 它可以是某种东西或什么都不是 如果它是某种东西,它就是T.“

如果有这样的定义,那么这个定义可能会更清楚:

enum Optional<T> {
Something(T),
Nothing,
}

应该这样认为。 但是,Rust总是尝试缩写名称,因此之前的定义是有效的。

回到示例,在循环的第一次迭代中,变量项的值是Some(33); 在第二次迭代中它是Some(22); 在第三次迭代中它是Some(11); 然后v向量变空,因此pop只能返回None,它在第四次和第五次迭代时被赋值给item。

匹配语句区分何时弹出某个数字,以及何时存在无。 在前一种情况下,打印该数字,在后一种情况下,仅打印#。

错误处理

Rust标准库还定义了一个通用枚举,用于处理函数无法返回预期类型值的情况。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0. {
Err(format!("Divide by zero"))
} else {
Ok(numerator / denominator)
}
}
print!("{:?}, {:?}", divide(8., 2.), divide(8., 0.));
This will print Ok(4), Err("Divide by zero").

除法函数应返回第一个数除以第二个数的结果,但仅当第二个数不为零时才返回。 在后一种情况下,它应该返回一条错误消息。

Result类型与Option类型类似,但Option类型在缺少结果的情况下表示为None,Result类型可以添加描述此类异常条件的值。

标准库中此通用枚举的定义是:

enum Result<T, E> {
Ok(T),
Err(E),
}

在我们的例子中,T是f64,因为这是由两个f64数字的划分产生的类型,而E是String,因为我们想要打印一条消息。

我们只使用调用结果将它们作为调试信息打印出来。 但是,在生产计划中,这是不可接受的。 更合适的代码将是以下代码。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0. {
Err(format!("Divide by zero"))
} else {
Ok(numerator / denominator)
}
}
fn show_divide(num: f64, den: f64) {
match divide(num, den) {
Ok(val) => println!("{} / {} = {}", num, den, val),
Err(msg) => println!("Cannot divide {} by {}: {}", num, den, msg),
}
}
show_divide(8., 2.);
show_divide(8., 0.);

这将打印:

8 / 2 = 4
Cannot divide 8 by 0: Divide by zero

枚举标准实用功能

Option和Result标准泛型类型允许我们以灵活有效的方式捕获现实代码中发生的所有情况; 但是,使用匹配语句来获得结果是非常不方便的。

因此,标准库包含一些实用程序函数,以便于解码选项或结果值。

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
if denominator == 0. {
Err(format!("Divide by zero"))
} else {
Ok(numerator / denominator)
}
}
let r1 = divide(8., 2.);
let r2 = divide(8., 0.);
println!("{} {}", r1.is_ok(), r2.is_ok());
println!("{} {}", r1.is_err(), r2.is_err());
println!("{}", r1.unwrap());
println!("{}", r2.unwrap());
This program first prints:
true false
false true
4

该程序首先打印:
真假
错误的
4
然后恐慌与消息:线程’主’惊慌失措’在’Err值上调用Result :: unwrap()`:“除以零”’。

如果is_ok函数应用于Ok变量,则返回true。 如果is_err函数应用于Err变量,则返回true。 因为它们是唯一可能的变体,is_err()相当于!is_ok()。

unwrap函数返回Ok变量的值,如果它应用于Ok变量,否则它会发生混乱。 这个函数的意思是“我知道这个值可能是一个包含在Ok变量中的值,所以我只想得到它包含的值,摆脱它的包装; 在奇怪的情况下,它不是一个Ok变体,发生了不可恢复的错误,所以我想立即终止程序“。

Option枚举也有一个解包函数。 要打印Vec中的所有值,您可以编写:

let mut v = vec![11, 22, 33];
for _ in 0..v.len() {
print!("{}, ", v.pop().unwrap())
}

这将打印:“33,22,11,”。 unwrap的调用获取pop()返回的Ok枚举中的数字。 我们避免在空向量上调用pop(); 否则,pop()会返回一个None,而unwrap()会惊慌失措。

解包功能在快速和脏的Rust程序中使用很多,其中可能不需要以用户友好的方式处理可能的错误。

猜你喜欢

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