Domyślne argumenty funkcji w Rust


111

Czy można utworzyć funkcję z domyślnym argumentem?

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

4
# 6973 zawiera kilka obejść (przy użyciu struktury).
— huon

Jak można to zakodować w 2020 roku?
— puentesdiaz

@puentesdias Zaakceptowana odpowiedź jest nadal poprawną odpowiedzią. Nie ma sposobu, aby to zrobić w Rust i musisz albo napisać makro, albo użyć Optioni jawnie przekazać None.
— Jeroen

Odpowiedzi:


58

Nie, obecnie nie ma. Myślę, że jest prawdopodobne, że w końcu zostanie wdrożony, ale obecnie nie ma aktywnej pracy w tej przestrzeni.

Typową techniką stosowaną tutaj jest użycie funkcji lub metod o różnych nazwach i podpisach.


2
@ ner0x652: ale pamiętaj, że takie podejście jest oficjalnie odradzane.
— Chris Morgan

@ChrisMorgan Czy masz źródło, dla którego oficjalnie się to zniechęca?
— Jeroen

1
@JeroenBollen Najlepsze, co mogę wymyślić w ciągu kilku minut, to reddit.com/r/rust/comments/556c0g/… , gdzie masz ludzi takich jak brson, który był wówczas liderem projektu Rust. IRC mógł mieć więcej, nie jestem pewien.
— Chris Morgan

116

Ponieważ domyślne argumenty nie są obsługiwane, możesz uzyskać podobne zachowanie używając Option<T>

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

Osiąga to cel polegający na tym, że wartość domyślna i funkcja są kodowane tylko raz (zamiast w każdym wywołaniu), ale oczywiście jest o wiele więcej do wpisania. Wywołanie funkcji będzie wyglądać add(None, None), co może ci się spodobać lub nie, w zależności od twojej perspektywy.

Jeśli widzisz, że nic nie wpisuje się na liście argumentów, ponieważ koder potencjalnie zapomina o dokonaniu wyboru, to dużą zaletą jest tutaj jawność; wywołujący wyraźnie mówi, że chce korzystać z domyślnej wartości i otrzyma błąd kompilacji, jeśli nic nie poda. Pomyśl o tym jak o pisaniu add(DefaultValue, DefaultValue).

Możesz także użyć makra:

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);

Duża różnica między tymi dwoma rozwiązaniami polega na tym, że w przypadku argumentów -al „Opcja” zapisywanie jest całkowicie poprawne add(None, Some(4)), ale przy dopasowaniu wzorca makra nie można (jest to podobne do domyślnych reguł argumentów Pythona).

Możesz także użyć struktury „arguments” i From/ Intotraits:

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)));
}

Ten wybór to oczywiście o wiele więcej kodu, ale w przeciwieństwie do projektu makra wykorzystuje system typów, co oznacza, że ​​błędy kompilatora będą bardziej pomocne dla użytkownika biblioteki / API. Pozwala to również użytkownikom na tworzenie własnych Fromimplementacji, jeśli jest to dla nich pomocne.


3
ta odpowiedź byłaby lepsza jako kilka odpowiedzi, po jednej dla każdego podejścia. Chcę zagłosować za tylko jednym z nich
— joel

60

Nie, Rust nie obsługuje domyślnych argumentów funkcji. Musisz zdefiniować różne metody o różnych nazwach. Nie ma też przeciążania funkcji, ponieważ Rust używa nazw funkcji do wyprowadzania typów (przeciążanie funkcji wymaga czegoś przeciwnego).

W przypadku inicjalizacji struktury możesz użyć składni aktualizacji struktury w następujący sposób:

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);
}

[na życzenie przesłałem tę odpowiedź z drugiego pytania]


5
Jest to bardzo użyteczny wzorzec dla domyślnych argumentów. Powinien być wyżej
— Ben

10

Rust nie obsługuje domyślnych argumentów funkcji i nie wierzę, że zostanie zaimplementowany w przyszłości. Napisałem więc proc_macro duang, aby zaimplementować go w postaci makra.

Na przykład:

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);
}

7

Jeśli używasz Rust 1.12 lub nowszego, możesz przynajmniej ułatwić używanie argumentów funkcji z Optioni into():

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);
}

7
Chociaż technicznie poprawne, społeczność Rusta jest wyraźnie podzielona co do tego, czy jest to „dobry” pomysł. Osobiście zaliczam się do obozu „niedobrego”.
— Shepmaster

1
@Shepmaster może zwiększyć rozmiar kodu i nie jest super czytelny. Czy to obiekcje wobec używania tego wzorca? Do tej pory stwierdziłem, że kompromisy są opłacalne w obsłudze ergonomicznych interfejsów API, ale uważam, że może mi brakować innych problemów.
— kałamarnice

2

Innym sposobem mogłoby być zadeklarowanie wyliczenia z opcjonalnymi parametrami jako wariantami, które można sparametryzować tak, aby przyjmowały odpowiedni typ dla każdej opcji. Funkcję można zaimplementować w celu pobrania wycinka o zmiennej długości wariantów wyliczeniowych. Mogą być w dowolnej kolejności i długości. Wartości domyślne są implementowane w funkcji jako przypisania początkowe.

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") ] );

}

wynik:

  name: Bob
weight: 90 kg
height: 1.8 m
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.