如何解决rust中缺少抽象类的问题?

kx1ctssn  于 2022-11-24  发布在  其他
关注(0)|答案(3)|浏览(145)

假设我有一个与数据成员密切相关的公共逻辑和一个抽象逻辑,我如何用rust类型来编写它,而不用为每个实现重写相同的代码呢?
这里有一个我用scala编写的小例子,注意抽象类的具体逻辑依赖于数据成员name和抽象逻辑formatDate()

abstract class Greeting(name: String) {
  def greet(): Unit = {
    println(s"Hello $name\nToday is ${formatDate()}.")
  }

  def formatDate(): String
}

class UsaGreeting(name: String) extends Greeting {
  override def formatDate(): String = {
    // somehow get year, month, day
    s"$month/$day/$year"
  }
}

class UkGreeting(name: String) extends Greeting {
  override def formatDate(): String = {
    // somehow get year, month, day
    s"$day/$month/$year"
  }
}

这只是一个玩具例子,但我的真实的生活约束是:

  • 我有多个数据成员-而不仅仅是一个(name)。
  • 每个子类都有相同的复杂方法,这些方法依赖于这些数据成员和特定于子类的抽象函数。
  • 对于良好的API设计,实现struct继续保持所有这些数据成员和复杂方法是很重要的。

下面是我的一些不太令人满意的想法,可以让这个工作在生 rust :

  • 我可能需要一个get_name()方法来处理每个实现都需要的trait,但这似乎是不必要的冗长,而且如果getter没有被内联,还可能导致性能下降。
  • 我可以完全避免使用rust特性,而是创建一个带有额外数据成员的结构体来实现缺少的抽象逻辑,但这会使抽象逻辑在编译时不可用,并且肯定会导致性能下降。
  • 我可以再次完全避免使用rust特性,而是使用泛型创建一个结构体,其关联函数完成抽象逻辑。到目前为止,这是我最好的想法,但使用泛型来填充缺失的逻辑感觉是错误的。

我对这些想法并不完全满意,那么在rust中有没有更好的方法来混合抽象逻辑和依赖于数据成员的具体逻辑呢?

xqnpmsa8

xqnpmsa81#

正如您所注意到的,Rust不是围绕类分类原则构建的,因此设计通常是不同的,您不应该试图在Rust中模仿OO语言。
你问了一个非常一般的问题,但有很多具体的情况需要不同的解决方案。
通常,当你想用OO语言定义什么对象是类时,你会使用traits来指定Rust中结构体行为的某些方面。
在您的特定情况下,假设正确的解决方案不涉及参数化或i18n实用程序,我可能会同时使用composition和enum来表示问候:

pub struct Greeting {
    name: String,
    greeter: Greeter;
}
impl Greeting {
    pub fn greet(&self) -> String {
        // use self.greeter.date_format() and self.name
    }
}

pub enum Greeter {
    USA,
    UK,
}
impl Greeter {
    fn date_format(&self) -> &'static str {
        match self {
           USA => ...,
           UK => ...,
        }
    }    
}

您的复合实现只需在需要时打开变体。

  • (请注意,我不编写这种情况下的实现,因为性能问题可能会为不同的设计调用Rust,而不是动态解释的模式,但这会使我们远离您的问题)*
pcrecxhr

pcrecxhr2#

您可以添加返回对必填字段的引用的函数,并定义函数的默认实现。这是java或C#中的常用技术(get; set;

trait Greeting: 
{
    fn get_name(&self) -> &str;
    fn format_date(&self) -> &str;
    fn greet(&self)
    {
        println!("Hello {}\n Today is {}", self.get_name(), self.format_date());
    }
}

struct USAGreeting{name: String }
struct UKGreeting{name: String }

impl Greeting for USAGreeting
{
    fn get_name(&self) -> &str { &self.name }

    fn format_date(&self) -> &str {
        return "$month/$day/$year"
    }
}

impl Greeting for UKGreeting
{
    fn get_name(&self) -> &str { &self.name }

    fn format_date(&self) -> &str {
        return "$day/$month/$year"
    }
}

fn main() {
    let uk = UKGreeting { name: "UK Greeting!".to_owned() };
    let usa = UKGreeting { name: "UK Greeting!".to_owned() };

    let dynamic: Vec<&dyn Greeting> = vec![&uk, &usa];

    for greeter in dynamic
    {
        greeter.get_name();
        greeter.greet();
    }
}

现在,Greeting可以在动态或通用上下文中使用。如果每次实现Greeting时都必须定义get_name() -> &str,那么解决方法如下所示:

trait Named
{
    fn get_name(&self) -> &str;
}

trait Greeting: Named 
{
    fn format_date(&self) -> &str;
    fn greet(&self)
    {
        println!("Hello {}\n Today is {}", self.get_name(), self.format_date());
    }
}

impl Greeting for USAGreeting
{
    fn format_date(&self) -> &str {
        return "$month/$day/$year"
    }
}

impl Greeting for UKGreeting
{
    fn format_date(&self) -> &str {
        return "$day/$month/$year"
    }
}

它并没有解决这个问题,而是把它从Greeting特征中分离出来。现在你可以使用过程宏(#[derive()])来自动生成get_name
例如,过程宏可能如下所示

use proc_macro::{self, TokenStream};
use quote::quote;
use syn;

#[proc_macro_derive(NamedMacro)]
pub fn describe(tokens: TokenStream) -> TokenStream {
    let ast: syn::DeriveInput = syn::parse(tokens).unwrap();

    let name = &ast.ident;

    quote! {
        impl Named for #name {
            fn get_name(&self) -> &str {
               &self.name
            }
        }
    }.into()

}

然后将get_name()添加到struct中就很容易了:

use my_macro::NamedMacro;

#[derive(NamedMacro)]
struct USAGreeting{ name: String }
#[derive(NamedMacro)]
struct UKGreeting{ name: String }

您可以在此处阅读有关宏的信息
Procedural Macros
Macros

oknwwptz

oknwwptz3#

最通用的解决方案似乎是我最初的第三个要点:而不是一个特征,用一个泛型来创建一个结构,泛型的关联函数完成了功能。
对于最初的玩具Greeting的例子,Denys的答案可能是最好的,但是一个更一般的解决主要问题的方法是:

trait Locale {
  pub fn format_date() -> String;
}

pub struct Greeting<LOCALE: Locale> {
  name: String,
  locale: PhantomData<LOCALE>, // needed to satisfy compiler
}

impl<LOCALE: Locale> Greeting<LOCALE> {
  pub fn new(name: String) {
    Self {
      name,
      locale: PhantomData,
    }
  }

  pub fn greet() {
    format!("Hello {}\nToday is {}", self.name, LOCALE::format_date());
  }
}

pub struct UsaLocale;

impl Locale for UsaLocale {
  pub fn format_date() -> {
    // somehow get year, month, day
    format!("{}/{}/{}", month, day, year)
  };
}

pub type UsaGreeting = Greeting<UsaLocale>;
pub type UkGreeting = ...

相关问题