如何在 Rust 中最好地 *fake* 关键字样式函数参数?

How to best *fake* keyword style function arguments in Rust?

我有兴趣在功能上类似于 Rust 中的关键字参数,目前不支持它们。

对于提供关键字参数的语言,类似这样的事情很常见:

panel.button(label="Some Button")
panel.button(label="Test", align=Center, icon=CIRCLE)

我见过使用构建器模式处理的,例如:

ui::Button::new().label("Some Button").build(panel)
ui::Button::new().label("Test").align(Center).icon(CIRCLE).build(panel)

这很好,但与 Python 中的关键字参数相比有时有点尴尬。


然而,在 Rust 中使用 impl DefaultOption<..> 成员的结构初始化可以用来获得非常接近于实际上类似于编写关键字参数的东西,例如:

ui::button(ButtonArgs { label: "Some Button".to_string(), .. Default::default() } );

ui::button(ButtonArgs {
    label: "Test".to_string(),
    align: Some(Center),
    icon: Some(Circle),
    .. Default::default()
});

这可行,但在尝试用作关键字参数的上下文中有一些缺点:

有没有办法减少其中的一些问题,(例如使用宏) 以替代关键字访问使这项工作更容易?

你可以利用From trait;这样你就可以放弃一些样板:

use self::Shape::*;
use self::Alignment::*;

#[derive(Debug)]
struct Button {
    label: String,
    align: Option<Alignment>,
    icon: Option<Shape>,
}

#[derive(Debug)]
enum Shape { Circle }

#[derive(Debug)]
enum Alignment { Center }

impl From<(&'static str, Alignment, Shape)> for Button {
    fn from((l, a, i): (&'static str, Alignment, Shape)) -> Self {
        Button {
            label: l.to_owned(),
            align: Some(a),
            icon: Some(i)
        }
    }
}

fn main() {
    let b: Button = ("button", Center, Circle).into();

    println!("{:?}", b);
}

此实现将专门用于 (&'static str, Alignment, Shape) 元组;但是,您还可以实现 From<&'static str>,它会为其他组件生成具有给定 labelNoneButton

impl From<&'static str> for Button {
    fn from(l: &'static str) -> Self {
        Button {
            label: l.to_owned(),
            align: None,
            icon: None
        }
    }
}

let b2: Button = "button2".into();

免责声明:我建议不要使用此解决方案,因为报告的错误非常可怕。代码方面最干净的解决方案很可能是构建器模式。


除此之外...我拼凑了一个 proof-of-concept 演示操作员滥用行为。

与使用 struct 语法传递参数或使用构建器相比,它的主要优势在于它允许在采用不同的相同参数集的函数之间重用。

另一方面,它确实需要导入大量符号(每个名称都要使用)。

看起来像:

//  Rust doesn't allow overloading `=`, so I picked `<<`.
fn main() {
    let p = Panel;
    p.button(LABEL << "Hello", ALIGNMENT << Alignment::Center);

    p.button(LABEL << "Hello", Alignment::Left);
    p.button(Label::new("Hello"), Alignment::Left);
}

请注意,名称实际上是可选的,它只是作为参数本身的构建器,但如果您已经有了参数,则可以避开它。这也意味着可能不值得为 "obvious" 参数(此处为 Alignment)创建名称。

button的正常定义:

#[derive(Debug)]
struct Label(&'static str);

#[derive(Debug)]
enum Alignment { Left, Center, Right }

struct Panel;

impl Panel {
    fn button(&self, label: Label, align: Alignment) {
        println!("{:?} {:?}", label, align)
    }
}

需要一些增强:

impl Carrier for Label {
    type Item = &'static str;
    fn new(item: &'static str) -> Self { Label(item) }
}

impl Carrier for Alignment {
    type Item = Alignment;
    fn new(item: Alignment) -> Self { item }
}

const LABEL: &'static Argument<Label> = &Argument { _marker: PhantomData };
const ALIGNMENT: &'static Argument<Alignment> = &Argument { _marker: PhantomData };

是的,这确实意味着您可以扩充第 3 方库中定义的 function/method。

支持者:

trait Carrier {
    type Item;
    fn new(item: Self::Item) -> Self;
}

struct Argument<C: Carrier> {
    _marker: PhantomData<*const C>,
}

impl<C: Carrier> Argument<C> {
    fn create<I>(&self, item: I) -> C
        where I: Into<<C as Carrier>::Item>
    {
        <C as Carrier>::new(item.into())
    }
}

impl<R, C> std::ops::Shl<R> for &'static Argument<C>
    where R: Into<<C as Carrier>::Item>,
          C: Carrier
{
    type Output = C;
    fn shl(self, rhs: R) -> C {
        self.create(rhs)
    }
}

请注意,这不涉及:

  • 乱序参数传递
  • 可选参数

如果用户有足够的耐心来枚举可选参数的所有组合,@ljedrz 之类的解决方案是可能的:

struct ButtonArgs {
    label: Label,
    align: Alignment,
    icon: Icon,
}

impl From<Label> for ButtonArgs {
    fn from(t: Label) -> ButtonArgs {
        ButtonArgs { label: t, align: Alignment::Center, icon: Icon::Circle }
    }
}

impl From<(Label, Alignment)> for ButtonArgs {
    fn from(t: (Label, Alignment)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.1, icon: Icon::Circle }
    }
}

impl From<(Label, Icon)> for ButtonArgs {
    fn from(t: (Label, Icon)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: Alignment::Center, icon: t.1 }
    }
}

impl From<(Label, Alignment, Icon)> for ButtonArgs {
    fn from(t: (Label, Alignment, Icon)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.1, icon: t.2 }
    }
}

