#comparable #testing #structures #data #data-structures #comparing #oriented

comparable_helper

一个用于在 Rust 中比较数据结构的库,面向测试

3 个版本

0.5.4 2022 年 10 月 10 日
0.5.3 2022 年 10 月 10 日
0.5.2 2022 年 8 月 7 日

#16#oriented

Download history 5868/week @ 2024-03-14 6341/week @ 2024-03-21 4677/week @ 2024-03-28 6108/week @ 2024-04-04 5774/week @ 2024-04-11 5603/week @ 2024-04-18 5146/week @ 2024-04-25 7478/week @ 2024-05-02 4581/week @ 2024-05-09 5679/week @ 2024-05-16 6219/week @ 2024-05-23 7282/week @ 2024-05-30 6362/week @ 2024-06-06 6336/week @ 2024-06-13 5975/week @ 2024-06-20 4667/week @ 2024-06-27

24,862 每月下载
4 个crate中使用 (通过 comparable)

MIT/Apache

34KB
50

comparable crate 定义了 Comparable trait,以及用于为大多数数据类型自动生成此 trait 实例的 derive 宏。此 trait 的主要目的是提供一个方法,Comparable::comparison,通过该方法,任何支持该 trait 的类型的两个值可以产生它们之间的差异摘要。

请注意,与其它主要在标量集合之间进行数据差异比较的crate不同,comparable 主要考虑了测试。也就是说,生成此类变化描述的目的是为了能够编写测试,以断言在初始状态和结果状态之间的某个操作之后预期的变化集合。这也意味着一些类型,如 HashMap,必须在排序键之后进行差异比较,以便生成的变化集可以是确定性的,从而可以作为测试期望来表示。

为此,还提供了宏 assert_changes!,它接受两个相同类型的值以及由 foo.comparison(&bar) 返回的预期 "变化描述"。此函数在底层使用 pretty_assertions crate,以便在失败输出中轻松查看深结构中的微小差异。

快速入门

如果您想快速使用 Comparable crate 来增强单元测试,请执行以下操作

  1. comparable 库作为依赖项添加,并启用 features = ["derive"]
  2. 在所需的结构体和枚举上派生 Comparable 特性。
  3. 将单元测试结构化,分为三个阶段:a. 创建你打算测试的初始状态或数据集,并对其创建一个副本。b. 将你的操作和更改应用于此状态。c. 使用 assert_changes! 在初始状态和结果状态之间进行断言,以确保发生的事情正好是你预期的。

与通常的“探测”结果状态的方法相比,这种方法的优点在于它针对所有可能的更改进行断言,以确保没有发生你未预期的副作用。因此,它既是一种正向测试,也是一种负向测试:检查你预期的内容以及你不想看到的内容。

Comparable 特性

Comparable 特性有两个关联类型和两个方法,一对对应于 值描述,另一对对应于 值变化

pub trait Comparable {
    type Desc: std::cmp::PartialEq + std::fmt::Debug;
    fn describe(&self) -> Self::Desc;

    type Change: std::cmp::PartialEq + std::fmt::Debug;
    fn comparison(&self, other: &Self) -> comparable::Changed<Self::Change>;
}

描述:Comparable::Desc 关联类型

值描述(Comparable::Desc 关联类型)是必需的,因为值层次结构可能涉及许多类型。也许其中一些类型实现了 PartialEqDebug,但并非所有。为了克服这种限制,Comparable derive 宏创建了一个具有所有相同构造函数和字段的“镜像”数据结构,但其包含的类型使用 Comparable::Desc 关联类型。

# use comparable_derive::*;
#[derive(Comparable)]
struct MyStruct {
  bar: u32,
  baz: u32
}

这生成一个与原始类型相似的描述,但使用类型描述而不是类型本身

struct MyStructDesc {
  bar: <u32 as comparable::Comparable>::Desc,
  baz: <u32 as comparable::Comparable>::Desc
}

你也可以选择一个替代的描述类型,例如值的简化形式或其他类型。例如,复杂结构可以通过它们从 Default 值表示的更改集来描述自己。这是如此常见,以至于通过 comparable 提供的 compare_default 宏属性来支持。

