如何解决 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"
}
}
这只是一个玩具示例,但我在现实生活中的限制是:
- 我有几个数据成员 - 不只是一个 (
name
)。
- 每个 subclass 都有相同的复杂方法,这些方法依赖于那些特定于 subclass 的数据成员和抽象函数。
- 为了良好的 API 设计,重要的是实施
struct
继续持有所有这些数据成员和复杂的方法。
这里有一些我的一些不尽如人意的想法,可以使这项工作生锈:
- 我可能需要一个
get_name()
方法来处理每个实现都需要的特征。但这似乎不必要地冗长,如果 getter 没有内联,也可能会导致性能下降。
- 我可以完全避免使用 rust 特性,而是创建一个带有附加数据成员的结构来实现缺失的抽象逻辑。但这使得抽象逻辑在编译时不可用,并且肯定会导致性能下降。
- 我可以再次完全避免使用 rust 特性,而是创建一个具有泛型的结构,其关联函数完成抽象逻辑。到目前为止,这是我最好的想法,但使用泛型来填补缺失的逻辑感觉是错误的。
我对这些想法不是很满意,那么在 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 }
您可以在此处阅读有关宏的信息
假设我有密切依赖于数据成员的通用逻辑以及一段抽象逻辑。如何在不为每个实现重写相同代码的情况下用 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"
}
}
这只是一个玩具示例,但我在现实生活中的限制是:
- 我有几个数据成员 - 不只是一个 (
name
)。 - 每个 subclass 都有相同的复杂方法,这些方法依赖于那些特定于 subclass 的数据成员和抽象函数。
- 为了良好的 API 设计,重要的是实施
struct
继续持有所有这些数据成员和复杂的方法。
这里有一些我的一些不尽如人意的想法,可以使这项工作生锈:
- 我可能需要一个
get_name()
方法来处理每个实现都需要的特征。但这似乎不必要地冗长,如果 getter 没有内联,也可能会导致性能下降。 - 我可以完全避免使用 rust 特性,而是创建一个带有附加数据成员的结构来实现缺失的抽象逻辑。但这使得抽象逻辑在编译时不可用,并且肯定会导致性能下降。
- 我可以再次完全避免使用 rust 特性,而是创建一个具有泛型的结构,其关联函数完成抽象逻辑。到目前为止,这是我最好的想法,但使用泛型来填补缺失的逻辑感觉是错误的。
我对这些想法不是很满意,那么在 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 }
您可以在此处阅读有关宏的信息