Rust中的默认函数参数

247

是否可以创建一个带有默认参数的函数?

fn add(a: int = 1, b: int = 2) { a + b }

4
#6973包含一些解决方法(使用结构体)。 - huon
2020年,你如何编写代码? - puentesdiaz
1
@puentesdias 接受的答案仍然是正确的。在Rust中没有其他方法,你必须编写宏或使用Option并显式传递None - Jeroen
10个回答

275

由于不支持默认参数,您可以使用 Option<T> 来获得类似的行为。

fn add(a: Option<i32>, b: Option<i32>) -> i32 {
    a.unwrap_or(1) + b.unwrap_or(2)
}

这样做实现了只需要一次编写默认值和函数的目标(而不是在每次调用时都编写),但当然要打出更多的代码。函数调用将会像是add(None, None),这取决于你的观点,你可能会喜欢也可能不喜欢。

如果你认为在参数列表中什么都不输入可能是程序员忘记作出选择,那么这里的最大优点在于显式性;调用者明确表示他们想使用默认值,并且如果他们什么也不放置,将获得编译错误。可以将其视为键入add(DefaultValue, DefaultValue)

你还可以使用宏:

fn add(a: i32, b: i32) -> i32 {
    a + b
}

macro_rules! add {
    ($a: expr) => {
        add($a, 2)
    };
    () => {
        add(1, 2)
    };
}
assert_eq!(add!(), 3);
assert_eq!(add!(4), 6);

这两种解决方案的主要区别在于,在使用 "Option" 类型参数时,完全可以编写 add(None, Some(4)),但是在使用宏模式匹配时则不行(这类似于Python的默认参数规则)。

您还可以使用 "arguments" 结构以及 From/Into 特征:

pub struct FooArgs {
    a: f64,
    b: i32,
}

impl Default for FooArgs {
    fn default() -> Self {
        FooArgs { a: 1.0, b: 1 }
    }
}

impl From<()> for FooArgs {
    fn from(_: ()) -> Self {
        Self::default()
    }
}

impl From<f64> for FooArgs {
    fn from(a: f64) -> Self {
        Self {
            a: a,
            ..Self::default()
        }
    }
}

impl From<i32> for FooArgs {
    fn from(b: i32) -> Self {
        Self {
            b: b,
            ..Self::default()
        }
    }
}

impl From<(f64, i32)> for FooArgs {
    fn from((a, b): (f64, i32)) -> Self {
        Self { a: a, b: b }
    }
}

pub fn foo<A>(arg_like: A) -> f64
where
    A: Into<FooArgs>,
{
    let args = arg_like.into();
    args.a * (args.b as f64)
}

fn main() {
    println!("{}", foo(()));
    println!("{}", foo(5.0));
    println!("{}", foo(-3));
    println!("{}", foo((2.0, 6)));
}

这个选择显然涉及更多的代码,但与宏设计不同的是它使用了类型系统,这意味着编译器错误将对您的库/API用户更有帮助。 这也允许用户自己制作From实现,如果他们觉得有用的话。


7
最好将此答案分为几个部分,每个部分讨论一种方法。我想要给其中一个部分点赞。 - joel
10
如果你提及了哪种方法更适合,那么你的评论将更有用。;-) 我猜你更喜欢宏方法。 - Romain Vincent
我尝试用选项和宏的方法解决了几个问题。宏的编写和使用都更加容易。 - Romain Vincent

119

目前不支持。我认为它最终可能会被实现,但目前在这个领域没有积极的工作。

这里通常使用具有不同名称和签名的函数或方法的技术。