# use comparable_derive::*;
#[derive(Comparable)]
#[compare_default]
struct MyStruct { /* ...lots of fields... */ }

impl Default for MyStruct {
    fn default() -> Self { MyStruct {} }
}

对于标量,Comparable::Desc 类型与它所描述的类型相同,这些被称为“自描述”。

还有一些宏属性可以进一步自定义,这些将在关于 结构体 的部分中进行介绍。

变化:Comparable::Change 关联类型

当同一类型下的两个值不同时,这种差异会使用关联的类型 Comparable::Change 来表示。这些值是由 Comparable::comparison 方法产生的,该方法实际上返回 Changed<Change>,因为结果可能是 Changed::UnchangedChanged::Changed(_changes_)。[^option]

[^option] ChangedOption 类型的另一种风味,创建它的目的是使集合变更比仅仅在各个地方看到 Some 更清晰。

一个 Comparable::Change 值的主要目的是将其与您期望看到的变更集进行比较,因此设计选择是为了优化清晰度和打印,而不是,例如,通过应用变更集将一个值转换成另一个值的能力。在给定数据集和变更描述的情况下,这是完全可能的,但尚未进行任何工作来实现这一目标。

在标量、集合、结构体和枚举之间,变化表示的方式可能大相径庭,因此下面将在讨论这些类型各自的章节中提供更多细节。

标量

Comparable 特性已对所有基本标量类型实现。这些是自我描述的,并使用一个名为 Comparable::Change 的结构体来命名该类型,该结构体包含先前值和更改值。例如,以下断言成立

# use comparable::*;
assert_changes!(&100, &100, Changed::Unchanged);
assert_changes!(&100, &200, Changed::Changed(I32Change(100, 200)));
assert_changes!(&true, &false, Changed::Changed(BoolChange(true, false)));
assert_changes!(
    &"foo",
    &"bar",
    Changed::Changed(StringChange("foo".to_string(), "bar".to_string())),
);

Vec 和 Set 集合

已实现 Comparable 的集合集合包括: VecHashSetBTreeSet

Vec 使用 Vec<VecChange> 来报告所有发生更改的索引。请注意,它无法检测中间的插入操作,因此可能会报告从那里到向量末尾的所有项目已更改,然后报告一个已添加的成员。

HashSetBTreeSet 类型都使用相同的 SetChange 类型来报告更改。请注意,为了使 HashSet 的更改结果具有确定性,HashSet 中的值必须支持 Ord 特性,以便在比较之前对它们进行排序。集合无法知道特定成员是否已更改,因此仅报告以 SetChange::AddedSetChange::Removed 为术语的更改。

以下是一些示例,摘自 comparable_test 测试套件

# use comparable::*;
# use std::collections::HashSet;
// Vectors
assert_changes!(
    &vec![1 as i32, 2],
    &vec![1 as i32, 2, 3],
    Changed::Changed(vec![VecChange::Added(2, 3)]),
);
assert_changes!(
    &vec![1 as i32, 3],
    &vec![1 as i32, 2, 3],
    Changed::Changed(vec![
        VecChange::Changed(1, I32Change(3, 2)),
        VecChange::Added(2, 3),
    ]),
);
assert_changes!(
    &vec![1 as i32, 2, 3],
    &vec![1 as i32, 3],
    Changed::Changed(vec![
        VecChange::Changed(1, I32Change(2, 3)),
        VecChange::Removed(2, 3),
    ]),
);
assert_changes!(
    &vec![1 as i32, 2, 3],
    &vec![1 as i32, 4, 3],
    Changed::Changed(vec![VecChange::Changed(1, I32Change(2, 4))]),
);

