将函数重载(通过特征)纳入范围

Phi*_*ZXX 4 overloading traits rust

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

/* 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() }
    }
}
Run Code Online (Sandbox Code Playgroud)

/* 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);
}
Run Code Online (Sandbox Code Playgroud)

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

其次,有没有办法让我不必use crate::my_class::Func;为每个特征都写?也就是说,当我导入时,如何将MyClass(通过impl MyClass和定义的)的所有函数纳入范围?impl Func<T> for MyClassMyClass

Apl*_*123 10

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

但不要。C++/Java 风格的函数重载不是一个好主意的原因有很多

  1. 这很令人困惑。当然,您可以想出一些不会令人困惑的示例,但是许多重载函数根据参数的类型执行完全不同的操作,此时,为什么不直接创建一个新函数呢?.func(5)在您的示例中,将添加5a,而.func("5")添加5到是非常不直观的b
  2. 它给调用者带来了不必要的负担。想象一下,您正在编写一个函数foo,该函数接受一些T可以传递给func. 你会如何绑定它?它看起来像这样:
fn foo<T>
where
    MyClass: Func<T>
{ unimplemented!() }
Run Code Online (Sandbox Code Playgroud)

这已经有点丑了。现在假设您有一个MyClass2具有重载函数的函数,该函数还接受类似 int 的值(i32或者&str可以解析为i32)。你的界限现在看起来像这样:

fn foo<T>
where
    MyClass: Func<T>,
    MyClass2: Func<T>
{ unimplemented!() }
Run Code Online (Sandbox Code Playgroud)

即使它们在概念上对类似 int 的值具有相同的界限。当添加更多的泛型和重载时,它只会变得越来越丑陋。

  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;
Run Code Online (Sandbox Code Playgroud)

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 }
    }
}
Run Code Online (Sandbox Code Playgroud)

/* 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);
}
Run Code Online (Sandbox Code Playgroud)

那不是更好吗?


附录

传统函数重载不是一个好主意的原因还有很多,但由于偏离要点而从帖子的主体中省略了。这里还有一些:
  1. 它会导致您一遍又一遍地编写相同的代码。如果您有一个函数可以查找整数的素因数分解,并且希望它能够处理任何类似 int 的值,那么您必须为每个参数类型复制/粘贴素因数分解代码,仅更改一行将参数转换为i32. 您可以将共享代码重构为一个单独的函数,然后从重载的函数中调用该函数,但这不是字面上限制特征参数的作用吗?
  2. 它不可扩展。假设有人正在编写一个带有类型的箱子BigInt,并且他们希望它能够与您的函数一起使用。他们必须导入您的特征,然后复制粘贴您的实现,并更改一行以将其转换BigInti32. 这不仅丑陋,而且如果您的实现引用任何私有方法或属性,这实际上是不可能的。最重要的是,如果您更改实现(及其 20 个重载)来修复错误,则外部包的开发人员现在需要手动添加错误修复。其他开发人员不必关心您的内部结构。特征IntLike将允许其他开发人员只处理转换为 的逻辑i32,然后让您处理其余的事情。
  3. 它违背了语言的设计。Rust书中关于特征的章节标题为“特征:定义共享行为”。它们的设计初衷就是为了:共享行为。使用它们进行函数重载只是一种 hack,是 Rust 中如何实现特征的副作用,因此它会带来如此多的关键问题。当您绑定参数而不是函数时,您可以编写自己的约束以及标准库中的许多预先实现的特征,例如DebugClone。事实上,大多数时候,你甚至不需要创造自己的特质,因为它已经有一个特质了。

TL;DR 惯用的 C++ 是可怕的 Rust。


cam*_*024 5

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

但可能有比重载更好的方法,而且我猜更标准的特征使用会更好。

我假设 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()
  }
}
Run Code Online (Sandbox Code Playgroud)

然后您可以更改您的函数以接受以下任何内容IntLike

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

这有多个优点:

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

由于 Rust 的单态泛型,它的速度同样快