在 Rust 中为泛型类型实现类型安全的工厂方法

Implementing Type-Safe Factory Methods for Generic Types in Rust

提问人:MysteryMoose 提问时间:11/1/2023 更新时间:11/1/2023 访问量:52

问:

这里是 Rust 的新手,尽管我在低级/内核驱动程序/嵌入式 C 上花了相当多的时间,并且对 C++11 和更新版本有一些少量经验。我怀疑我的部分问题将是忘记旧习惯。

我将如何实现一个类型安全的方法,该方法来构造实现给定特征但本身需要泛型参数的对象?

这个项目是一个简单的 MQTT 到 Prometheus 的桥接;这里的目标是能够解析一个配置文件,然后调用每个配置的项目来返回一个对象,MQTT 端稍后可以在发布时使用该对象来更新值(通过特征)。考虑(为简洁起见进行了修剪):register_metricUpdateFromString

enum MetricType { IntegerCounter, FloatCounter, IntegerGauge, FloatGauge }

pub trait UpdateFromString {
    fn update_from_string(&self, value: String);
}

impl UpdateFromString for prometheus::Counter {
    fn update_from_string(&self, value: String) {
        self.inc();
    }
}

// snip - impl UpdateFromString for other Prometheus types we support

struct PrometheusEndpoint { ... }

impl PrometheusEndpoint
{
    fn new(runtime: std::rc::Rc<tokio::runtime::Runtime>) -> Result<PrometheusEndpoint, i32> { ... }

    async fn metrics_handler(registry: std::sync::Arc<prometheus::Registry>) -> Result<impl warp::Reply, std::convert::Infallible> { ... }

    fn register_metric(&self, name: String, desc: String, mtype: MetricType) -> Box<impl UpdateFromString>
    {
        let metric: Box<dyn std::any::Any>;

        // Instantiate raw metric type
        match mtype {
            MetricType::IntegerCounter => metric = Box::new(prometheus::IntCounter::new(name, desc).unwrap()),
            MetricType::IntegerGauge   => metric = Box::new(prometheus::IntGauge::new(name, desc).unwrap()),
            MetricType::FloatCounter   => metric = Box::new(prometheus::Counter::new(name, desc).unwrap()),
            MetricType::FloatGauge     => metric = Box::new(prometheus::Gauge::new(name, desc).unwrap()),
            _ => panic!("Unknown metric type"),
        };

        // Register metric with registry
        // NOTE: After this completes, this metric can potentially be scraped by the remote Prometheus instance
        self.registry.register(metric.clone()).unwrap();

        // We implement UpateFromString on the Prometheus base types above so we can just return them directly.
        // This is safe since Rust won't let us use any functions not specified via our trait so a container
        // object is just redundant.
        metric
    }
}

我定义了自己的输入枚举,以将 Prometheus 隐藏在应用程序的其余部分,我认为这可能是问题的一部分。如果我完全静态地(即)这样做,这是非常微不足道的,但需要将原始 Prometheus 类型泄漏到配置解析逻辑中。它也不仅限于 Prometheus 类型。fn register_metric<T>(&self, name: String, desc: String) -> Box<impl UpdateFromString>

另一种方法是使用动态调度,这是我上面尝试的。也就是说,我不确定如何声明正确的类型以使编译器满意。不知不觉中,我在编译时不知道最终的存储大小似乎很不高兴。SO 帖子似乎表明这应该有效,并且作为 C 的类型安全版本对我的 C 大脑有意义。不幸的是,这仍然在编译时给出未知的大小错误;这是有道理的,因为 Box 至少具有内部类型的大小,现在是可变的。std::any::AnyBox<Any>struct foo myvar = *((struct foo *)malloc(sizeof struct foo))

AFAICT,即使是 / 也不起作用,因为该签名必须是类似的东西,它只会将整个不同大小的类型事物更深一层。RcArcRc<Box<foo>>

那么,如上所述,构造和返回实现给定特征接受泛型参数的受支持类型列表之一的类型安全方法是什么?

最后的一些说明:

  • Prometheus 在注册新指标时需要使用 in。Box
  • 该对象是在 中创建的;使用 是 Prometheus 示例中的复制粘贴(尽管我不完全理解为什么这是必要的)。registrynewBox.clone()
  • 如果可能的话,我真的很想这样做。unsafe
  • 如果可能的话,我宁愿不使用外部板条箱;实施“艰难的方式”对我来说更有启发性。
动态 多态性

评论

0赞 Chayim Friedman 11/1/2023
来自的完整错误是什么?cargo check

答:

1赞 Ivan C 11/1/2023 #1

您想要的是类型擦除。你通过返回一个存在类型来部分地实现它,但要一路走到那里,你必须使用一个动态调度的对象,在某种间接的后面。幸运的是,您已经以 .impl Traitdyn TraitBox

这是我建议你应该是什么样子的:register_metric

    fn register_metric(&self, name: String, desc: String, mtype: MetricType) -> Box<dyn UpdateFromString>
    {
        // Instantiate raw metric type
        let metric = match mtype {
            MetricType::IntegerCounter => Box::new(prometheus::IntCounter::new(name, desc).unwrap()),
            MetricType::IntegerGauge   => Box::new(prometheus::IntGauge::new(name, desc).unwrap()),
            MetricType::FloatCounter   => Box::new(prometheus::Counter::new(name, desc).unwrap()),
            MetricType::FloatGauge     => Box::new(prometheus::Gauge::new(name, desc).unwrap()),
            _ => panic!("Unknown metric type"),
        };

        // Register metric with registry
        // NOTE: After this completes, this metric can potentially be scraped by the remote Prometheus instance
        self.registry.register(metric.clone()).unwrap();

        // We implement UpateFromString on the Prometheus base types above so we can just return them directly.
        // This is safe since Rust won't let us use any functions not specified via our trait so a container
        // object is just redundant.
        metric
    }

评论

0赞 MysteryMoose 11/1/2023
不幸的是,编写的示例仍然没有为我编译。我得到这似乎是一种类似的类型混淆形式(并且是它们各自/实现的基本类型)。我注意到,无论我首先放入块中的哪个子句,都将是错误消息中的预期类型。这难道不表明 Rust 仍然对内部类型不满意吗?还是应该让 Rust 只考虑由 trait 定义的 vtable 部分?expected 'Box<GenericCounter<AtomicU64>>', found 'Box<GenericGauge<AtomicI64>>GenericCounterGenericGaugeintfloatmatchdyn
0赞 Ivan C 11/1/2023
啊,是的,胁迫有一些粗糙的边缘。我认为像第一个分支一样添加明确的强制应该就足够了。dynBox::new(... as dyn _)
1赞 Michael Anderson 11/1/2023
您可以以另一种方式解决该错误消息。let metric: Box<dyn UpdateFromString> = match {...
0赞 MysteryMoose 11/2/2023
我无法让显式强制起作用,但显式键入度量变量就奏效了。