Rust 练习册 :进制转换与错误处理的艺术
在计算机科学中,数字可以用不同的进制表示。我们最熟悉的是十进制,但计算机内部使用二进制,网络协议可能使用十六进制,还有一些特殊应用会使用其他进制。在 Exercism 的 “all-your-base” 练习中,我们将实现一个通用的进制转换器,它可以在任意进制之间转换数字。这不仅能帮助我们理解数字的表示方式,还能深入学习 Rust 中的错误处理和算法实现。
问题描述
练习要求我们实现一个能够在任意进制之间转换数字的函数。让我们先看看函数签名:
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidInputBase,
InvalidOutputBase,
InvalidDigit(u32),
}
///
/// Convert a number between two bases.
///
/// A number is any slice of digits.
/// A digit is any unsigned integer (e.g. u8, u16, u32, u64, or usize).
/// Bases are specified as unsigned integers.
///
/// Return an `Err(.)` if the conversion is impossible.
/// The tests do not test for specific values inside the `Err(.)`.
///
///
/// You are allowed to change the function signature as long as all test still pass.
///
///
/// Example:
/// Input
/// number: &[4, 2]
/// from_base: 10
/// to_base: 2
/// Result
/// Ok(vec![1, 0, 1, 0, 1, 0])
///
/// The example corresponds to converting the number 42 from decimal
/// which is equivalent to 101010 in binary.
///
///
/// Notes:
/// * The empty slice ( "[]" ) is equal to the number 0.
/// * Never output leading 0 digits, unless the input number is 0, in which the output must be `[0]`.
/// However, your function must be able to process input with leading 0 digits.
///
pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
unimplemented!(
"Convert {:?} from base {} to base {}",
number,
from_base,
to_base
)
}
我们需要实现一个函数,它能够:
- 将一个数字从[from_base]进制转换为[to_base]进制
- 正确处理各种边界情况
- 在输入无效时返回适当的错误
进制转换原理
在不同进制之间转换数字需要两个步骤:
- 将输入数字从源进制转换为十进制
- 将十进制数字转换为目标进制
例如,将二进制数 101010 转换为十进制:
1×2⁵ + 0×2⁴ + 1×2³ + 0×2² + 1×2¹ + 0×2⁰ = 32 + 0 + 8 + 0 + 2 + 0 = 42
然后将十进制数 42 转换为二进制:
42 ÷ 2 = 21 余 0
21 ÷ 2 = 10 余 1
10 ÷ 2 = 5 余 0
5 ÷ 2 = 2 余 1
2 ÷ 2 = 1 余 0
1 ÷ 2 = 0 余 1
从下往上读取余数:101010
实现步骤
1. 输入验证
首先我们需要验证输入参数的有效性:
// 检查进制是否有效(必须大于1)
if from_base < 2 {
return Err(Error::InvalidInputBase);
}
if to_base < 2 {
return Err(Error::InvalidOutputBase);
}
// 检查每个数字是否有效(必须小于进制基数)
for &digit in number {
if digit >= from_base {
return Err(Error::InvalidDigit(digit));
}
}
2. 处理特殊情况
我们需要处理一些特殊情况:
// 空数组表示数字0
if number.is_empty() {
return Ok(vec![0]);
}
// 全为0的数组也表示数字0
if number.iter().all(|&d| d == 0) {
return Ok(vec![0]);
}
3. 进制转换算法
完整的进制转换实现:
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidInputBase,
InvalidOutputBase,
InvalidDigit(u32),
}
///
/// Convert a number between two bases.
///
/// A number is any slice of digits.
/// A digit is any unsigned integer (e.g. u8, u16, u32, u64, or usize).
/// Bases are specified as unsigned integers.
///
/// Return an `Err(.)` if the conversion is impossible.
/// The tests do not test for specific values inside the `Err(.)`.
///
///
/// You are allowed to change the function signature as long as all test still pass.
///
///
/// Example:
/// Input
/// number: &[4, 2]
/// from_base: 10
/// to_base: 2
/// Result
/// Ok(vec![1, 0, 1, 0, 1, 0])
///
/// The example corresponds to converting the number 42 from decimal
/// which is equivalent to 101010 in binary.
///
///
/// Notes:
/// * The empty slice ( "[]" ) is equal to the number 0.
/// * Never output leading 0 digits, unless the input number is 0, in which the output must be `[0]`.
/// However, your function must be able to process input with leading 0 digits.
///
pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
// 验证进制
if from_base < 2 {
return Err(Error::InvalidInputBase);
}
if to_base < 2 {
return Err(Error::InvalidOutputBase);
}
// 验证数字
for &digit in number {
if digit >= from_base {
return Err(Error::InvalidDigit(digit));
}
}
// 处理空数组和全零数组
if number.is_empty() || number.iter().all(|&d| d == 0) {
return Ok(vec![0]);
}
// 第一步:将数字从 from_base 转换为十进制
let mut value = 0u32;
for &digit in number {
value = value.checked_mul(from_base)
.ok_or(Error::InvalidDigit(digit))?
.checked_add(digit)
.ok_or(Error::InvalidDigit(digit))?;
}
// 第二步:将十进制数转换为 to_base
let mut digits = Vec::new();
let mut n = value;
while n > 0 {
digits.push(n % to_base);
n /= to_base;
}
// 由于我们是从低位到高位计算的,需要反转数组
digits.reverse();
Ok(digits)
}
测试用例分析
通过查看测试用例,我们可以更好地理解需求:
#[test]
fn single_bit_one_to_decimal() {
let input_base = 2;
let input_digits = &[1];
let output_base = 10;
let output_digits = vec![1];
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Ok(output_digits)
);
}
最简单的用例,将二进制的 1 转换为十进制的 1。
#[test]
fn binary_to_single_decimal() {
let input_base = 2;
let input_digits = &[1, 0, 1];
let output_base = 10;
let output_digits = vec![5];
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Ok(output_digits)
);
}
将二进制的 101 转换为十进制的 5。
#[test]
fn trinary_to_hexadecimal() {
let input_base = 3;
let input_digits = &[1, 1, 2, 0];
let output_base = 16;
let output_digits = vec![2, 10];
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Ok(output_digits)
);
}
将三进制的 1120 转换为十六进制的 2A(这里 10 代表十六进制的 A)。
#[test]
fn empty_list() {
let input_base = 2;
let input_digits = &[];
let output_base = 10;
let output_digits = vec![0];
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Ok(output_digits)
);
}
空数组应该转换为 [0]。
#[test]
fn leading_zeros() {
let input_base = 7;
let input_digits = &[0, 6, 0];
let output_base = 10;
let output_digits = vec![4, 2];
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Ok(output_digits)
);
}
需要正确处理前导零。
#[test]
fn invalid_positive_digit() {
let input_base = 2;
let input_digits = &[1, 2, 1, 0, 1, 0];
let output_base = 10;
assert_eq!(
ayb::convert(input_digits, input_base, output_base),
Err(ayb::Error::InvalidDigit(2))
);
}
当数字大于或等于进制基数时,应该返回错误。
错误处理的重要性
这个练习展示了三种不同的错误情况:
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidInputBase, // 输入进制无效(小于2)
InvalidOutputBase, // 输出进制无效(小于2)
InvalidDigit(u32), // 数字无效(大于等于进制基数)
}
使用枚举来表示不同类型的错误是 Rust 中的惯用法,它比使用简单的错误码更加明确和安全。
优化版本
考虑到大数的情况,我们可以提供一个更安全的版本:
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidInputBase,
InvalidOutputBase,
InvalidDigit(u32),
}
pub fn convert(number: &[u32], from_base: u32, to_base: u32) -> Result<Vec<u32>, Error> {
// 验证进制
if from_base < 2 {
return Err(Error::InvalidInputBase);
}
if to_base < 2 {
return Err(Error::InvalidOutputBase);
}
// 验证数字并处理特殊情况
if number.is_empty() {
return Ok(vec![0]);
}
// 检查数字有效性,并同时检查是否全为0
let mut all_zero = true;
for &digit in number {
if digit >= from_base {
return Err(Error::InvalidDigit(digit));
}
if digit != 0 {
all_zero = false;
}
}
if all_zero {
return Ok(vec![0]);
}
// 第一步:将数字从 from_base 转换为十进制(使用u32防止溢出)
let mut value: u32 = 0;
for &digit in number {
value = value.checked_mul(from_base)
.ok_or(Error::InvalidDigit(digit))?
.checked_add(digit)
.ok_or(Error::InvalidDigit(digit))?;
}
// 第二步:将十进制数转换为 to_base
let mut digits = Vec::new();
let mut n = value;
while n > 0 {
digits.push(n % to_base);
n /= to_base;
}
// 反转数组以获得正确的顺序
digits.reverse();
Ok(digits)
}
性能和边界考虑
在实现进制转换时,我们需要考虑:
- 溢出处理:使用
checked_mul和checked_add防止算术溢出 - 内存效率:预分配合适大小的 Vec 可以提高性能
- 算法复杂度:时间复杂度为 O(n log m),其中 n 是输入数字位数,m 是数值大小
总结
通过 all-your-base 练习,我们学到了:
- 进制转换算法:理解了不同进制之间转换的数学原理
- 错误处理:学会了如何使用 Result 和自定义错误类型
- 边界条件处理:掌握了如何处理空数组、前导零等特殊情况
- 数值安全:了解了如何防止算术溢出
- 算法实现:实践了基础的数学算法实现
这些技能在实际开发中非常有用,无论是处理不同格式的数据,还是实现编解码器,都需要类似的技能。进制转换虽然看起来简单,但涉及到了许多计算机科学的基础概念,是学习更高级主题的良好起点。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)