Begginng_Rust(译):定义通用函数和结构(第十章)

在本章中,您将学习:

•如何编写单个函数定义,其调用可以有效地处理不同的数据类型
•如何使用类型推断来避免指定泛型函数使用的类型
•如何编写单个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标准库还定义了一个通用枚举,用于处理函数无法返回预期类型值的情况。

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.));

这将打印Ok(4),Err(“除以零”)。

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

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

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

猜你喜欢

转载自blog.csdn.net/m0_37696990/article/details/82809650
今日推荐