#comparable #testing #derive #change #enums #structures #data

comparable_derive

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

11 个版本

0.5.4 2022 年 10 月 10 日
0.5.3 2022 年 10 月 10 日
0.5.2 2022 年 8 月 7 日
0.5.1 2021 年 11 月 23 日
0.1.0 2021 年 11 月 1 日

#157 in #change

Download history 6122/week @ 2024-04-04 5780/week @ 2024-04-11 5640/week @ 2024-04-18 5166/week @ 2024-04-25 7454/week @ 2024-05-02 4582/week @ 2024-05-09 5676/week @ 2024-05-16 6221/week @ 2024-05-23 7299/week @ 2024-05-30 6379/week @ 2024-06-06 6332/week @ 2024-06-13 6014/week @ 2024-06-20 5703/week @ 2024-06-27 8528/week @ 2024-07-04 6748/week @ 2024-07-11 3759/week @ 2024-07-18

25,924 每月下载量
4 个 crate 中使用 (通过 comparable)

MIT/Apache

68KB
1K SLoC

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 crate添加为依赖项,并启用features = ["derive"]
  2. 在所需的结构体和枚举上派生Comparable trait。
  3. 将单元测试结构为以下三个阶段:a. 创建你打算测试的初始状态或数据集,并对其创建一个副本。b. 对此状态应用你的操作和更改。c. 使用assert_changes!在初始状态和结果状态之间断言,确保发生的事情正是你所期望的。

与传统的“探测”结果状态的常用方法相比,这种方法的主要优点是它断言了所有可能的更改集合,以确保没有发生你意料之外的不希望出现的副作用。因此,它既是一种正面测试,也是一种负面测试:检查你所期望看到的以及你不想看到的。

Comparable trait

Comparable trait有两个关联类型和两个方法,一对对应于值描述,另一对应于值更改

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 derive 宏来实现,该宏自动生成此类比较所需的代码。本节的目的在于解释该宏的工作原理以及可用于指导过程的各个属性宏。如果其他方法都不可行,手动实现特质始终是一个备选方案。

以下是一个结构体具有多个字段时派生 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 字段。然而,在测试中,我们可能不在乎向量内容的更改,只要最终的总和保持相同即可。这是通过忽略该字段,使其根本不会生成或描述,同时创建一个从完整对象派生的合成字段来实现的,该字段产生总和。

请注意,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 类型设置为单元类型,并且相应地实现 Comparable::describe 方法

type Desc = ();

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

假设在这种情况下,这些值永远不会出现在任何更改输出中,因此如果看到大量单元出现,请考虑不同的方法。

宏属性: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 })
    }
}

为结构体派生 ComparableChange 类型

对于结构体,默认情况下派生 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(...),
    ]));

如果一个字段没有被更改,它不会出现在向量中,每个字段最多出现一次。采取这种方法的理由是,具有许多字段的许多结构体可以通过一个小的更改集来表示,如果大多数其他字段保持不变。

枚举

枚举的处理方式与结构体截然不同,原因在于结构体总是字段的组合产物,而枚举可以不仅仅是变体的总和——它还可以是多个组合产物的总和。

为了更好地理解这一点:“字段的组合产物”意味着结构体是一个简单地将类型化的字段分组,其中相同的字段适用于这种结构体的每一个值。

与此同时,枚举是一种变体的总和或选择。然而,其中一些变体可以包含字段的组合,就像变体中嵌入了一个无名的结构体。考虑以下枚举:

# 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 会创建一个相关的枚举,其中原始枚举的每个变体在 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实例。

依赖项

~1.5MB
~36K SLoC