#数据结构 #测试 #变更 #比较 #宏派生 #单元测试 #特质

comparable

一个用于在 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 日

#160开发工具

Download history 6107/week @ 2024-04-21 5158/week @ 2024-04-28 6300/week @ 2024-05-05 5796/week @ 2024-05-12 5230/week @ 2024-05-19 6790/week @ 2024-05-26 7614/week @ 2024-06-02 6226/week @ 2024-06-09 5865/week @ 2024-06-16 5877/week @ 2024-06-23 6951/week @ 2024-06-30 8183/week @ 2024-07-07 5526/week @ 2024-07-14 5375/week @ 2024-07-21 4433/week @ 2024-07-28 5708/week @ 2024-08-04

21,562 每月下载量
3 包中(直接使用 2 个)

MIT/Apache

93KB
780

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

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

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

快速入门

如果您想快速开始使用Comparable crate来增强单元测试,请按照以下步骤操作

  1. comparable crate添加为依赖项,启用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

映射集合

已经实现了 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>,
}

此结构有一个 ensemble 字段,包含一个 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将创建一个具有结构体中每个字段变体的enum,它使用这种值的向量来表示更改。这意味着对于以下定义

# 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):允许枚举变体中存在合成字段。

为枚举类型派生 Comparable:`Change` 类型

默认情况下,对于枚举类型,使用 Comparable 派生操作会创建一个相关的 enum,其中原始枚举中的每个变体都由 Both<Name> 变体在 Change 类型中表示,并添加了一个名为 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 实例。

依赖关系

~2.5MB
~51K SLoC