// Sets
assert_changes!(
    &vec![1 as i32, 2].into_iter().collect::<HashSet<_>>(),
    &vec![1 as i32, 2, 3].into_iter().collect::<HashSet<_>>(),
    Changed::Changed(vec![SetChange::Added(3)]),
);
assert_changes!(
    &vec![1 as i32, 3].into_iter().collect::<HashSet<_>>(),
    &vec![1 as i32, 2, 3].into_iter().collect::<HashSet<_>>(),
    Changed::Changed(vec![SetChange::Added(2)]),
);
assert_changes!(
    &vec![1 as i32, 2, 3].into_iter().collect::<HashSet<_>>(),
    &vec![1 as i32, 3].into_iter().collect::<HashSet<_>>(),
    Changed::Changed(vec![SetChange::Removed(2)]),
);
assert_changes!(
    &vec![1 as i32, 2, 3].into_iter().collect::<HashSet<_>>(),
    &vec![1 as i32, 4, 3].into_iter().collect::<HashSet<_>>(),
    Changed::Changed(vec![SetChange::Added(4), SetChange::Removed(2)]),
);

请注意,如果上面的第一个 VecChange::Change 使用了索引 1 而不是 0,则导致的失败将类似于以下内容

running 1 test
test test_comparable_bar ... FAILED

failures:

---- test_comparable_bar stdout ----
thread 'test_comparable_bar' panicked at 'assertion failed: `(left == right)`

Diff < left / right > :
 Changed(
     [
         Change(
<            1,
>            0,
             I32Change(
                 100,
                 200,
             ),
         ),
     ],
 )

', /Users/johnw/src/comparable/comparable/src/lib.rs:19:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    test_comparable_bar

Map 集合

已经实现了Comparable特性的映射集合包括:HashMapBTreeMap

它们以相同的方式报告变更,使用MapChange类型。请注意,为了使HashMap的变更结果具有确定性,HashMap中的键必须支持Ord特质,以便在比较之前进行排序。变更以MapChange::AddedMapChange::RemovedMapChange::Changed的形式报告,这与上面的VecChange相同。

以下是一些示例,摘自 comparable_test 测试套件

