#comparable #testing #comparison #traits #difference #change #data

comparable_test

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

#1028开发工具

每月下载量 44 次

MIT/Apache

61KB
826

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

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

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

快速入门

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

  1. comparable crate 添加为依赖项,并启用 features = ["derive"]
  2. 在所需的struct和enum结构上派生出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派生宏创建了一个与你的数据结构相同的“镜像”,具有所有相同的构造函数和字段,但使用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 derive 宏来实现,该宏会自动生成所需比较的代码。本节的目的在于解释这个宏的工作原理,以及可以用来指导过程的各个属性宏。如果所有其他方法都失败了,手动特性行为实现始终是一个替代方案。

下面是一个具有多个字段的结构的 Change derive 的典型输出

# 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 {
    ()
}

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

宏属性: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:更改类型

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

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

枚举

枚举与结构体的处理方式相当不同,原因在于虽然结构体总是字段的乘积,但枚举可以是变体的总和——也可以是乘积的总和。

稍作解释:所谓的“字段乘积”,指的是一种结构体(struct)是同类型字段的简单分组,其中相同的字段对这种结构体的每个值都是可用的。

同时,枚举(enum)是一个选择,或称为总和,在各个变体之间。然而,其中一些变体本身可以包含字段组,就像在变体中嵌入了一个未命名的结构体。考虑以下枚举(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()))
        }
    }
}

为枚举推导Comparable:类型Desc

默认情况下,为枚举推导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创建了一个相关的枚举类型,其中每个原始变体都由一个在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::Desc`或`Comparable::Change`类型都被省略,并且总是报告为未更改。

联合体

目前,联合体不能派生`Comparable`实例。

没有运行时依赖