如何为仅需要&self的自定义记录器提供内部状态?

时间:2018-10-31 06:09:25

标签: rust

我正在尝试通过实现log crate来实现一个简单的记录器。

记录器的行为应如下:

[1] First log message
[2] Second log message
[3] Third log message

要实现这一点,我有我的记录器结构

struct SeqLogger {
    seq: i64,
}

并实现Log特质

fn enabled(&self, metadata: &Metadata) -> bool
fn log(&self, record: &Record)
fn flush(&self)

log(&self, record: &Record)实现中,我会做

fn log(&self, record: &Record) {
    println!("[{}] {}", self.seq, record.args());
    self.seq = self.seq + 1;
}

但是,编译器抱怨self不可更改。我是否以正确的方式来实现这一目标?没有&mut self的情况下如何更新记录器的状态?

1 个答案:

答案 0 :(得分:7)

似乎logger板条箱不打算让记录器具有任何内部状态,因此它迫使它们被共享为不可变的。实际上,这使事情变得很容易,因为记录器通常应该在线程之间共享并同时使用,而使用& mut self是不可能的。

但是,有一个通常的解决方法:内部可变性。有一种std::cell::Cell类型专门针对该用例而设计:具有对应该可变的内容的不可变引用。您的内部状态仅仅是一个整数,所以它是Copy,我们可以尝试按原样使用Cell

extern crate log; // 0.4.5
use log::*;
use std::cell::Cell;

struct SeqLogger {
    seq: Cell<i64>,
}

impl Log for SeqLogger {

    fn log(&self, record: &Record) {
        println!("[{}] {}", self.seq.get(), record.args());
        self.seq.set(self.seq.get() + 1);
    }

    fn enabled(&self, metadata: &Metadata) -> bool { if false {true} else {unimplemented!()} }

    fn flush(&self) { unimplemented!(); }

}

但是,编译器立即再次生气:

error[E0277]: `std::cell::Cell<i64>` cannot be shared between threads safely
 --> src/lib.rs:9:6
  |
9 | impl Log for SeqLogger {
  |      ^^^ `std::cell::Cell<i64>` cannot be shared between threads safely
  |
  = help: within `SeqLogger`, the trait `std::marker::Sync` is not implemented for `std::cell::Cell<i64>`
  = note: required because it appears within the type `SeqLogger`

这是有道理的,因为正如我之前所说,记录器本身必须是Sync,所以我们必须保证共享其内容也是安全的。同时,Cell不是Sync-正是由于我们在这里使用的内部可变性。同样,有一种通常的解决方法-Mutex

extern crate log; // 0.4.5
use log::*;
use std::cell::Cell;
use std::sync::Mutex;

struct SeqLogger {
    seq: Mutex<Cell<i64>>,
}

impl Log for SeqLogger {

    fn log(&self, record: &Record) {
        let seq = self.seq.lock().unwrap(); // perhaps replace this with match in production
        println!("[{}] {}", seq.get(), record.args());
        seq.set(seq.get() + 1);
    }

    fn enabled(&self, metadata: &Metadata) -> bool { if false {true} else {unimplemented!()} }

    fn flush(&self) { unimplemented!(); }

}

现在它可以编译了。

Playground with the last variant


编辑:根据评论,由于Mutex授予我们内部可变性(一种)和Sync能力,因此我们可以剥离一层间接。因此,我们可以删除Cell并直接引用MutexGuard

// --snip--
fn log(&self, record: &Record) {
    let mut seq = self.seq.lock().unwrap(); // perhaps replace this with match in production
    println!("[{}] {}", *seq, record.args());
    *seq = *seq + 1;
}
// --snip--

此外,由于我们的状态只是一个整数,我们可以使用a standard atomic type而不是Mutex。请注意,AtomicI64是不稳定的,因此您可能想使用AtomicIsizeAtomicUsize来代替:

use std::sync::atomic::{AtomicIsize, Ordering};

struct SeqLogger {
    seq: AtomicIsize,
}

impl Log for SeqLogger {

    fn log(&self, record: &Record) {
        let id = self.seq.fetch_add(1, Ordering::SeqCst);
        println!("[{}] {}", id, record.args());
    }
    // --snip--
}

Playground