是否可以通过委托给结构成员来创建一个实现 Ord 的宏?

Haf*_*fix 4 rust rust-macros

我有一个结构:

struct Student {
    first_name: String,
    last_name: String,
}
Run Code Online (Sandbox Code Playgroud)

我想创建一个Vec<Student>可以按last_name. 我需要实现OrdPartialOrd并且PartialEq

use std::cmp::Ordering;

impl Ord for Student {
    fn cmp(&self, other: &Student) -> Ordering {
        self.last_name.cmp(&other.last_name)
    }
}

impl PartialOrd for Student {
    fn partial_cmp(&self, other: &Student) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl PartialEq for Student {
    fn eq(&self, other: &Student) -> bool {
        self.last_name == other.last_name
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您有很多结构体都有明显的字段可供排序,这可能会非常单调和重复。是否可以创建一个宏来自动实现这一点?

就像是:

impl_ord!(Student, Student.last_name)
Run Code Online (Sandbox Code Playgroud)

我发现自动实现 Rust newtypes (tuple structs with one field) 的封闭类型特征,但这并不是我想要的。

Luk*_*odt 6

是的,你可以,但首先:请阅读为什么你不应该


为什么不?

当一个类型实现OrdPartialOrd意味着该类型具有自然排序时,这反过来意味着实现的排序是唯一的逻辑顺序。取整数:3 自然小于 4。当然还有其他有用的排序。您可以使用倒序对整数进行降序排序,但只有一个自然数。

现在你有一个由两个字符串组成的类型。有自然排序吗?我声明:不!有很多有用的排序方式,但是按姓氏排序是否比按名字排序更自然?我不这么认为。

那怎么办呢?

还有另外两种排序方法:

两者都可以让您修改排序算法比较值的方式。按姓氏排序可以这样完成(完整代码):

students.sort_by(|a, b| a.last_name.cmp(&b.last_name));
Run Code Online (Sandbox Code Playgroud)

这样,您可以指定如何对每个方法调用进行排序。有时您可能想按姓氏排序,有时您想按名字排序。由于没有明显且自然的排序方式,因此您不应将任何特定的排序方式“附加”到类型本身。

但说真的,我想要一个宏...

当然,在 Rust 中可以编写这样的宏。一旦你理解了宏系统,这实际上很容易。但是让我们不要以您的Student示例为例,因为 - 正如我希望您现在理解的那样 - 这是一个坏主意。

什么时候是个好主意?当语义上只有一个字段是类型的一部分时。以这个数据结构为例:

struct Foo {
    actual_data: String,
    _internal_cache: String,
}
Run Code Online (Sandbox Code Playgroud)

在这里,在_internal_cache语义上不属于您的类型。这只是一个实现细节,因此应该忽略Eqand Ord。简单的宏是:

macro_rules! impl_ord {
    ($type_name:ident, $field:ident) => {
        impl Ord for $type_name {
            fn cmp(&self, other: &$type_name) -> Ordering {
                self.$field.cmp(&other.$field)
            }
        }

        impl PartialOrd for $type_name {
            fn partial_cmp(&self, other: &$type_name) -> Option<Ordering> {
                Some(self.cmp(other))
            }
        }

        impl PartialEq for $type_name {
            fn eq(&self, other: &$type_name) -> bool {
                self.$field == other.$field
            }
        }

        impl Eq for $type_name {}
    }
}
Run Code Online (Sandbox Code Playgroud)

你问我为什么把这么大的代码称为简单?好吧,这些代码的绝大部分正是您已经编写的内容:impls. 我执行了两个简单的步骤:

  1. 在您的代码周围添加宏定义并考虑我们需要哪些参数(type_namefield
  2. 替换所有提及的Student$type_name和所有提及的last_name$field

这就是为什么它被称为“示例宏”的原因:您基本上只是编写普通代码作为示例,但可以使每个参数的一部分可变。

您可以在这里测试整个过程。