如何创建具有计时时区的通用 Rust 结构?

Posted

技术标签:

【中文标题】如何创建具有计时时区的通用 Rust 结构?【英文标题】:How do I create a generic Rust struct with a chrono time zone? 【发布时间】:2021-10-24 08:47:15 【问题描述】:

免责声明:我是 Rust 的新手(以前的经验是 Python、TypeScript 和 Go,按顺序排列),我完全有可能遗漏了一些非常明显的东西。

我正在尝试构建一个 Rust 时钟接口。我的基本目标是我有一个报告实际时间的小时钟结构,以及一个报告伪造版本以供测试的存根版本。请注意,这些是历史测试而不是单元测试:我的目标是重放历史数据。我认为部分问题也可能是我对chrono的理解不够好。这显然是一个很棒的库,但我在 chronochrono_tz 中的类型与实例关系方面遇到了问题。

无论如何,这就是我所拥有的:

use chrono::DateTime, TimeZone, Utc;

/// A trait representing the internal clock for timekeeping requirements.
/// Note that for some testing environments, clocks may be stubs.
pub trait Clock<Tz: TimeZone> 
  fn now() -> DateTime<Tz>;

我的最终目标是让其他结构在特定时区拥有dyn Clock。该时钟可能是系统时钟(具有适当的时区转换),也可能是某种存根。

这是我对系统时钟 shim 的尝试,但一切都出现了可怕的错误:

/// A clock that reliably reports system time in the requested time zone.
struct SystemClock<Tz: TimeZone> 
  time_zone: std::marker::PhantomData<*const Tz>,

impl<Tz: TimeZone> Clock<Tz> for SystemClock<Tz> 
  /// Return the current time.
  fn now() -> DateTime<Tz> 
    Utc::now().with_timezone(&Tz)
  

关键问题是Utc::now().with_timezone(&amp;Tz)。编译器需要一个,而不是类型。很公平,除了chronochrono_tz 似乎没有时区值。我一直在寻找合适的东西放在这里,但似乎没有什么是正确的答案。

【问题讨论】:

【参考方案1】:

问题是时区 type 不足以实现指定的now()。大多数时区没有实现为单独的类型,Utc 在这方面实际上是特殊的(Local 也是如此)。正常时区被实现为更一般的时区类型的,例如FixedOffsetchrono_tz::Tz。这些类型在运行时存储时区偏移量,因此有效的FixedOffsets 包括FixedOffset::east(1)(CET)、FixedOffset::west(5)(EST),甚至FixedOffset::east(0)(GMT、UTC)。这就是为什么DateTime::with_timezone() 需要一个具体的时区值,而不仅仅是它的类型。

解决此问题的最简单方法是修改 now() 以接受时区值:

pub trait Clock<Tz: TimeZone> 
    fn now(tz: Tz) -> DateTime<Tz>;


struct SystemClock<Tz: TimeZone> 
    time_zone: std::marker::PhantomData<*const Tz>,


impl<Tz: TimeZone> Clock<Tz> for SystemClock<Tz> 
    fn now(tz: Tz) -> DateTime<Tz> 
        Utc::now().with_timezone(&tz)
    

用法如下所示:

fn main() 
    // now in Utc
    println!(":?", SystemClock::now(Utc));
    // now in GMT+1
    println!(":?", SystemClock::now(FixedOffset::east(1)));
    // now in Copenhagen time
    println!(
        ":?",
        SystemClock::now("Europe/Copenhagen".parse::<chrono_tz::Tz>().unwrap())
    );

特别注意第二个和最后一个示例,其中时区是在运行时选择的,显然没有被时区类型捕获。

如果您发现在 now() 等 trait 方法中指定时区值是多余的,您可以让这些方法访问 self 并将时区值保留在 SystemClock 的字段中(这也将很好地消除PhantomData):

pub trait Clock<Tz: TimeZone> 
    fn now(&self) -> DateTime<Tz>;


struct SystemClock<Tz: TimeZone> 
    time_zone: Tz,


impl SystemClock<Utc> 
    fn new_utc() -> SystemClock<Utc> 
        SystemClock  time_zone: Utc 
    


impl<Tz: TimeZone> SystemClock<Tz> 
    fn new_with_time_zone(tz: Tz) -> SystemClock<Tz> 
        SystemClock  time_zone: tz 
    


impl<Tz: TimeZone> Clock<Tz> for SystemClock<Tz> 
    fn now(&self) -> DateTime<Tz> 
        Utc::now().with_timezone(&self.time_zone)
    


fn main() 
    println!(":?", SystemClock::new_utc().now());
    println!(":?", SystemClock::new_with_time_zone(FixedOffset::east(1)).now());
    // ...

Playground

对于在编译时已知偏移量的时区,例如UtcLocal,时区字段将不占用空间,SystemClock 的大小为零,就像您原来的一样设计。对于在运行时选择偏移量的时区,SystemClock 会将该信息存储在结构中。

最后,随着 const 泛型的出现,可以想象FixedOffset 的一种变体,它在编译时将偏移量存储为 const 泛型。此类类型由 chrono-simpletz crate 提供,您可以使用它来创建您最初想要的 Clock 特征。由于它的类型是在编译时完全指定的,它们实现了Default,因此您可以使用Tz::default() 轻松获得时区值。结果(遗憾的是再次需要PhantomData)可能如下所示:

use std::marker::PhantomData;

use chrono::DateTime, TimeZone, Utc;
use chrono_simpletz::UtcZst, known_timezones::UtcP1;

type UtcP0 = UtcZst<0, 0>;  // chrono_simpletz doesn't provide this

pub trait Clock<Tz: TimeZone + Default> 
    fn now() -> DateTime<Tz>;


struct SystemClock<Tz: TimeZone> 
    time_zone: PhantomData<fn() -> Tz>,


impl<Tz: TimeZone + Default> Clock<Tz> for SystemClock<Tz> 
    fn now() -> DateTime<Tz> 
        Utc::now().with_timezone(&Tz::default())
    


fn main() 
    println!(":?", SystemClock::<UtcP0>::now());
    println!(":?", SystemClock::<UtcP1>::now());

如果在生产中选择哪个选项不明显,我推荐第二个,即带有游乐场链接的那个。

【讨论】:

不是OP,但答案很明确,谢谢。每当我需要使用日期时间时,我都会花很长时间来了解它们,所以它会被添加到书签中。 感谢您提供如此详细的答案。我将在今晚尝试实施,并在我这样做后正式接受。谢谢!

以上是关于如何创建具有计时时区的通用 Rust 结构?的主要内容,如果未能解决你的问题,请参考以下文章

Rust:从(仅)<T> 不同的函数返回通用结构

通用时区:你应该知道的数据库时区知识

Rust 无法推断通用特征 impl 的返回类型

通用时区:你应该知道的数据库时区知识

如何使我的 Rust 函数更通用和高效?

如何获取 pytz 时区的通用名称,例如。美国/纽约的 EST/EDT