首页 > 解决方案 > 如何为依赖于 Rust 中的泛型类型参数的结构定义关联函数的不同实现?

问题描述

给定一个依赖于泛型类型参数的结构,我们可以定义一个实现依赖于该类型的关联函数吗?

我想将结构传递给例程,但相关函数的计算方式取决于内部类型。此例程还取决于结构中的成员,因此我宁愿不要将所有内容都移至特征。

例如,以下代码尝试printme根据所涉及的类型定义不同的函数:

// Trait locked to a type
trait MyF64 {}
impl MyF64 for f64 {}
trait MyU32 {}
impl MyU32 for u32 {}

// Some kind of struct
struct Foo<T> {
    t: T,
}

// Implementation for f64
impl<T> Foo<T>
where
    T: MyF64,
{
    fn printme(&self) {
        println!("In a f64: {}", self.t);
    }
}

// Implementation for u32
impl<T> Foo<T>
where
    T: MyU32,
{
    fn printme(&self) {
        println!("In a u32: {}", self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>) {
    foo.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

这会产生编译器错误:

error[E0592]: duplicate definitions with name `printme`
  --> src/main.rs:17:5
   |
17 | /     fn printme(&self) {
18 | |         println!("In a f64: {}", self.t);
19 | |     }
   | |_____^ duplicate definitions for `printme`
...
27 | /     fn printme(&self) {
28 | |         println!("In a u32: {}", self.t);
29 | |     }
   | |_____- other definition for `printme`

如果我将定义printme移到另一个特征中,我们会遇到类似但不同的问题

// Trait locked to a type
trait MyF64 {}
impl MyF64 for f64 {}
trait MyU32 {}
impl MyU32 for u32 {}

// Some kind of struct
struct Foo<T> {
    t: T,
}

// Trait for Foo
trait FooTrait {
    fn printme(&self);
}

// Implementation for f64
impl<T> FooTrait for Foo<T>
where
    T: MyF64,
{
    fn printme(&self) {
        println!("In a f64: {}", self.t);
    }
}

// Implementation for u32
impl<T> FooTrait for Foo<T>
where
    T: MyU32,
{
    fn printme(&self) {
        println!("In a u32: {}", self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>)
where
    Foo<T>: FooTrait,
{
    foo.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

这会产生编译器错误:

error[E0119]: conflicting implementations of trait `FooTrait` for type `Foo<_>`:
  --> src/main.rs:28:1
   |
18 | / impl<T> FooTrait for Foo<T>
19 | | where
20 | |     T: MyF64,
21 | | {
...  |
24 | |     }
25 | | }
   | |_- first implementation here
...
28 | / impl<T> FooTrait for Foo<T>
29 | | where
30 | |     T: MyU32,
31 | | {
...  |
34 | |     }
35 | | }
   | |_^ conflicting implementation for `Foo<_>`

严格来说,我们可以通过为以下类型添加更好的特征来修复这个实验:

// External libraries
use std::fmt::Display;

// Trait gives the name
trait MyTrait {
    fn name(&self) -> String;
}
impl MyTrait for f64 {
    fn name(&self) -> String {
        "f64".to_string()
    }
}
impl MyTrait for u32 {
    fn name(&self) -> String {
        "u32".to_string()
    }
}

// Some kind of struct
struct Foo<T> {
    t: T,
}
impl<T> Foo<T>
where
    T: MyTrait + Display,
{
    fn printme(&self) {
        println!("In a {}: {}", self.t.name(), self.t);
    }
}

// Takes a foo
fn foo<T>(x: Foo<T>)
where
    T: MyTrait + Display,
{
    x.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t: 1.2 });
    foo(Foo { t: 12 });
}

这给出了正确的输出:

In a f64: 1.2
In a u32: 12

也就是说,这段代码比较简单,所以这种修复很容易。更一般地说,我有一个依赖于用户定义数据的结构。这些数据必然有一组不同的关联方法,并且很难强制每种数据都有一个共同的接口。但是,依赖于这些数据的结构只要知道它拥有什么样的数据,就可以很好地吸收这些信息。理论上,我们可以定义两个不同的结构来接受两种不同的数据,并让这些结构实现一个公共接口。也就是说,我真的想要访问一组通用的字段,并且宁愿不必定义许多 setter 和 getter。有没有更好的方法来实现这一点?

标签: rust

解决方案


好吧,我想我应该考虑更长的时间,但是以下内容在没有 setter 和 getter 的情况下完成了我想要的:

// Trait locked to a type
trait Float {
    fn bar(&self) -> String;
}
impl Float for f32 {
    fn bar(&self) -> String {
        "f32".to_string()
    }
}
impl Float for f64 {
    fn bar(&self) -> String {
        "f64".to_string()
    }
}
trait Int {
    fn baz(&self) -> &'static str;
}
impl Int for i32 {
    fn baz(&self) -> &'static str {
        "i32"
    }
}
impl Int for i64 {
    fn baz(&self) -> &'static str {
        "i64"
    }
}

// Define all of the different implementations that we care to implement in Foo
enum MyTypes {
    Float(Box <dyn Float>),
    Int(Box <dyn Int>),
}

// Some kind of struct
struct Foo {
    t : MyTypes,
}

// Implementation for f64
impl Foo {
    fn printme(&self) {
        match self.t {
            MyTypes::Float(ref t) => {
                println!("In a Float({})", t.bar());
            }
            MyTypes::Int(ref t) => {
                println!("In an Int({})", t.baz());
            }
        }
    }
}

// Takes a foo
fn foo(x : Foo) {
    x.printme();
}

fn main() {
    // Try both cases
    foo(Foo { t : MyTypes::Float(Box::new(1.2_f32))});
    foo(Foo { t : MyTypes::Int(Box::new(12_i64))});
}

本质上,如果我们知道用户定义的数据将来自有限数量的特征,我们可以将所有内容包装在一个中enum,然后让结构根据用户提供的数据类型更改其实现。这需要根据 trait 对传入的用户数据进行装箱,从而适当地隐藏底层类型。


推荐阅读