将函数重载(通过特征)引入作用域

Bring function overload (via trait) into scope

我正在尝试 重载 我的 class 的成员函数(类似于 C++ 中可以完成的操作)。所以我在 Rust 中读到必须使用 traits 来实现这一点。下面是一些示例代码(注意,这只是为了演示这个想法):

/* my_class.rs */
pub struct MyClass {
    pub a: i32,
    pub b: i32,
}

pub trait Func<T> {
    fn func(&self, t: T) -> Self;
}

impl Func<i32> for MyClass {
    fn func(&self, t: i32) -> MyClass {
        MyClass { a: self.a + t, b: self.b }
    }
}

impl Func<&str> for MyClass {
    fn func(&self, t: &str) -> MyClass {
        MyClass { a: self.a, b: self.b + t.parse::<i32>().unwrap() }
    }
}

/* main.rs */
mod my_class;
use crate::my_class::MyClass;
use crate::my_class::Func;

fn main() {
    let m1 = MyClass {a: 10, b:20}.func(5);
    let m2 = MyClass {a: 10, b:20}.func("-8");
    println!("a={}, b={}", m1.a, m1.b);
    println!("a={}, b={}", m2.a, m2.b);
}

首先,这是重载 class 成员函数的正确方法吗?这似乎有点麻烦,因为需要为每个函数重载添加样板 pub trait Func<T>

其次,有没有办法让我不必为每个特征都写 use crate::my_class::Func;?也就是说,当我导入 MyClass?

时,如何将 MyClass 的所有函数(均通过 impl MyClassimpl Func<T> for MyClass 定义)纳入范围

问题“这是重载 class-成员函数的正确方法吗”有点误导。您拥有的代码有效,并且可能是您在当前 Rust 中可以获得的最优雅的代码。

但可能有比重载更好的方法,我猜你会通过更标准地使用 traits 得到更好的服务。

我假设 API 的意图是调用 .func("5") 与调用 .func(5).

做同样的事情

如果是这样的话,您可能想要定义一个特征来表示“可以传递给 func 的东西”。例如,我们称它为 IntLike,我们将为“看起来大致像整数的东西”实现它:

trait IntLike {
  fn to_int(self) -> i32;  // consume self, return an i32
}

impl IntLike for i32 {
  fn to_int(self) -> i32 { self }
}

impl IntLike for &'static str {
  fn to_int(self) {
    self.parse::<i32>().unwrap()
  }
}

然后你可以改变你的函数来接受任何 IntLike:

impl MyClass {
  fn func(&self, i: impl IntLike) {
    let i: i32 = i.to_int();
    // rest of the function body
  }
}

这有很多好处:

  • 逻辑只写一次。出错的机会更少,实现之间差异的可能性为零
  • 您的代码的使用者可以使他们的自定义数据类型更轻松地与您的函数一起使用(只需在他们的类型上实现 IntLike
  • 更加地道,有经验的 Rust 程序员会对它的工作原理有更直观的理解

并且由于 Rust 的单态泛型,它同样快

如果你想模拟完整的函数重载,那么是的,traits 是可行的方法。如果您不想导入它们,您可以将所有相关特征放在与结构相同的模块中,然后使用 use crate::my_class::*.

导入它们

但不要这样。有很多很多原因说明为什么 C++/Java-style 函数重载不是好主意:

  1. 令人困惑。当然,您可以想出不会混淆的示例,但是很多重载函数会根据参数的类型做截然不同的事情,在这一点上,为什么不创建一个新函数呢?在您的示例中,.func(5)5 添加到 a,而 .func("5")5 添加到 b,[=81] 是非常不直观的=]
  2. 给来电者带来不必要的负担。想象一下,您正在编写一个函数 foo,它接收一些可以传递给 funcT。你会如何绑定它?它看起来像这样:
fn foo<T>
where
    MyClass: Func<T>
{ unimplemented!() }