5
请注意,官方不鼓励使用那种方法。 - Chris Morgan
@ChrisMorgan,你有官方不鼓励这样做的来源吗? - Jeroen
2
@JeroenBollen 在我花了几分钟的搜索后,我能找到的最好资料是 https://www.reddit.com/r/rust/comments/556c0g/optional_arguments_in_rust_112/,在这里你可以找到一些像当时 Rust 项目负责人 brson 这样的人。可能 IRC 上有更多信息,但我不确定。 - Chris Morgan
4
我认为它最终会被实现。为什么呢?它不会增加额外的运行时开销吗?如果Rust要添加它,似乎与"零成本抽象"的整个哲学相悖。 - Dylan Kerler
@DylanKerler 他们可以采用类似于单态化的方式,这样只会增加编译时开销。 - Jack Clayton
1
另一种选择通常是使用 Option<T> 并通过 .unwrap_or_else(|| …) 或类似方式填充默认值;或者使用 T 并让调用者编写 T::default()。如果有的话,一流的默认参数值可能会更有效率,因为默认值的填充可以可靠地转移到调用者处,编译器认为这是值得的,而不需要内联整个过程,在提供值的情况下减少工作量 - 虽然通常优化器会使两种方法大致相同。 - Chris Morgan

95

不,Rust不支持默认函数参数。您必须使用不同的名称定义不同的方法。因为Rust使用函数名推导类型(函数重载需要相反的操作),所以也没有函数重载。

在结构体初始化的情况下,您可以使用结构体更新语法,就像这样:

use std::default::Default;

#[derive(Debug)]
pub struct Sample {
    a: u32,
    b: u32,
    c: u32,
}

impl Default for Sample {
    fn default() -> Self {
        Sample { a: 2, b: 4, c: 6}
    }
}

fn main() {
    let s = Sample { c: 23, ..Sample::default() };
    println!("{:?}", s);
}

[应要求,我从重复的问题中转载了这个答案]


谢谢分享。那么 trait 对象的默认值怎么样:Box<dyn TraitObject>? - Charlie 木匠

24

Rust不支持默认函数参数,我也不相信它将来会实现。 因此,我编写了一个proc_macro duang,以宏的形式实现它。

例如:

duang! ( fn add(a: i32 = 1, b: i32 = 2) -> i32 { a + b } );
fn main() {
    assert_eq!(add!(b=3, a=4), 7);
    assert_eq!(add!(6), 8);
    assert_eq!(add(4,5), 9);
}

15
如果您使用的是 Rust 1.12 或更高版本,您至少可以通过 Optioninto() 来使函数参数更易于使用:
fn add<T: Into<Option<u32>>>(a: u32, b: T) -> u32 {
    if let Some(b) = b.into() {
        a + b
    } else {
        a
    }
}

fn main() {
    assert_eq!(add(3, 4), 7);
    assert_eq!(add(8, None), 8);
}

8
从技术上讲,Rust社区在是否这是一个“好”想法方面存在明显分歧。就我个人而言,我属于“不好”的阵营。 - Shepmaster
3
@Shepmaster,这种设计模式可能会增加代码大小,而且不是非常易读。这些是使用该模式时的反对意见吗?到目前为止,我发现为了实现人性化的API,权衡利弊是值得的,但我考虑可能还有其他需要注意的地方。 - squidpickles
这段代码对于普通读者暗示了函数重载的存在。它的可行性使其成为一种被允许的方式,这是否意味着可能存在语言设计上的漏洞? - George
1
因为有趣且对讨论有所贡献,我点了赞。虽然我并不认为这是最佳答案,但在我的看法中也不应该排在回答的最后面。 - Bryan Larsen
1
今天最好使用 impl Into<Option<>> 来实现。 - Chayim Friedman

13

另一种方法可以是声明一个枚举,将可选参数作为变体,可以对其进行参数化以获取每个选项的正确类型。函数可以实现为接受枚举变体的可变长度切片。它们可以按任何顺序和长度排列。默认值在函数中作为初始赋值实现。

enum FooOptions<'a> {
    Height(f64),
    Weight(f64),
    Name(&'a str),
}
use FooOptions::*;

