有没有办法模拟调用父 class 静态方法的 Java 行为来进行简单的全局错误处理?

Is there a way to emulate the Java behaviour of calling a parent class' static method for simple global-ish error handling?

我正在尝试在 Rust 中为一种名为 rlox 的编造编程语言实现一个简单的解释器,遵循 Bob Nystrom 的书 Crafting Interpreters

我希望错误能够在任何子模块中发生,并且它们在 main 模块中是 "reported"(这在书中完成,Java ,通过简单地调用包含 class 的静态方法来打印有问题的标记和行)。但是,如果发生错误,我不能用 Result::Err 提前 return (我认为这是处理 Rust 错误的惯用方法),因为解释器应该保持 运行 - 不断寻找错误。

有没有一种(惯用的)方法可以让我在 Rust 中用模块模拟 Java 从子 class 调用父 class 静态方法的行为?我应该完全放弃这样的事情吗?

我考虑了一种策略,我将对某些 ErrorReporter 结构的引用作为对 ScannerToken 结构的依赖注入,但这对我来说似乎很笨拙(我不我觉得错误报告器应该是结构签名的一部分,我错了吗?):

struct Token {
   error_reporter: Rc<ErrorReporter>, // Should I avoid this?
   token_type: token::Type,
   lexeme: String,
   line: u32   
}

如果您需要可视化我所说的模块关系方面的内容,这就是我的项目布局。如有必要,很乐意提供一些源代码。

rlox [package]
└───src
    ├───main.rs (uses scanner + token mods, should contain logic for handling errors)
    ├───lib.rs (just exports scanner and token mods)
    ├───scanner.rs (uses token mod, declares scanner struct and impl)
    └───token.rs (declares token struct and impl)

直译

重要的是,Java 静态方法无法访问任何实例状态。这意味着它可以通过 函数 关联函数 在 Rust 中复制,它们都没有任何状态。唯一的区别在于您如何称呼它们:

fn example() {}

impl Something {
    fn example() {}
}

fn main() {
    example();
    Something::example();
}

查看source you are copying,它没有"just"报错,它有这样的代码:

public class Lox {
  static boolean hadError = false;

  static void error(int line, String message) {
    report(line, "", message);
  }

  private static void report(int line, String where, String message) {
    System.err.println(
        "[line " + line + "] Error" + where + ": " + message);
    hadError = true;
  }
}

我不是 JVM 专家,但我很确定使用这样的静态变量意味着您的代码不再是线程安全的。你根本无法在安全的 Rust 中做到这一点;你不能 "accidentally" 编写内存不安全的代码。

最安全的字面翻译是使用关联函数和原子变量:

use std::sync::atomic::{AtomicBool, Ordering, ATOMIC_BOOL_INIT};

static HAD_ERROR: AtomicBool = ATOMIC_BOOL_INIT;

struct Lox;

impl Lox {
    fn error(line: usize, message: &str) {
        Lox::report(line, "", message);
    }

    fn report(line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {}] Error{}: {}", line, where_it_was, message);
        HAD_ERROR.store(true, Ordering::SeqCst);
    }
}

如果需要,您还可以使用 lazy_static 和 MutexRwLock 选择更丰富的数据结构来存储在全局状态中。

地道翻译

虽然方便,但我觉得这样的设计不好。全局状态简直太糟糕了。我更喜欢使用依赖注入。

定义一个包含您需要的状态和方法的错误报告器结构,并将对错误报告器的引用传递到需要的位置:

struct LoggingErrorSink {
    had_error: bool,
}

impl LoggingErrorSink {
    fn error(&mut self, line: usize, message: &str) {
        self.report(line, "", message);
    }

    fn report(&mut self, line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {} ] Error {}: {}", line, where_it_was, message);
        self.had_error = true;
    }
}

fn some_parsing_thing(errors: &mut LoggingErrorSink) {
    errors.error(0, "It's broken");
}

实际上,我宁愿为允许报告错误的事物定义一个特征并为具体类型实现它。 Rust 使它变得很好,因为使用这些泛型时性能差异为零。

trait ErrorSink {
    fn error(&mut self, line: usize, message: &str) {
        self.report(line, "", message);
    }

    fn report(&mut self, line: usize, where_it_was: &str, message: &str);
}

struct LoggingErrorSink {
    had_error: bool,
}

impl LoggingErrorSink {
    fn report(&mut self, line: usize, where_it_was: &str, message: &str) {
        eprintln!("[line {} ] Error {}: {}", line, where_it_was, message);
        self.had_error = true;
    }
}

fn some_parsing_thing<L>(errors: &mut L)
where
    L: ErrorSink,
{
    errors.error(0, "It's broken");
}

实现这个有很多变体,都取决于您的权衡。

  • 您可以选择让记录器采用 &self 而不是 &mut,这将迫使这种情况使用类似 Cell 的东西来获得 [=20= 的内部可变性].
  • 您可以使用 Rc 之类的东西来避免向调用链添加任何额外的生命周期。
  • 您可以选择将记录器存储为结构成员而不是函数参数。

对于额外的键盘工作,您可以测试错误。只需快速创建一个将信息保存到内部变量并在测试时传入的特性的虚拟实现。

意见,嘿!

a strategy where I inject a reference to some ErrorReporter struct as a dependency into the Scanner

是的,依赖注入是解决大量编码问题的绝佳解决方案。

and Token structs

我不知道为什么 token 需要报告错误,但 tokenizer 这样做是有意义的.

but that seems unwieldy to me. I don't feel like an error reporter should be part of the struct's signature, am I wrong?

我会说是的,你错了;您已将其表述为绝对真理,但在编程中很少存在。

具体来说,很少有人关心inside你的类型是什么,可能只是实现者。构造您的类型的值的人可能会有点在意,因为他们需要传递依赖项,但这是一件 好事 。他们现在知道这个值会产生他们需要处理的错误 "out-of-band",而不是在他们的程序无法运行后阅读一些文档。

还有一些人关心您的类型的实际签名。这是一把双刃剑。为了获得最佳性能,Rust 将强制您在类型签名中公开泛型类型和生命周期。有时,这很糟糕,但要么性能提升是值得的,要么你可以以某种方式隐藏它并承受微小的打击。这就是给你选择的语言的好处。

另见

  • How to synchronize a static variable among threads running different instances of a class in Java?
  • Where are static methods and static variables stored in Java?
  • Static fields in a struct in Rust
  • How can you make a safe static singleton in Rust?
  • How do I create a global, mutable singleton?