如何在 Rust 中干净地实现"waterfall"逻辑



我有一个类型集合,代表我的数据模式的各种旧版本和新版本:

struct Version1;
struct Version2;
struct Version3;
struct Version4;

这些类型可以在彼此之间迁移,每次一个:

impl Version1 { fn migrate_to_v2(self) -> Version2 { Version2 } }
impl Version2 { fn migrate_to_v3(self) -> Version3 { Version3 } }
impl Version3 { fn migrate_to_v4(self) -> Version4 { Version4 } }

我有一个包含所有这些版本的枚举,这是我从磁盘读取的数据格式:

enum Versioned {
V1(Version1),
V2(Version2),
V3(Version3),
V4(Version4),
}

我想写一个函数,执行Versioned对象到Version4的完全迁移。有很多方法可以做到这一点,但它们都有明显的缺陷;我的问题是,有没有其他我忽略的解决方法?

  • 直接调用方法。这里的问题是指数膨胀;当我在这个产品的生命周期中添加更多版本时,这段代码的大小将以二次增长:
fn migrate_versioned(versioned: Versioned) -> Version4 {
match versioned {
V1(data) => data.migrate_to_v2().migrate_to_v3().migrate_to_v4(),
V2(data) => data.migrate_to_v3().migrate_to_v4(),
V3(data) => data.migrate_to_v4(),
V4(data) => data,
}
}
  • 使用一系列连锁if let。这需要通过枚举类型进行往返,并且我们失去了match的耗尽性检查:
fn migrate_versioned(mut versioned: Versioned) -> Version4 {
use Versioned::*;
if let V1(data) = versioned {
versioned = V2(data.migrate_to_v2());
}
if let V2(data) = versioned {
versioned = V3(data.migrate_to_v3());
}
if let V3(data) = versioned {
versioned = V4(data.migrate_to_v4());
}
if let V4(data) = versioned {
return data;
}
unreachable!();
}
  • 使用loop。这恢复了if let版本中丢失的耗尽性检查,但我们失去了语法停止保证,并且仍然有if let的其他缺陷:
fn migrate_versioned(mut versioned: Versioned) -> Version4 {
loop {
versioned = match versioned {
V1(data) => V2(data.migrate_to_v2()),
V2(data) => V3(data.migrate_to_v3()),
V3(data) => V4(data.migrate_to_v4()),
V4(data) => break data,
};
}
}
  • 一些连锁性状。这比其他的要好,但是非常沉重的样板文件:
trait ToV2: Sized {
fn migrate_to_v2(self) -> Version2;
}
impl ToV2 for Version1 { ... }
trait ToV3: Sized {
fn migrate_to_v3(self) -> Version3;
}
impl ToV3 for Version2 { ... }
impl<T: ToV2> ToV3 for T {
fn migrate_to_v3(self) -> Version3 { self.migrate_to_v2().migrate_to_v3() }{
}
trait ToV4: Sized {
fn migrate_to_v4(self) -> Version4;
}
impl ToV4 for Version3 { ... }
impl<T: ToV3> ToV4 for T {
fn migrate_to_v4(self) -> Version3 { self.migrate_to_v3().migrate_to_v4() }{
}
fn migrate_versioned(versioned: Versioned) -> Version4 {
match versioned {
V1(data) => data.migrate_to_v4(),
V2(data) => data.migrate_to_v4(),
V3(data) => data.migrate_to_v4(),
V4(data) => data,
}
}
  • 各种基于宏的解决方案。一般来说,它们使用宏来产生嘈杂的解决方案之一(例如trait版本或直接调用版本)。由于使用宏重复规则(据我所知,这需要一个复杂的递归宏)创建链接调用的棘手之处,这些调用往往非常嘈杂,以至于不能有意地说它是一个复杂性节省器:https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=c8b8bed94afe654bbee7cfabfe24c784.

这里有我错过的解决方案吗?从概念上讲,这样做似乎很简单:

// V1 starts here
let v2 = v1.migrate_to_v2();
// V2 starts here
let v3 = v2.migrate_to_v3();
// V3 starts here
let v4 = v3.migrate_to_v4();
// v4 starts here

但是我一点也不清楚如何(如果有的话)表达一个看起来如此简单的解决方案的控制流。

一种方法是一对特征。第一个性状决定如何进入下一个版本,第二个性状将完成这个链。

pub trait Migrate {
type NextVersion;
fn migrate(self) -> Self::NextVersion;
}
pub trait MigrateToLatest {
type Latest;
fn migrate_to_latest(self) -> Self::Latest;
}
/// If the next version knows how to MigrateToLatest, call their implementation 
impl<M: Migrate> MigrateToLatest for M
where
M::NextVersion: MigrateToLatest,
{
type Latest = <M::NextVersion as MigrateToLatest>::Latest;
#[inline]
fn migrate_to_latest(self) -> Self::Latest {
self.migrate().migrate_to_latest()
}
}
/// Start the waterfall for the LatestVersion type alias (below).
impl MigrateToLatest for LatestVersion {
type Latest = Self;
fn migrate_to_latest(self) -> Self::Latest {
self
}
}

那么我们只需要在每次发布新版本时为以前的版本实现Migrate

impl Migrate for Version1 {
type NextVersion = Version2;
fn migrate(self) -> Self::NextVersion {
Version2
}
}
impl Migrate for Version2 {
type NextVersion = Version3;
fn migrate(self) -> Self::NextVersion {
Version3
}
}
impl Migrate for Version3 {
type NextVersion = Version4;
fn migrate(self) -> Self::NextVersion {
Version4
}
}

当一个新版本是版本,唯一需要做的改变是更新LatestVersion和添加一个新的Migrate从以前的版本。

/// Declare the latest version which ends the waterfall
type LatestVersion = Version4;

这种方法意味着我们只需要处理最新的版本,而不需要自己写出整个链。

您可能还会发现在宏中编写Versioned枚举很有帮助,因为您可能需要对每个变体执行一些统一的操作。

macro_rules! make_versioned {
($($name:ident: $type:ty),+) => {
enum Versioned {
/// Define the elements of the macro in the enum
$($name($type)),+
}

impl MigrateToLatest for Versioned {
type Latest = LatestVersion;

fn migrate_to_latest(self) -> Self::Latest {
use Versioned::*;
match self {
// Take advantage of the macro to easily call on every variant
$($name(x) => x.migrate_to_latest()),+
}
}
}
}
}
/// Define the elements in the Versioned enum
make_versioned! {
V1: Version1,
V2: Version2,
V3: Version3,
V4: Version4
}

相关内容

  • 没有找到相关文章

最新更新