fn foo(args: &[FooOptions]) {
    let mut height   = 1.8;
    let mut weight   = 77.11;
    let mut name     = "unspecified".to_string();
    
    for opt in args {
        match opt {
            Height(h) => height = *h,
            Weight(w) => weight = *w,
            Name(n)   => name   =  n.to_string(),
        }
    }
    println!("  name: {}\nweight: {} kg\nheight: {} m", 
             name, weight, height);
}
    
fn main() { 

    foo( &[ Weight(90.0), Name("Bob") ] );

}

输出:

  name: Bob
weight: 90 kg
height: 1.8 m

args本身也可以是可选的。

fn foo(args: Option<&[FooOptions]>) {
    let args = args.or(Some(&[])).unwrap();
    // ...
}

1
@EduardoLuisSantos,好主意。我加了一个类似的例子。谢谢 =) - Todd
我不认为这种传递函数参数的方法是最有效的。Python比Rust应用程序快3倍有点令人惊讶。我可以理解PyPy3比Python更快3倍,但是解释型的Python与发布版本的Rust应用程序相比?@EduardoLuisSantos - Todd
为了性能而牺牲舒适度? - JulianW
1
@JulianH,对每个变量进行循环确实会增加一些开销,但并不多。所以是的...你在为“人体工程学”交换一些效率。然而,关于Python比其他语言快3倍的说法是可疑的。一个很好的例子是,如果没有编译发布版本,就会产生性能上的误解:Python vs. Rust - Todd
这看起来既棒又可怕,哈哈 - undefined
显示剩余4条评论

8

在之前的回答基础上,还要记住您可以创建与现有变量相同名称的新变量,这将隐藏先前的变量。如果您不打算再使用Option<...>,这对于保持代码清晰很有用。

fn add(a: Option<i32>, b: Option<i32>) -> i32 {
    let a = a.unwrap_or(1);
    let b = a.unwrap_or(2);
    a + b
}

这真是太棒了! - undefined

2

有一个 default_args crate 可以解决这个问题。

例子:

use default_args::default_args;

default_args! {
    fn add(a: int = 1, b: int = 2) {
        a + b
    }
}

请注意,您现在使用宏调用来调用函数。 (例如:add!(12)


0

问问自己,你为什么想要默认参数?这取决于你的原因,有很多答案:

如果你有太多的参数,最好将代码重构为更多不同的结构体,比如一些构建器模式,也许通过函数记录更新(FRU)。

pub struct MyFunction { ... many arguments ... }
impl Default for MyFunction { ... }
impl MyFunction {  go(self) { ... }  }

调用方式

MyFunction { desired arguments, ..Default::default() }.go()

你的构建器通常可以是一些相关类型,这使得方法链接更加美观。在这些类型中,你可以在类型级别隐藏参数,假设用户不嵌入你的中间类型。

pub struct MyWorker<R: RngCore = OsRng> { ... }

如果非默认情况很少,那么您可以通过一些已使用的 trait 来公开它们。例如,在 schnorrkel 中,我需要一个 R: RngCore 参数用于测试向量,以及想要取消随机化签名的利基用户。然而,我希望更广泛的生态系统仅使用良好随机化的签名。我已经采用了 merlin::Transcript 抽象来进行 Fiat-Shamir 变换。因此,我只通过 trait 提供 OsRng,但是您可以更改 trait 后面的类型以进行测试向量或其他操作。https://github.com/w3f/schnorrkel/blob/master/src/context.rs#L94-L103

-1
我想提出这种方法。
///
/// ## Argument
/// should_eat_veg: Option<bool> **default=false**
///
fn plan_my_meal(should_eat_veg: Option<bool>) -> Meal {
  let should_eat_veg = should_eat_veg.map_or(false, |v| v);
  ...
}
  • 对于带有默认值的参数,使用Option<T>类型声明,并将None作为默认值传递
  • 在函数体中"解包"并赋予参数默认值

1
最佳答案已经提到了这一点。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接