一个以trait为参数的Rust闭包

3

我希望我的函数可以接受一个闭包作为参数,该闭包以PartialOrd(或任何其他特性,这只是一个示例)作为参数,我希望能够在我的函数中使用任何实现了PartialOrd的类型来调用该闭包。类似于这样:

fn my_func(cmp: fn(impl PartialOrd, impl PartialOrd) -> bool) {
    cmp(3, 5);
}

当然,这并不起作用,因为impl Trait在闭包签名中无效。(我不知道为什么,但我猜它还没有实现。)所以我尝试了类似于以下的东西:

fn my_func<T: PartialOrd>(cmp: fn(T, T) -> bool) {
    cmp(3, 5);
}

这段代码无法编译,可能是因为T由调用方确定,而我只传递了i32,这可能与调用方的意图不符。那么,我该怎么做呢?我的主要目的是实现以下功能:
fn my_func(cmp: fn(impl PartialOrd, impl PartialOrd) -> bool) {
    if some_condition {
        cmp(type1_instance, type1_instance_2);
    } else {
        cmp(type2_instance, type2_instance_2);
    }
}

type1type2都实现了PartialEq

为了更具体,我将使用chrono库,并想写出类似以下的代码:

fn compare_dates(d1: DateTime, d2: DateTime, cmp: fn(impl PartialOrd, impl PartialOrd) -> bool) -> bool {
    if some_condition {
        cmp(d1, d2)
    } else {
        cmp(d1.date(), d2.date())
    }
}

compare_dates(
    Utc.date(2000 10, 11).and_hour(0,0,0),
    Utc.date(2000 10, 12).and_hour(0,0,0),
    PartialOrd::lt)

注意:`DateTime::date()` 方法返回的不是 `DateTime` 类型,而是另一种名为 `Date` 的类型。

1
这听起来像是一个XY问题。当你知道类型是i32时,为什么要使用一个通用类型参数呢?你有点在尝试做与类型参数的本意相反的事情。my_func函数并不决定T将会是什么,而是由调用者决定。 - vallentin
@vallentin Type 不是 i32,我在底部描述了我的真正问题,以免陷入 XY 问题。该参数可能是实现给定 trait 的两种不同类型之一。根据条件,我想要调用闭包并传递 type1type2 实例之一。 - isamert
1
我注意到 type*_instance*,但在那个例子中它们是哪些类型?我说这是 XY 问题,因为你没有给出一个具体的例子来说明背景和情况。 - vallentin
@vallentin 我进行了编辑,并使用了一个更具体的例子。如果这样可以,请告诉我。 - isamert
1
你不能真的这样做。因为在你的函数声明中,cmp只会是一个通用类型。所以当最终调用compare_dates时,T将会是一个具体类型。你可能可以用Any来做一些疯狂的事情来实现这个。然而,那样的话,你可能就不能简单地使用compare_dates(..., PartialOrd::lt)了。最简单的方法很可能是有两个cmp函数。你有没有一个例子能说明cmp除了小于号之外还能做更多的事情? - vallentin
显示剩余2条评论
2个回答

3

只有一个cmp存在的问题在于,虽然它可能在函数声明中是通用的,但当函数被调用时就会变得专门化。因此,它不能同时用于DateTimeDate(假设您想避免涉及更复杂的内容,比如Any。)

您可以通过拥有两个cmp参数来解决这个问题。不过,您仍然可以使用单个定义的实际cmp函数。

// chrono = "0.4.19"
use chrono::{Date, DateTime, TimeZone, Utc};

fn compare_dates<F, G>(
    d1: DateTime<Utc>,
    d2: DateTime<Utc>,
    cmp_date_times: F,
    cmp_dates: G,
) -> bool
where
    F: FnOnce(&DateTime<Utc>, &DateTime<Utc>) -> bool,
    G: FnOnce(&Date<Utc>, &Date<Utc>) -> bool,
{
    let some_condition = ...;
    if some_condition {
        cmp_date_times(&d1, &d2)
    } else {
        cmp_dates(&d1.date(), &d2.date())
    }
}