这已经有点丑了。现在假设您有一个 MyClass2,它有一个重载函数,该函数也接受一个 int-like 值(i32&str 可以解析为 i32) .您的绑定现在看起来像这样:

fn foo<T>
where
    MyClass: Func<T>,
    MyClass2: Func<T>
{ unimplemented!() }

尽管它们在概念上是 相同的边界 int-like 值。当添加更多泛型和重载时,它只会变得越来越丑陋。

  1. 它不能扩展到多个参数。假设您想要一个函数,它接收一个值以添加到 a,以及一个值以添加到 b。您现在需要 4 个实现:
fn func(&self, t1: i32, t2: i32) -> Self;
fn func(&self, t1: i32, t2: &str) -> Self;
fn func(&self, t1: &str, t2: i32) -> Self;
fn func(&self, t1: &str, t2: &str) -> Self;

如果你也想支持 i64 怎么办?现在你有 9 个实现。如果你想添加第三个参数?现在你有 27.

所有这些问题都源于这样一个事实,即从概念上讲,在 参数 而不是 函数。因此,编写代码以匹配概念并将特征绑定到参数而不是函数上。它可以防止混淆执行根本不同操作的重载,减轻调用者的使用负担,并阻止实现呈指数爆炸式增长。最重要的是,甚至不需要为要使用的方法导入特征。考虑一下:

/* my_class.rs */
pub struct MyClass {
    pub a: i32,
    pub b: i32,
}

pub trait IntLike {
    fn to_i32(self) -> i32;
}

impl IntLike for i32 {
    fn to_i32(self) -> i32 { self }
}

impl IntLike for &str {
    fn to_i32(self) -> i32 { self.parse().unwrap() }
}

impl MyClass {
    pub fn func<T: IntLike>(&self, t: T) -> Self {
        Self { a: self.a + t.to_i32(), b: self.b }
    }
}

/* main.rs */
mod my_class;
// don't even need to import the trait
use crate::my_class::MyClass;

fn main() {
    let m1 = MyClass {a: 10, b:20}.func(5);
    let m2 = MyClass {a: 10, b:20}.func("-8");
    println!("a={}, b={}", m1.a, m1.b);
    println!("a={}, b={}", m2.a, m2.b);
}

那不是更好吗?


附录

body 的 body 中忽略了传统函数重载不是一个好主意的原因有很多,因为离题了。这里还有一些:
  1. 它会导致您一遍又一遍地编写相同的代码。如果你有一个函数可以找到一个整数的质因数分解,并且你希望它对任何 int-like 值起作用,那么你必须 copy/paste 每个参数类型的质因数分解代码,只是为了改变将参数转换为 i32 的一行。您 可以 将共享代码重构为一个单独的函数,然后只从重载的函数中调用该函数,但这不就是字面意义上的特征参数边界的作用吗?
  2. 不可扩展。假设某人正在编写一个 BigInt 类型的箱子,他们希望它与您的函数一起使用。他们必须导入您的特征,然后 copy-paste 您的实现,并更改一行以将他们的 BigInt 转换为 i32。这不仅丑陋,而且如果您的实现引用任何私有方法或属性,实际上 不可能 。最重要的是,如果您更改实现(及其 20 个重载)来修复错误,外部 crate 的开发人员现在需要手动添加错误修复。其他开发人员永远不必关心您的内部结构。 IntLike 特性将允许其他开发人员只处理转换为 i32 的逻辑,然后让您处理其余部分。
  3. 这违背了语言的设计。 Rust book chapter on traits 的标题是“特征:定义共享行为”。它们主要是为此而设计的:共享行为。将它们用于函数重载只是一种 hack,它是 side-effect 如何在 Rust 中实现特征,因此它会带来如此多的关键问题。当您绑定参数而不是函数时,您可以自己编写自己的约束以及标准库中的众多 pre-implemented 特征,例如 DebugClone。事实上,大多数时候,您甚至不必创建自己的特质,因为已经有适合它的特质了。

TL;DR 地道的 C++ 是可怕的 Rust。