一些常见的应用逻辑示例

发布时间:2022-10-18 10:31
最后更新:2024-06-20 22:38
所属分类:
Rust

在编程过程中,我们常常对于一种或者几种处理过程采用特定公认比较简洁有效的处理逻辑。但是由于高级编程语言对于底层内存控制的屏蔽,导致这些本来看起来十分简单的处理逻辑在Rust变得十分复杂且应用起来困难重重。本文将不断的收集一些常见的处理逻辑写法供实际编程过程中参考。

常见所有权发生转移的情况

Rust中的很多功能都是由各种特征定义的,但是经常在不经意间就调用了会发生所有权转移的方法。因为会发生所有权转移的方法太多,这里不一一列举,只是通过几个识别特征来帮助确认所有权转移的情况。

使用for循环的时候发生所有权转移

使用for ... in循环迭代一个可迭代对象是一件再普通不过的事情了,但是这个语句存在一个容易被忽略的问题:for ... in是利用IntoIterator特征的,所以会首先调用被迭代对象实现的.into_iter()方法,将其转换成一个Iterator

因为IntoIterator.into_iter()的第一个参数为self,并不是其引用形式,所以在for语句进行迭代的过程中,被迭代对象中的内容就被消耗掉了,所以经常会出现在经过for循环以后,被迭代对象的内容被Rust编译器提示所有权已被转移的问题。

要解决这个问题可以通过创建被迭代对象克隆(如果实现了Clone特征)或者切片来先复制被迭代对象解决。

使用“替身”是最好的保护方法。

.map().collect()

在其他的语言中,.map()经常被用来对一个列表中的值进行变换,从而可以获得一个内容为全新类型的列表。但是由于Rust中循环都是基于迭代器而且.map()需要从其闭包中返回在函数中生成的内容(包括值和引用),所以就使得.map()方法看起来比较难以使用。

从函数返回内容的限制

在Rust中,从一个函数中返回在函数中生成的内容,不能是引用,必须是可以转移所有权的。这一条规则很容易理解,因为在函数内部生成的内容,会在函数执行结束的时候被销毁,如果返回这个内容的引用,那么在函数外部获得的指针就直接变成了悬垂指针。

所以在从函数内部返回由函数生成的新内容时,必须是可以转移所有权的实例,而不是引用。

这里经常出现的编译器错误提示是:cannot return value referencing temporary value,并且会跟随用returns a value referencing data owned by the current function指出返回值的所有权是属于函数的。

根据从函数中返回内容的限制,即便我们传入.map()函数的内容是引用,那么我们也必须从.map()函数中定义的闭包中返回一个可转移所有权的实例。

从这个角度来说,就已经开始存在解决方法的思路了。

  1. 构建一个新的实例。
  2. 通过ToOwned特征提供的.to_owned()方法转换。

比如可以将一个Vec<&str>转换为一个Vec<String>

1
let target = origin.into_iter().map(|item| String::from(item)).collect::<Vec<String>>();

因为示例中的originVec<&str>类型,是没有实现Map特征的,所以不能直接使用.map()方法。但是Vec可以通过IntoIterator特征提供的.into_inter()方法转换成迭代器,这样就可以使用.map()方法了。.map()方法返回的返回值类型依旧是一个Iterator,所以还需要通过Iterator特征提供的.collect()方法收敛一下,重新使其形成所需要的Vec类型结果。

扫描目录树

扫描目录树是众多语言中比较基本的文件系统操作,比较常见的扫描方法是使用递归。但是一般说来,递归的方法因为在循环调用自身的时候会保存调用现场,所以对于空间和时间并不友好,但它的特点就是条理清晰易于理解。所以这里从递归法开始,记录两种对目录树进行扫描,并同时进行过滤,并对过滤后的内容执行指定处理功能的方法。

递归法

递归法是比较简单的,这里首先列出递归法的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn process_directories<P, F, A>(path: P, filter: &F, action: &A) -> io::Result<()>
  where
    P: AsRef<Path>,
    F: Fn(&PathBuf) -> bool,
    A: Fn(&PathBuf) -> io::Result<()>
{
  if path.as_ref().is_dir() {
    for entry in fs::read_dir(path.as_ref())? {
      let entry = entry?;
      let entry_path = entry.path()?;
      if entry_path.is_dir() {
        process_directories(&entry_path, filter, action)?;
      } else if filter(&entry_path) {
        action(&entry_path)?;
      }
    }
  }
  Ok(())
}

在这个实现中,逐层深入目录树的逻辑是十分清晰的,但是这里需要注意的一点是,因为在函数中调用了自身,所以传入的闭包filteraction就必须是引用的形式,否则在调用自身的时候,就会将filteraction两个闭包的所有权转移给自身的新实例。利用这个实现可以这样来实现一个删除目录树中所有文件名开头为as的文件的功能。

1
2
3
4
5
6
7
8
9
// 这里的originPath的类型可以是&str。
process_directories(
  Path::new(originPath),
  &|path| {
    let file_name = path.file_name().and_then(|n| n.to_str()).unwrap();
    file_name.starts_with("as")
  },
  &|path| fs::remove_file(path)
);
不必要一层一层的解开Option类型的值,可以直接利用其提供的.and_then()方法将其中的内容转换成其他类型的值,但是值还是由Option包裹的,这是因为Option类型的值还可能取None值,.and_then()方法只会在Option值中的内容为Some()的时候才会继续调用。

迭代法

