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

How to work around the lack of abstract classes in rust?

假设我有密切依赖于数据成员的通用逻辑以及一段抽象逻辑。如何在不为每个实现重写相同代码的情况下用 rust 类型编写它?

这是我可能会用 Scala 编写的玩具示例。请注意,抽象 class 具有依赖于数据成员 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"
  }
}

这只是一个玩具示例,但我在现实生活中的限制是:

这里有一些我的一些不尽如人意的想法,可以使这项工作生锈:

我对这些想法不是很满意,那么在 Rust 中是否有更好的方法将抽象逻辑与依赖于数据成员的具体逻辑混合在一起?

如您所见,Rust 并非围绕 class 分类原则构建,因此设计通常不同,您不应尝试在 Rust 中模拟 OO 语言。

您问的是一个非常笼统的问题,但有很多具体情况需要不同的解决方案。

很多时候,当您试图使用 OO 语言来定义哪些对象具有 classes 时,您会使用 traits 来指定 Rust 结构行为的某些方面。

在您的特定情况下,假设正确的解决方案不涉及参数化或 i18n 实用程序,我可能会同时使用组合和枚举作为问候方式:

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 进行不同的设计而不是动态解释的模式,但这会使我们远离您的问题)

最通用的解决方案似乎是我原来的第 3 个项目符号:用泛型代替特征,创建一个结构,其关联函数完成功能。

对于原始 Greeting 示例,Denys 的回答可能是最好的。但是,如果 Greeting 还需要根据实现对类型进行通用,则它不再有效。在那种情况下,这将是最通用的解决方案,其中 T 是特定于实现的类型。

这种添加额外泛型的模式应该能够实现任何依赖于数据成员和抽象逻辑(实际上是抽象 class)的共享逻辑:

trait Locale<T> {
  pub fn local_greeting(info: T) -> String;
}

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

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

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

pub struct UsaLocale {}
impl Locale<Date> for UsaLocale {
  pub fn local_greeting(info: Date) -> {
    format!("{}/{}/{}", info.month, info.day, info.year)
  };
}

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

您可以添加将 return 引用必填字段的函数,并定义函数的默认实现。这是 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 可用于动态或通用上下文。 Ofc 每次实现 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() 添加到结构中很容易:

use my_macro::NamedMacro;

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

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

Procedural Macros

Macros