# use comparable::*;
# use std::collections::HashMap;
// HashMaps
assert_changes!(
    &vec![(0, 1 as i32), (1, 2)].into_iter().collect::<HashMap<_, _>>(),
    &vec![(0, 1 as i32), (1, 2), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    Changed::Changed(vec![MapChange::Added(2, 3)]),
);
assert_changes!(
    &vec![(0, 1 as i32), (1, 2), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    &vec![(0, 1 as i32), (1, 2)].into_iter().collect::<HashMap<_, _>>(),
    Changed::Changed(vec![MapChange::Removed(2)]),
);
assert_changes!(
    &vec![(0, 1 as i32), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    &vec![(0, 1 as i32), (1, 2), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    Changed::Changed(vec![MapChange::Added(1, 2)]),
);
assert_changes!(
    &vec![(0, 1 as i32), (1, 2), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    &vec![(0, 1 as i32), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    Changed::Changed(vec![MapChange::Removed(1)]),
);
assert_changes!(
    &vec![(0, 1 as i32), (1, 2), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    &vec![(0, 1 as i32), (1, 4), (2, 3)].into_iter().collect::<HashMap<_, _>>(),
    Changed::Changed(vec![MapChange::Changed(1, I32Change(2, 4))]),
);

结构体

比较任意结构体是创建comparable的原始动机。这可以通过使用一个Comparable宏来自动生成所需比较的代码来实现。本节旨在解释该宏的工作原理以及各种属性宏如何指导此过程。如果所有其他方法都失败了,手动特质实现始终是一个备选方案。

以下是针对具有多个字段的结构的Change派生通常产生的结果

# use comparable_derive::*;
# use comparable::*;
struct MyStruct {
  bar: u32,
  baz: u32,
}

// The following would be generated by `#[derive(Comparable)]`:

#[derive(PartialEq, Debug)]
struct MyStructDesc {
    bar: <u32 as Comparable>::Desc,
    baz: <u32 as Comparable>::Desc,
}

#[derive(PartialEq, Debug)]
enum MyStructChange {
    Bar(<u32 as Comparable>::Change),
    Baz(<u32 as Comparable>::Change),
}

impl Comparable for MyStruct {
    type Desc = MyStructDesc;

    fn describe(&self) -> Self::Desc {
        MyStructDesc {
            bar: self.bar.describe(),
            baz: self.baz.describe(),
        }
    }

    type Change = Vec<MyStructChange>;

    fn comparison(&self, other: &Self) -> Changed<Self::Change> {
        let changes: Self::Change = vec![
            self.bar.comparison(&other.bar).map(MyStructChange::Bar),
            self.baz.comparison(&other.baz).map(MyStructChange::Baz),
        ]
            .into_iter()
            .flatten()
            .collect();
        if changes.is_empty() {
            Changed::Unchanged
        } else {
            Changed::Changed(changes)
        }
    }
}

有关只有一个字段或没有字段的结构的详细信息,请参阅下面的相关部分。

字段属性:comparable_ignore

您可以应用到单个字段的第一个属性宏是#[comparable_ignore],如果问题的类型无法进行比较差异,则必须使用它。

字段属性:comparable_synthetic

#[comparable_synthetic { <BINDINGS...> }]属性允许您将一个或多个“合成属性”附加到字段上,然后在描述和更改集中将其视为实际字段,就像它们是具有计算值的实际字段一样。以下是一个示例

# use comparable_derive::*;
#[derive(Comparable)]
pub struct Synthetics {
    #[comparable_synthetic {
        let full_value = |x: &Self| -> u8 { x.ensemble.iter().sum() };
    }]
    #[comparable_ignore]
    pub ensemble: Vec<u8>,
}

这个结构有一个包含u8值的向量的ensemble字段。然而,在测试中,只要最终的总和保持不变,我们可能不在乎向量内容的更改。这是通过忽略ensemble字段来实现的,这样它就不会生成或描述,同时创建一个从完整对象派生的合成字段,该字段产生总和。

请注意,comparable_synthetic属性的语法相当特定:一系列简单命名的let绑定,其中每个案例的值是一个完整的闭包,它接受包含原始字段的对象的引用(&Self),并为实现了或派生Comparable的某些类型生成一个值。

为结构体派生ComparableDesc类型

默认情况下,为结构体派生 Comparable 将创建该结构体的“镜像”,包含所有相同的字段,但将每个类型 T 替换为 <T as Comparable>::Desc

# use comparable::*;
struct MyStructDesc {
  bar: <u32 as Comparable>::Desc,
  baz: <u32 as Comparable>::Desc
}

可以使用多个属性宏来影响此过程。

宏属性:self_describing

如果使用 self_describing 属性,则将 Comparable::Desc 类型设置为本身类型,并且 Comparable::describe 方法返回值的克隆。

注意,自描述类型需要以下特质:CloneDebugPartialEq

宏属性:no_description

如果您想为类型完全不提供描述,因为您只关心其变化情况,并且永远不会在其他任何上下文中报告值的描述,则可以使用 #[no_description]。这将 Comparable::Desc 类型设置为 unit,并且相应地调整 Comparable::describe 方法。

type Desc = ();

fn describe(&self) -> Self::Desc {
    ()
}

假设在这种情况下,此类值永远不会出现在任何更改输出中,因此如果您看到许多 unit 出现,请考虑不同的方法。

宏属性:describe_typedescribe_body

通过指定 Comparable::Desc 类型以及 Comparable::describe 函数的主体,您可以更精确地控制描述。基本上,对于以下定义

# use comparable_derive::*;
#[derive(Comparable)]
#[describe_type(T)]
#[describe_body(B)]
struct MyStruct {
  bar: u32,
  baz: u32
}

以下内容将被生成

type Desc = T;

fn describe(&self) -> Self::Desc {
    B
}

这也意味着传递给 describe_body 的表达式参数可以引用 self 参数。以下是一个实际用例

# use comparable_derive::*;
#[cfg_attr(feature = "comparable",
           derive(comparable::Comparable),
           describe_type(String),
           describe_body(self.to_string()))]
struct MyStruct {}

可以使用相同的方法通过其校验和哈希表示大块数据,例如,或者通过其 Merkle 根哈希表示不需要显示的大型数据结构。

宏属性:compare_default

当使用#[compare_default]属性宏时,定义了类型Comparable::Desc,使其与类型Comparable::Change相同,其中Comparable::describe方法通过对比Default::default()的值来实现。

# use comparable::*;
impl comparable::Comparable for MyStruct {
    type Desc = Self::Change;

    fn describe(&self) -> Self::Desc {
        MyStruct::default().comparison(self).unwrap_or_default()
    }

    type Change = Vec<MyStructChange>;

    /* ... */
}

注意,结构体的更改始终是一个向量,因为这允许为每个字段分别报告更改。更多内容将在下一节中介绍。

宏属性:comparable_publiccomparable_private

默认情况下,自动生成的类型Comparable::DescComparable::Change的可见性与它们的父类型相同。但是,如果您希望保持原始数据类型私有,但允许导出描述和更改集,则这可能不合适。为了支持这一点——以及相反的情况——您可以使用#[comparable_public]#[comparable_private]来明确指定这些生成的类型的可见性。

特殊情况:单元结构体

如果一个结构体没有任何字段,它永远不会改变,因此只生成一个单元类型的Comparable::Desc类型。

特殊情况:单例结构体

如果一个结构体只有一个字段(无论是命名的还是未命名的),则使用枚举值向量来记录已更改的内容就不再有意义。在这种情况下,派生过程变得更加简单。

# use comparable_derive::*;
# use comparable::*;
struct MyStruct {
  bar: u32,
}

// The following would be generated by `#[derive(Comparable)]`:

#[derive(PartialEq, Debug)]
struct MyStructDesc {
    bar: <u32 as Comparable>::Desc,
}

#[derive(PartialEq, Debug)]
struct MyStructChange {
    bar: <u32 as Comparable>::Change,
}

impl Comparable for MyStruct {
    type Desc = MyStructDesc;

    fn describe(&self) -> Self::Desc {
        MyStructDesc { bar: self.bar.describe() }
    }

    type Change = MyStructChange;

    fn comparison(&self, other: &Self) -> Changed<Self::Change> {
        self.bar.comparison(&other.bar).map(|x| MyStructChange { bar: x })
    }
}

为结构体派生Comparable:类型Change

对于结构体,默认情况下,派生Comparable将创建一个具有结构体中每个字段变体的枚举,并且它使用这种值的向量来表示更改。这意味着对于以下定义

# use comparable_derive::*;
#[derive(Comparable)]
struct MyStruct {
  bar: u32,
  baz: u32
}

类型Comparable::Change被定义为Vec<MyStructChange>,其中MyStructChange如下所示

#[derive(PartialEq, Debug)]
enum MyStructChange {
    Bar(<u32 as Comparable>::Change),
    Baz(<u32 as Comparable>::Change),
}

impl comparable::Comparable for MyStruct {
    type Desc = MyStructDesc;
    type Change = Vec<MyStructChange>;
}

请注意,如果一个结构体只有一个字段,就没有必要使用向量来指定更改,因为结构体要么没有变化,要么只是那个字段发生了变化。因此,单字段结构体在它们的 Comparable 派生中使用 type Change = [type]Change,而不是像多字段结构体那样使用 type Change = Vec<[type]Change>

以下是一个简化的例子,展示了如何为具有多个字段的结构体断言更改

assert_changes!(
    &initial_foo, &later_foo,
    Changed::Changed(vec![
        MyStructChange::Bar(...),
        MyStructChange::Baz(...),
    ]));

如果字段没有发生变化,它将不会出现在向量中,并且每个字段最多出现一次。采取这种方法的理由是,如果一个结构体有大量的字段,只要大部分字段保持不变,就可以用一个小的更改集来表示。

枚举

枚举的处理方式与结构体大不相同,因为虽然结构体总是字段的乘积,但枚举不仅可以是变体的和,还可以是乘积的和。

为了更好地解释这一点:这里的“字段的乘积”意味着结构体是一个简单的类型化字段的分组,其中相同的字段对于该结构体的每个值都是可用的。

而枚举是一种变体之间的和,或者选择。然而,其中一些变体可以包含字段的组,就像在变体中嵌入了一个无名的结构体。考虑以下 enum

# use comparable_derive::*;
#[derive(Comparable)]
enum MyEnum {
    One(bool),
    Two { two: Vec<bool>, two_more: u32 },
    Three,
}

在这里,我们看到有一个没有字段的变体(Three),一个有未命名字段的变体(One),以及一个具有像通常结构体一样的命名字段的变体(Two)。然而,问题在于这些嵌入的结构体永远不会被表示为独立的类型,因此我们不能为它们定义 Comparable 并仅计算枚举参数之间的差异。我们也不能简单地创建一个带有真实名称的字段类型的副本并为它生成 Comparable,因为不是每个值都是可复制的或可克隆的,并且自动生成一个具有所有引用类型字段的新层次结构变得非常复杂...

因此,以下被生成,这可能有点冗长,但可以捕捉到任何差异的全貌

enum MyEnumChange {
    BothOne(<bool as comparable::Comparable>::Change),
    BothTwo {
        two: Changed<<Vec<bool> as comparable::Comparable>::Change>,
        two_more: Changed<Baz as comparable::Comparable>::Change
    },
    BothThree,
    Different(
        <MyEnum as comparable::Comparable>::Desc,
        <MyEnum as comparable::Comparable>::Desc
    ),
}

请注意,具有单字段变体的不需要使用 Comparable::Change,因为当变体报告为已更改时,该信息已经得到了反映,例如使用 BothOne。在 BothTwo 的情况下,每个字段类型都被包装在 Changed 中,因为可能有一个或两个字段发生了变化。

以下是一个完整的例子,说明了上述枚举的派生内容

# use comparable_derive::*;
# use comparable::*;
enum MyEnum {
    One(bool),
    Two { two: Vec<bool>, two_more: u32 },
    Three,
}

// The following would be generated by `#[derive(Comparable)]`:

#[derive(PartialEq, Debug)]
enum MyEnumDesc {
    One(<bool as Comparable>::Desc),
    Two { two: <Vec<bool> as Comparable>::Desc,
          two_more: <u32 as Comparable>::Desc },
    Three,
}

#[derive(PartialEq, Debug)]
enum MyEnumChange {
    BothOne(<bool as Comparable>::Change),
    BothTwo { two: Changed<<Vec<bool> as Comparable>::Change>,
              two_more: Changed<<u32 as Comparable>::Change> },
    BothThree,
    Different(MyEnumDesc, MyEnumDesc),
}

impl Comparable for MyEnum {
    type Desc = MyEnumDesc;

    fn describe(&self) -> Self::Desc {
        match self {
            MyEnum::One(x) => MyEnumDesc::One(x.describe()),
            MyEnum::Two { two: x, two_more: y } =>
                MyEnumDesc::Two { two: x.describe(),
                                  two_more: y.describe() },
            MyEnum::Three => MyEnumDesc::Three,
        }
    }

    type Change = MyEnumChange;

    fn comparison(&self, other: &Self) -> Changed<Self::Change> {
        match (self, other) {
            (MyEnum::One(x), MyEnum::One(y)) =>
                x.comparison(&y).map(MyEnumChange::BothOne),
            (MyEnum::Two { two: x0, two_more: x1 },
             MyEnum::Two { two: y0, two_more: y1 }) => {
                let c0 = x0.comparison(&y0);
                let c1 = x1.comparison(&y1);
                if c0.is_unchanged() && c1.is_unchanged() {
                    Changed::Unchanged
                } else {
                    Changed::Changed(MyEnumChange::BothTwo {
                        two: c0, two_more: c1
                    })
                }
            }
            (MyEnum::Three, MyEnum::Three) => Changed::Unchanged,
            (_, _) => Changed::Changed(
                MyEnumChange::Different(self.describe(), other.describe()))
        }
    }
}

为枚举派生 ComparableDesc 类型

默认情况下,对于枚举类型,使用 Comparable 继承会创建该结构的“镜像”,具有相同的变体和字段,但将每个类型 T 替换为 <T as Comparable>::Desc

# use comparable::*;
enum MyEnumDesc {
  Bar(<u32 as Comparable>::Desc),
  Baz { some_field: <u32 as Comparable>::Desc }
}

可以使用与结构体相同的属性宏来影响此过程,但需要注意的是,枚举变体的字段上目前尚不支持合成属性。在此上下文中使用此属性目前会被静默忽略。

待办事项:jww(2021-11-01):允许枚举变体中存在合成字段。

为枚举类型继承 ComparableChange 类型

默认情况下,对于枚举类型,使用 Comparable 继承会创建一个相关的 enum,其中原始枚举中的每个变体在 Change 类型中以 Both<Name> 变体表示,并添加了一个名为 Different 的新变体,该变体接受原始枚举的两个描述。

每次比较两个枚举值并且它们具有不同的变体时,都使用 Change 类型的 Different 变体来表示不同值的描述。如果值具有相同的变体,则使用 Both<Variant>

请注意,Both<Variant> 有两种形式:对于具有单个命名或未命名字段的变体,它只是与原始字段类型关联的 Change 类型;对于具有多个命名或未命名字段的变体,每个 Change 类型也被包裹在一个 Changed 结构中,以反映该字段的变体是否已更改。

字段属性:variant_struct_fields

请注意,可以像处理结构体一样处理变体字段,并且可以像上面为结构体那样精确比较它们。这不是默认设置,因为具有命名字段的枚举变体通常比结构体包含的字段少,这会增加更改描述的冗长性,因为始终需要命名这些隐含的结构体。然而,在变体中发现的字段数量很多的情况下,这可以像结构体一样有益。

因此,提供了宏属性 variant_struct_fields 来生成此类转换。例如,它将导致以下代码生成,新 MyEnumTwoChange 类型及其使用与旧类型的主要区别

# use comparable_derive::*;
# use comparable::*;
enum MyEnum {
    One(bool),
    Two { two: Vec<bool>, two_more: u32 },
    Three,
}

// The following would be generated by `#[derive(Comparable)]`:

#[derive(PartialEq, Debug)]
enum MyEnumDesc {
    One(<bool as Comparable>::Desc),
    Two { two: <Vec<bool> as Comparable>::Desc,
          two_more: <u32 as Comparable>::Desc },
    Three,
}

#[derive(PartialEq, Debug)]
enum MyEnumChange {
    BothOne(<bool as Comparable>::Change),
    BothTwo(Vec<MyEnumTwoChange>),
    BothThree,
    Different(MyEnumDesc, MyEnumDesc),
}

#[derive(PartialEq, Debug)]
enum MyEnumTwoChange {
    Two(<Vec<bool> as Comparable>::Change),
    TwoMore(<u32 as Comparable>::Change),
}

impl Comparable for MyEnum {
    type Desc = MyEnumDesc;

    fn describe(&self) -> Self::Desc {
        match self {
            MyEnum::One(x) => MyEnumDesc::One(x.describe()),
            MyEnum::Two { two: x, two_more: y } =>
                MyEnumDesc::Two { two: x.describe(),
                                  two_more: y.describe() },
            MyEnum::Three => MyEnumDesc::Three,
        }
    }

    type Change = MyEnumChange;

    fn comparison(&self, other: &Self) -> Changed<Self::Change> {
        match (self, other) {
            (MyEnum::One(x), MyEnum::One(y)) =>
                x.comparison(&y).map(MyEnumChange::BothOne),
            (MyEnum::Two { two: x0, two_more: x1 },
             MyEnum::Two { two: y0, two_more: y1 }) => {
                let c0 = x0.comparison(&y0);
                let c1 = x1.comparison(&y1);
                let changes: Vec<MyEnumTwoChange> = vec![
                    c0.map(MyEnumTwoChange::Two),
                    c1.map(MyEnumTwoChange::TwoMore),
                ].into_iter().flatten().collect();
                if changes.is_empty() {
                    Changed::Unchanged
                } else {
                    Changed::Changed(MyEnumChange::BothTwo(changes))
                }
            }
            (MyEnum::Three, MyEnum::Three) => Changed::Unchanged,
            (_, _) => Changed::Changed(
                MyEnumChange::Different(self.describe(), other.describe()))
        }
    }
}

特殊情况:空枚举

如果一个枚举没有任何变体,则无法构建它,因此省略了 Comparable::DescComparable::Change 类型,并且始终报告为未更改。

联合

目前不能为联合推导出 Comparable 实例。

依赖关系

~2MB
~44K SLoC