【问题标题】:Passing closures dynamically to .filter(..) results in compiler error将闭包动态传递给 .filter(..) 会导致编译器错误
【发布时间】:2021-12-28 05:23:03
【问题描述】:

我正在从事本书的 I/O 项目(基本上是一个 grep 副本)。 该函数采用insensitive 参数,这应该使文本搜索不区分大小写。

如果那个参数是true,那么函数体中唯一会改变的就是我正在做的过滤,所以我想有条件地改变回调。

fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
  let lines = content.lines();
  
  let mut matches = Vec::<&str>::new();
  
  let mut condition = |line: &str| -> bool { line.contains(query) };

  if insensitive {
    condition = |line: &str| -> bool { 
      line.to_lowercase().contains(query.to_lowercase()) // err 1
    };
  };

  lines
    .filter::<dyn Fn(&str) -> bool>(condition) // err 2
    .for_each(|line| matches.push(line));

  matches
}

我收到两个编译器错误:

  1. 没有两个闭包,即使相同,也有相同的类型。
  2. 无法对 Trait 对象调用 filter 方法。

我知道两个闭包不能有相同的类型,它们只能实现一个特定的函数特征。但是我不应该能够为filter 指定这样的特征吗?有没有办法有条件地改变回调?

【问题讨论】:

  • 错误 2:dyn Fn(&amp;str) -&gt; bool 是动态大小的类型 (DST)。您尝试将 DST 作为您的 filter 泛型参数 P 的类型,但 P 隐含为 Sized (see docs) 而您的 DST 不是。
  • 你可以在你的闭包中使用insensitive 例如` let condition = |line: &str|如果不敏感 { line.contains(&query) } else { line.to_lowercase().contains(&query.to_lowercase()) }; ` 这样你就可以避免Boxing

标签: rust closures


【解决方案1】:

第一点let mut condition = ...不是很好,因为编译器会推断闭包的大小,当你想改变它时无法兼容。

一种解决方案是:

let condition: Box<dyn Fn(&str) -> bool> = if insensitive {
    Box::new(|line: &str| -> bool { line.contains(&query) })
}else{
    Box::new(|line: &str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) })
};

下一点是过滤器。签名在下面

fn filter<P>(self, predicate: P) -> Filter<Self, P>
where
    P: FnMut(&Self::Item) -> bool, 

你应该这样使用

fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
  let lines = content.lines();
  
  let mut matches = Vec::<&str>::new();
  
  let condition: Box<dyn Fn(&str) -> bool> = if insensitive {
    Box::new(|line: &str| -> bool { line.contains(&query) })
  }else{
    Box::new(|line: &str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) })
  };
  
  lines
   .filter(|x| condition(x)) // err 2
   .for_each(|line| matches.push(line));
    
  matches
}

fn main()
{
  println!("{:?}", search_lines("test1", "test1\ntest2", true));
}

【讨论】:

  • 第二点不正确。 filter() 接受 Box&lt;dyn Fn&gt;,因为它还包含 FnMut
【解决方案2】:

您的示例有多个错误,让我们一个一个地检查它们。

首先,您不能将dyn Trait 存储在堆栈上,因为它可以具有可变大小。 filter() 要求其回调为Sized,因此您不能指定dyn Trait

我们可以通过使用Box&lt;dyn Fn(&amp;str) -&gt; bool&gt; 来解决这个问题:

pub fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
    let lines = content.lines();

    let mut matches = Vec::<&str>::new();

    let mut condition: Box<dyn Fn(&str) -> bool> =
        Box::new(|line: &str| -> bool { line.contains(query) });

    if insensitive {
        condition =
            Box::new(|line: &str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) });
    };

    lines.filter(condition).for_each(|line| matches.push(line));

    matches
}

然后我们得到另外两个错误,重要的是:

error[E0277]: expected a `FnMut<(&&str,)>` closure, found `dyn for<'r> Fn(&'r str) -> bool`
  --> src/lib.rs:14:18
   |
14 |     lines.filter(condition).for_each(|line| matches.push(line));
   |                  ^^^^^^^^^ expected an `FnMut<(&&str,)>` closure, found `dyn for<'r> Fn(&'r str) -> bool`
   |
   = help: the trait `FnMut<(&&str,)>` is not implemented for `dyn for<'r> Fn(&'r str) -> bool`
   = note: required because of the requirements on the impl of `for<'r> FnMut<(&'r &str,)>` for `Box<dyn for<'r> Fn(&'r str) -> bool>`

会发生什么?

如果我们查看at filter()'s signature,我们会发现它需要Pwhere P: FnMut(&amp;Self::Item) -&gt; bool 类型的回调。重要的部分是过滤器没有给回调一个拥有的Self::Item,而是一个对它的引用,&amp;Self::Item,因为我们需要在之后使用它。 Self::Item 这里是&amp;str,所以&amp;Self::Item&amp;&amp;str

pub fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
    let lines = content.lines();

    let mut matches = Vec::<&str>::new();

    let mut condition: Box<dyn Fn(&&str) -> bool> =
        Box::new(|line: &&str| -> bool { line.contains(query) });

    if insensitive {
        condition =
            Box::new(|line: &&str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) });
    };

    lines.filter(condition).for_each(|line| matches.push(line));

    matches
}

这可行,但我们可以进一步改进它。

首先,for_each(|line| matches.push(line)) 绝对不是惯用语。你要的是collect():

pub fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
    let lines = content.lines();

    let mut condition: Box<dyn Fn(&&str) -> bool> =
        Box::new(|line: &&str| -> bool { line.contains(query) });

    if insensitive {
        condition =
            Box::new(|line: &&str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) });
    };

    lines.filter(condition).collect()
}

其次,我们可以使用一个小技巧来避免Box的堆分配:

pub fn search_lines<'a>(query: &str, content: &'a str, insensitive: bool) -> Vec<&'a str> {
    let lines = content.lines();

    let condition_intensive;
    let mut condition: &dyn Fn(&&str) -> bool = &|line: &&str| -> bool { line.contains(query) };

    if insensitive {
        condition_intensive =
            |line: &&str| -> bool { line.to_lowercase().contains(&query.to_lowercase()) };
        condition = &condition_intensive;
    };

    lines.filter(condition).collect()
}

我们没有将回调存储在堆上,而是将其存储在堆栈中,有条件地

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多