首页 文章

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

提问于
浏览
0

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

我希望错误能够在任何子模块中发生,并且它们在 main 模块中是"reported"(这是在本书中使用Java完成的,只需在包含类的类上调用静态方法来打印违规令牌和线) . 但是,如果发生错误,我不能像 Result::Err 那样提早返回(我认为,这是处理Rust中错误的惯用方法),因为解释器应该继续运行 - 不断寻找错误 .

有没有(惯用)方式来模拟从Rust中的子类中使用模块调用父类的静态方法的Java行为?我应该完全抛弃这样的东西吗?

我想到了一个策略,我将一些 ErrorReporter 结构的引用作为依赖注入到 ScannerToken 结构中,但这对我来说似乎不实用(我不是't feel like an error reporter should be part of the struct'签名,我错了吗?):

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)

1 回答

  • 6

    直译

    重要的是,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中做到这一点;你不能“意外”制造内存不安全的代码 .

    这种安全的最直接的翻译将使用相关的函数和原子变量:

    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");
    }
    

    有很多实现这一点的变体,都取决于你的权衡 .

    • 你可以选择让 Logger 取 &self 而不是 &mut ,这会强制这种情况使用类似Cell的内容来获得 had_error 的内部可变性 .

    • 您可以使用Rc这样的东西来避免为调用链添加任何额外的生命周期 .

    • 您可以选择将 Logger 存储为struct成员而不是函数参数 .

    对于额外的键盘工作,您可以获得测试错误的好处 . 简单地创建一个特性的虚拟实现,它将信息保存到内部变量并在测试时传递给它 .

    意见,啊!

    一种策略,我将一些ErrorReporter结构的引用作为依赖项注入Scanner

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

    和令牌结构

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

    但这对我来说似乎很笨拙 . 我不觉得错误记者应该是结构签名的一部分,我错了吗?

    我会说是的,你错了;你已经说过这是一个绝对的事实,其中很少有编程存在 .

    具体而言,很少有人关心你的类型内部,可能只是作为实现者 . 构造你的类型值的人可能会关心一点,因为他们需要传递依赖关系,但这是一件好事 . 他们现在知道这个值可能会产生他们需要处理"out-of-band"的错误,而不是在他们的程序不起作用之后读取一些文档 .

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

    另见

相关问题