impl From<(Label, Icon, Alignment)> for ButtonArgs {
    fn from(t: (Label, Icon, Alignment)) -> ButtonArgs {
        ButtonArgs { label: t.0, align: t.2, icon: t.1 }
    }
}

然后将允许以下所有组合:

fn main() {
    let p = Panel;
    p.button( LABEL << "Hello" );
    p.button((LABEL << "Hello"));
    p.button((LABEL << "Hello", ALIGNMENT << Alignment::Left));
    p.button((LABEL << "Hello", ICON << Icon::Circle));
    p.button((LABEL << "Hello", ALIGNMENT << Alignment::Left, ICON << Icon::Circle));
    p.button((LABEL << "Hello", ICON << Icon::Circle, ALIGNMENT << Alignment::Left));

    p.button(Label::new("Hello"));
    p.button((LABEL << "Hello", Alignment::Left, Icon::Circle));
}

当有多个参数时,需要额外的一组括号。

但是有一个很大的缺点:使用错误的参数集会降低用户体验。

调用p.button("Hello");的结果是:

error[E0277]: the trait bound `ButtonArgs: std::convert::From<&str>` is not satisfied    --> <anon>:124:7
    | 124 |     p.button("Hello");
    |       ^^^^^^ the trait `std::convert::From<&str>` is not implemented for `ButtonArgs`
    |
    = help: the following implementations were found:
    = help:   <ButtonArgs as std::convert::From<Label>>
    = help:   <ButtonArgs as std::convert::From<(Label, Alignment)>>
    = help:   <ButtonArgs as std::convert::From<(Label, Icon)>>
    = help:   <ButtonArgs as std::convert::From<(Label, Alignment, Icon)>>
    = help: and 1 others
    = note: required because of the requirements on the impl of `std::convert::Into<ButtonArgs>` for `&str`

我认为宏是解决这个问题的最佳方法。您可以使用构建器 API 并为那些不喜欢构建器模式的人提供更简单的基于宏的糖。使用问题中的示例:

pub enum Shape { Circle }
pub enum Alignment { Center }
pub struct Button();

impl Button {
    pub fn new() -> Button {Button()}
    pub fn label(self, x: &str) -> Button { self }
    pub fn align(self, x: Alignment) -> Button { self }
    pub fn icon(self, x: Shape) -> Button { self }
}


macro_rules! button {
    ( $($i:ident = $e:expr),* ) => { 
        {
            let btn = Button::new();
            $(
                btn = btn.$i($e);
            )*
            btn
        }
    };
}

fn main() {
    let my_button = button!(label="hello", align=Alignment::Center, icon=Shape::Circle);
    // Equivalent to
    // let my_button = Button::new().label("hello").align(Alignment::Center).icon(Shape::Circle);
}