现在,您可以定义一个通用的cmp函数,并将其用于两种情况。
fn cmp<T: PartialOrd>(lhs: &T, rhs: &T) -> bool {
    lhs < rhs
}

compare_dates(
    Utc.ymd(2000, 10, 11).and_hms(0, 0, 0),
    Utc.ymd(2000, 10, 12).and_hms(0, 0, 0),
    cmp,
    cmp,
    // or
    // cmp::<DateTime<Utc>>,
    // cmp::<Date<Utc>>,
);

你也可以直接传递 PartialOrd::lt

compare_dates(
    Utc.ymd(2000, 10, 11).and_hms(0, 0, 0),
    Utc.ymd(2000, 10, 12).and_hms(0, 0, 0),
    PartialOrd::lt,
    PartialOrd::lt,
    // or
    // <DateTime<Utc> as PartialOrd>::lt,
    // <Date<Utc> as PartialOrd>::lt,
    // or
    // DateTime::<Utc>::lt,
    // Date::<Utc>::lt,
);

如果你想避免重复的参数,你可以定义一个名为compare_dates!的宏。
macro_rules! compare_dates {
    ($d1:expr, $d2:expr, $cmp:expr $(,)?) => {
        compare_dates($d1, $d2, $cmp, $cmp)
    };
}

您可以这样调用它:
compare_dates!(
    Utc.ymd(2000, 10, 11).and_hms(0, 0, 0),
    Utc.ymd(2000, 10, 12).and_hms(0, 0, 0),
    PartialOrd::lt,
);

compare_dates!(
    Utc.ymd(2000, 10, 11).and_hms(0, 0, 0),
    Utc.ymd(2000, 10, 12).and_hms(0, 0, 0),
    cmp,
);

3
“当然,这样做是行不通的,因为impl Trait在闭包签名中无效。(我不知道为什么,但我猜它还没有实现。)” “我认为这是不可能的;这将被反解成类似于以下内容:”
fn my_func(cmp: for<T1: PartialOrd, T2: PartialOrd> fn(T1, T2) -> bool) {
    cmp(3, 5);
}

这不是一种合法的类型(尽管我刚学到在那个位置使用生命周期参数是合法的 合法的,例如for<'a> fn(&'a i32) -> &'a i32)。还要注意,T1T2将是不同的类型,因此PartialOrd::lt将没有这个类型。如果支持的话,您可以再次显式地编写它而不使用impl

fn my_func(cmp: for<T: PartialOrd> fn(T, T) -> bool) {
    cmp(3, 5);
}

然而,另一种解决方法是将闭包类型转换为特质,并将您想要的具体闭包转换为该结构的实现。在这种情况下,由于闭包未捕获任何内容,因此使用一个单位结构:
trait Cmp {
    fn compare<T: PartialOrd>(&self, x: T, y: T) -> bool;
}

struct Lt;

impl Cmp for Lt {
    fn compare<T: PartialOrd>(&self, x: T, y: T) -> bool {
        x < y
    }
}

fn compare_dates(d1: DateTime, d2: DateTime, cmp: impl Cmp) -> bool {
    if some_condition {
        cmp.compare(d1, d2)
    } else {
        cmp.compare(d1.date(), d2.date())
    }
}

正如你所看到的,你只有一个cmp参数,它扮演着闭包的角色,甚至可以在本地定义:
fn main() {
    struct Lt;
    
    impl Cmp for Lt {
        fn cmp<T: PartialOrd>(&self, x: T, y: T) -> bool {
            x < y
        }
    }

    println!("{}", my_func(Lt));
}

但是仍然不如闭包方便。与vallentin的答案相比是否是一个好的权衡取决于具体情况。

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