迭代法没有递归法那么大的开销,实际运行速度也更快,但是其实现稍微有一点儿不太容易理解。总结起来说,迭代法是提前排列好了需要扫描的目录任务,然后逐一解决即可。以下是迭代法的实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn process_directories<P, F, A>(path: P, filter: F, action: A) -> io::Result<()>
  where
    P: AsRef<Path>,
    F: Fn(&PathBuf) -> bool,
    A: Fn(&PathBuf) -> io::Result<()>
{
  let mut task_queue = VecDequeue::from([path.as_ref().to_path_buf()]);
  while let Some(task) = task_queue.pop_front() {
    if task.is_dir() {
      for entry in fs::read_dir(task)? {
        let entry = entry?;
        let entry_path = entry.path()?;
        if entry_path.is_dir() {
          task_queue.push_back(entry_path.to_owned());
        } else if filter(&entry_path) {
          action(&entry_path)?;
        }
      }
    }
  }
  Ok(())
}

迭代法的函数定义看起来有一些不一样,主要是filteraction参数不是引用的形式了。这是因为在迭代法中,函数不必调用自身,所以filteraction参数在传入以后就不用再考虑向其他的函数中转移了。所以,对于这个函数的调用,也就少了两个&

1
2
3
4
5
6
7
8
9
// 这里的originPath的类型可以是&str。
process_directories(
  Path::new(originPath),
  |path| {
    let file_name = path.file_name().and_then(|n| n.to_str()).unwrap();
    file_name.starts_with("as")
  },
  |path| fs::remove_file(path)
);

构建全局可访问的资源

在其他很多语言中,我们都习惯于把一些在整个应用中共享使用的资源定义成全局静态变量或者是单例变量,例如从其他资源读入的应用配置、数据库连接、数据库连接池、日志记录器等等。但是在Rust中,由于有所有权转移这一特性的存在,在不同的模块和函数中使用全局变量也会导致全局资源的所有权被转移,从而影响应用中其他部位对与全局资源的使用。

如果打算在Rust中构建单例,那么这个单例必定是一个生命期为'static的引用。涉及到可以反复利用的引用,那么就一定会想到使用Rc或者Arc这种带引用计数的引用类型搭配Cell包装类型来实现,但是如果这样使用的话,为了线程安全就必须在资源访问的时候联合使用锁来协调多线程下的资源使用。在实际使用中更简单的方法是借助一个现成的库:once_cell

以下以构建一个全局共享的数据库连接池来简要记录once_cell库的使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
use once_cell::sync::OnceCell;
use sqlx::postgres::PgPool;

static INSTANCE: OnceCell<PgPool> = OnceCell::new();

impl PgPool {
  pub fn global() -> &'static PgPool {
    INSTANCE.get().expect("connection pool is not initialized")
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let pool = PgPool::connect(&env::var("DATABASE_URL")?).await?;
  INSTANCE.set(pool).unwrap();

  // 在完成INSTANCE的初始化以后,就可以使用 PgPool::global() 来获取共享的资源引用了。
}
通过.global()方法获取到的全局资源引用的生命期是'static,在使用的时候需要注意,不要让其影响了其他函数和方法的生命期标注,从而导致编译器编译失败。

在这个示例中实现资源的全局访问的核心原理主要是OnceCell结构体提供的线程安全的仅可被赋值一次的特性,而供全局使用的特性则是由static静态标识符提供的。

如果不使用once_cell库提供的功能,而是选择自行实现的话,可以参考以下代码。

这一套示例代码实际上也是借鉴了once_cell中的部分处理逻辑。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
use std::{cell::UnsafeCell, hint, sync::atomic::{AtomicBool, Ordering}};

struct GShareObject<T> {
    initialized: AtomicBool,
    value: UnsafeCell<Option<T>>,
}

impl<T> GShareObject<T> {
  pub const fn new() -> GShareObject<T> {
    GShareObject { initialized: AtomicBool::new(false), value: UnsafeCell::new(None) }
  }

  fn initialize<F, E>(&self, f: F) -> Result<(), E>
  where
    F: FnOnce() -> Result<T, E>
  {
    assert!(!self.initialized.load(Ordering::SeqCst));
    let val = self.value.get();
    let mut res: Result<(), E> = Ok(());
    match f() {
      Ok(value) => unsafe {
        self.initialized.store(true, Ordering::Relaxed);
        *val = Some(value);
      },
      Err(err) => {
        res = Err(err);
      },
    }
    res
  }

  unsafe fn get_unchecked(&self) -> &T {
    let c = &*self.value.get();
    match c {
      Some(t) => t,
      None => {
        debug_assert!(!self.initialized.load(Ordering::SeqCst));
        hint::unreachable_unchecked()
      },
    }
  }

  pub fn get(&self) -> Option<&T> {
    assert!(!self.initialized.load(Ordering::SeqCst));
    unsafe { &*self.value.get() }.as_ref()
  }

  pub fn get_or_try_init<F, E>(&self, f: F) -> Result<&T, E>
  where
    F: FnOnce() -> Result<T, E>
  {
    if let Some(value) = self.get() {
      return Ok(value);
    }
    f()?;

    debug_assert!(!self.initialized.load(Ordering::SeqCst));
    Ok(unsafe { self.get_unchecked() })
  }

  pub fn set<E>(&self, value: T) -> Result<(), E> {
    let mut value = Some(value);
    self.initialize(|| Ok(unsafe { take_unchecked(&mut value) }))
  }
}

unsafe fn take_unchecked<T>(val: &mut Option<T>) -> T {
  match val.take() {
    Some(value) => value,
    None => hint::unreachable_unchecked(),
  }
}

索引标签
Rust
逻辑示例
代码块