Rust: Vec::drain_filter 和 LinkedList::drain_filter 的跟踪问题

创建于 2017-07-15  ·  119评论  ·  资料来源: rust-lang/rust

    /// Creates an iterator which uses a closure to determine if an element should be removed.
    ///
    /// If the closure returns true, then the element is removed and yielded.
    /// If the closure returns false, it will try again, and call the closure
    /// on the next element, seeing if it passes the test.
    ///
    /// Using this method is equivalent to the following code:
    ///
    /// ```
    /// # let mut some_predicate = |x: &mut i32| { *x == 2 };
    /// # let mut vec = vec![1, 2, 3, 4, 5];
    /// let mut i = 0;
    /// while i != vec.len() {
    ///     if some_predicate(&mut vec[i]) {
    ///         let val = vec.remove(i);
    ///         // your code here
    ///     }
    ///     i += 1;
    /// }
    /// ```
    ///
    /// But `drain_filter` is easier to use. `drain_filter` is also more efficient,
    /// because it can backshift the elements of the array in bulk.
    ///
    /// Note that `drain_filter` also lets you mutate ever element in the filter closure,
    /// regardless of whether you choose to keep or remove it.
    ///
    ///
    /// # Examples
    ///
    /// Splitting an array into evens and odds, reusing the original allocation:
    ///
    /// ```
    /// let mut numbers = vec![1, 2, 3, 4, 5, 6, 8, 9, 11, 13, 14, 15];
    ///
    /// let evens = numbers.drain_filter(|x| *x % 2 == 0).collect::<Vec<_>>();
    /// let odds = numbers;
    ///
    /// assert_eq!(evens, vec![2, 4, 6, 8, 14]);
    /// assert_eq!(odds, vec![1, 3, 5, 9, 11, 13, 15]);
    /// ```
    fn drain_filter<F>(&mut self, filter: F) -> DrainFilter<T, F>
        where F: FnMut(&mut T) -> bool,
    { ... }

我确定这在某个地方存在问题,但我找不到它。 有人书呆子狙击我实施它。 公关进来。

A-collections B-unstable C-tracking-issue Libs-Tracked T-libs

最有用的评论

有什么阻止它稳定下来吗?

所有119条评论

也许这不需要包括厨房水槽,但它_可以_有一个范围参数,所以它就像排水管的超集。 这样做有什么缺点吗? 我想为范围添加边界检查是一个缺点,这是另一件可能会恐慌的事情。 但是 drain_filter(.., f) 不能。

有没有可能在不久的将来以某种形式稳定下来?

如果编译器足够聪明,可以消除边界检查
drain_filter(.., f)的情况下,我会选择这样做。

(而且我很确定您可以通过某种方式实现它
这使得编译器足够聪明,在最坏的情况下
如果你可以有一个“功能专业化”,
基本上像if Type::of::<R>() == Type::of::<RangeFull>() { dont;do;type;checks; return }

我知道这在某种程度上是自行车脱落,但是将其命名drain_filter而不是drain_where的原因是什么? 对我来说,前者意味着整个Vec将被耗尽,但我们也在结果上运行filter (当我第一次看到它时,我想:“这不仅仅是.drain(..).filter() ?”)。 另一方面,前者表明我们只在某些条件成立的情况下排出元素。

不知道,但是drain_where听起来好多了,而且更直观。
还有机会改变吗?

.remove_if也是之前的建议

我认为drain_where确实解释得最好。 与 drain 一样,它返回值,但它不会耗尽/删除所有值,而只是在给定条件为真的情况下。

remove_if听起来很像remove的条件版本,如果条件为真,它只会按索引删除单个项目,例如letters.remove_if(3, |n| n < 10);如果索引 3 < 10 则删除索引 3 处的字母.

另一方面, drain_filter有点模棱两可,是drain然后filter以更优化的方式(如 filter_map)还是 if drain 使得返回的迭代器与迭代器filter将返回,
如果是这样,它不应该被称为filtered_drain因为过滤器之前在逻辑上使用过......

在标准库的任何地方都没有使用_where_if的先例。

@Gankro是否有在任何地方使用_filter的先例? 我也不知道这是不使用不那么含糊的术语的原因吗? 标准库中的其他地方已经使用了各种后缀,例如_until_while

评论中的“所说的等效”代码不正确......你必须在“你的代码在这里”网站上从 i 中减去一个,否则会发生不好的事情。

IMO 这不是filter的问题。 刚刚搜索过这个(并且是新手),与其他语言相比, drain似乎相当不标准。

同样,从新手的角度来看,如果试图找到一些事情来做这个问题所建议的事情,我会搜索的东西是delete (如delete_if ), remove , filterreject

我实际上_搜索_了filter ,看到了drain_filter和 _kept search_ 没有阅读,因为drain似乎并不代表我想做的简单事情。

看起来一个名为filterreject的简单函数会更直观。

另外,我不觉得这应该改变它所调用的向量。 它可以防止链接。 在理想情况下,人们希望能够执行以下操作:

        vec![
            "",
            "something",
            a_variable,
            function_call(),
            "etc",
        ]
            .reject(|i| { i.is_empty() })
            .join("/")

对于当前的实现,它将加入的是被拒绝的值。

我想同时看到acceptreject 。 两者都不会改变原始值。

您已经可以单独使用filter进行链接操作。 drain_filter的全部意义在于改变向量。

@rpjohnst所以我在这里搜索,我在某处错过filter吗?

是的,它是Iterator的成员,而不是Vec的成员。

Drain 是一个新颖的术语,因为它代表了 Rust 中仅适用于容器的第四种所有权,同时在几乎任何其他语言中通常也是一个毫无意义的区别(在没有移动语义的情况下,没有必要将迭代和删除结合到单个“原子”操作)。

虽然 Drain_filter 将 Drain 术语移动到其他语言会关心的空间中(因为避免后移在所有语言中都是相关的)。

我在文档中遇到了drain_filter作为rust consume vec的谷歌结果。 我知道由于 rust 默认情况下的不变性,过滤器不会消耗数据,只是不记得如何处理它,所以我可以更好地管理内存。

drain_where很好,但只要用户知道drainfilter做了什么,我认为很明显该方法会根据谓词过滤器排出数据。

我仍然觉得drain_filter意味着它会排空(即清空)然后过滤。 另一方面, drain_where听起来像是耗尽了给定条件成立的元素(这是提议的函数所做的)。

linked_list::DrainFilter不应该也实现Drop以删除与谓词匹配的任何剩余元素吗?

是的

为什么删除迭代器会导致它运行到最后? 我认为这对于迭代器来说是令人惊讶的行为,如果需要,它也可以显式地完成。 另一方面,只取出所需数量的元素是不可能的,因为mem::forget迭代器会遇到泄漏放大。

我一直在使用这个函数,而且我总是要记住为我想要排出的条目返回true ,这与retain() / retain_mut()相比感觉违反直觉
在直观的逻辑层面上,为我想要保留的条目返回true会更有意义,其他人有这种感觉吗? (特别是考虑到retain()已经以这种方式工作)
为什么不这样做,并将drain_filter()重命名为retain_iter()retain_drain() (或drain_retain() )?
然后它也会更接近地反映retain()

这就是为什么我建议将其重命名为
drain_where因为很明显:

  1. 它是一种排水形式,因此我们在名称中使用排水

  2. 通过将wheredrain结合使用,很明显
    元素_where_谓词为真得到耗尽,即删除并返回

  3. 它与 std 中的其他名称更同步,通常如果您有
    由两个谓词组成的函数,您可以通过使用(大致)模拟它
    以“然后”方式表示每个谓词的函数,例如
    filter_map可以(大致)模拟为filter an then map 。 目前的
    名称表明它是drain and then filter ,但它甚至不接近它
    因为它根本没有完全排水。

2018 年 2 月 25 日星期日 17:04 Boscop [email protected]写道:

我一直在使用这个功能,我总是要记住
为我想要排空的条目返回 true,这感觉
与retain_mut() 相比违反直觉。
在原始层面上,对于 I 的条目返回 true 会更有意义
想保留,有没有人有这种感觉?
为什么不这样做,并将 drain_filter() 重命名为 retain_filter()?


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-368320990 ,
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kfwaNvz8YBwCE4BxDkeHgGxLvcWxks5tYYRxgaJpZM4OY1me
.

但是对于drain_where() ,对于应该删除的元素,闭包仍然必须返回 true,这与retain()相反,这使得它不一致。
也许retain_where
但我认为你是对的,在名称中包含“drain”是有意义的,所以我认为drain_retain()最有意义:它就像drain()但保留闭包返回的元素true

虽然它没有改变,但你必须返回 true 这清楚地表明
你必须这样做。

我个人会使用:

一个。 drain_where
湾。 retain_where
C。 retain

我不会使用drain_retain
Drain 和 Retain 谈论相同的过程,但从相反的角度
观点,drain 谈论您删除(和返回)保留的内容
谈论您保留的内容(以在 std 中已使用的方式)。

目前retaim已经在 std 中实现,主要区别在于
它正在丢弃元素,而drain正在返回它们,这使得
retain (遗憾地)不适合在名字中使用(除非你想打电话
retain_and_return或类似)。

另一个关于流失的观点是易于迁移,即迁移
drain_where就像运行基于单词的搜索和替换一样简单
代码,而将其更改为保留将需要额外的否定
所有使用的谓词/过滤器函数。

2018 年 2 月 25 日星期日 18:01 Boscop [email protected]写道:

但是对于 drain_where() 闭包仍然必须返回 true
应该删除的元素,这与 retain() 相反
使它不一致..
也许retain_where?
但我认为你是对的,名字中带有“排水”是有道理的,
所以我认为 drain_retain() 最有意义:它就像 drain() 但是
保留闭包返回 true 的元素。


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-368325374
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kfG4oZHxGfpOSK8DjXW3_2O1Eo3Rks5tYZHxgaJpZM4OY1me
.

但是,您多久会从drain()迁移到drain_filter()
到目前为止,在所有情况下,我都从retain()迁移到drain_filter() ,因为 std 中没有retain_mut()并且我需要改变元素! 所以我不得不反转闭包返回值..
我认为drain_retain()是有道理的,因为drain()方法无条件地排出范围内的所有元素,而drain_retain()保留闭包返回true的元素,它结合drain()retain()方法的效果。

抱歉,我的意思是从drain_filter迁移到drain_where

你有一个使用retain的解决方案,然后需要使用
drain_filter是我尚未考虑的方面。

2018 年 2 月 25 日星期日 19:12 Boscop [email protected]写道:

但是为什么要从 drain() 迁移到 drain_filter()?
到目前为止,在所有情况下,我都从 retain() 迁移到了 drain_filter()
因为std中没有retain_mut(),我需要改变元素!
我认为 drain_retain() 是有道理的,因为 drain() 方法会耗尽
无条件地在范围内的所有元素,而 drain_retain() 保留
闭包返回 true 的元素。


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-368330896 ,
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kSayIk_fbp5M0RsZW5pYs3hDICQIks5tYaJ0gaJpZM4OY1me
.

啊,是的,但我认为在使用drain_filter()的当前代码中反转闭包的“代价”是值得的,以便在 std 中获得一致且直观的 API,然后再稳定。
这只是一个很小的固定成本(并且由于它会伴随函数的重命名而得到缓解,所以编译器错误可以告诉用户必须反转闭包,所以它不会默默地引入错误) ,与标准化drain_filter()的成本相比,然后人们在从retain()迁移到drain_filter()时总是不得不反转闭包 ..(除了记住这样做,以及在文档中更难找到它的成本,来自retain()并搜索“类似于retain()但将&mut传递给它的关闭,这这就是为什么我认为这个函数的新名称在名称中包含“retain”是有道理的,以便人们在文档中搜索时找到它)。

一些轶事数据:在我的代码中,我总是只需要 $#$ drain_filter() $#$ 的retain_mut()方面(以前他们经常使用retain() ),我从来没有需要使用的用例处理耗尽的值。 我认为这也是未来其他人最常见的用例(因为retain()不会将&mut传递给它的闭包,因此drain_filter()必须涵盖该用例,也是一个比需要处理耗尽的值更常见的用例)。

我反对drain_retain的原因是目前在 std wrt 中使用名称的方式。 收藏:

  1. 您有使用谓词的函数名称,这些谓词具有与之相关的生产/消费概念(wrt. rust,迭代)。 例如drain , collect , fold , all , take , ...
  2. 这个谓词有时有修饰符,例如*_where*_while
  3. 你有使用具有修改属性的谓词的函数名称( mapfilterskip ,...)

    • 在这里,如果它是元素或迭代修改是模糊的( mapfilter / skip

  4. 使用修改属性链接多个谓词的函数名称,例如filter_map

    • 有一个大约apply modifier_1 and then apply modifier_2的概念,只是一步完成它更快或更灵活

你有时可能有:

  1. 将生产/消费谓词与修改谓词组合在一起的函数名称(例如drain_filter

    • _but_ 大多数时候,将它们与修饰符结合起来会更好/更少混淆(例如drain_where

您通常没有:

  1. 两个生产/消费谓词组合成一个名称,即我们没有像take_collect这样的想法,因为它很容易混淆

drain_retain确实有点道理,但属于最后一类,虽然你可能会猜到它的作用,但它基本上说的是remove and return all elements "somehow specified" and then keep all elements "somehow specified" discarding other elements


另一方面,我不知道为什么不应该有retain_mut可能打开一个快速的 RFC 引入retain_mut作为组合modify + retain的有效方式
然后稳定了这个功能。 在此之前,您可以考虑编写一个扩展特征提供
您拥有retain_mut使用iter_mut + 一个布尔数组(或位数组,或...)来跟踪哪些元素
必须删除。 或提供您自己的drain_retain内部使用drain_filer / drain_where
但将谓词包装成 not |ele| !predicate(ele)

@dathiab

  1. 我们在这里讨论的是集合上的方法,而不是迭代器上的方法。 map、filter、filter_map、skip、take_while 等都是 Iterator 上的方法。 顺便说一句,您的意思是使用*_where哪些方法?
    所以我们必须将命名方案与集合中已经存在的方法进行比较,例如retain()drain() 。 与将一个迭代器转换为另一个迭代器的迭代器方法没有混淆。
  2. AFAIK 的共识是retain_mut()不会被添加到 std,因为drain_filter()已经被添加并且建议人们使用它。 这让我们回到了从retain()迁移到drain_filter()的用例非常普遍,因此它应该具有相似的名称和 API(返回true的闭包意味着保留条目)..

我不知道retain_mut已经讨论过了。

我们正在讨论 std 中的一般命名方案,主要是 wrt。 到
集合,其中确实包括迭代器,因为它们是主要的
rust 中集合的访问方法,尤其是当它关于
修改多个条目。

  • _where只是后缀的一个例子,表示稍加修改
    功能。 目前在 std 中使用的这种后缀主要是
    _until_while_then_else_mut_back

drain_retain之所以让人迷惑,是因为不清楚是不是
基于流失或保留,如果是基于流失,则返回 true 将删除
该值,如果是基于保留的,它将保留它。 使用_where使它在
最后明确传入函数的预期。

2018 年 2 月 26 日星期一 00:25 Boscop [email protected]写道:

@dathinab https://github.com/dathinab

  1. 我们在这里讨论的是集合上的方法,而不是迭代器上的方法。
    map、filter、filter_map、skip、take_while 等都是 Iterator 上的方法。
    顺便说一句,您的意思是使用 *_where 的哪些方法?
    所以我们必须将命名方案与已经存在的方法进行比较
    在集合上,例如retain()、drain()。 没有混淆
    将迭代器转换为另一个迭代器的迭代器方法。
  2. AFAIK 的共识是不会将 retain_mut() 添加到 std
    因为 drain_filter() 已经被添加并且人们被告知
    使用它。 这让我们回到了迁移的用例
    retain() 到 drain_filter()常见,所以它应该有一个
    类似的名称和 API(闭包返回 true 表示保留条目)..


你收到这个是因为你被提到了。

直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-368355110
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kfkRAZ5OtLFZ-SciAmjHDEXdgp-0ks5tYevegaJpZM4OY1me
.

我一直在使用这个函数,而且我总是必须记住为我想要排出的条目返回 true,这与 retain()/retain_mut() 相比感觉违反直觉。

FWIW,我认为retain是这里的反直觉名称。 我通常会发现自己想从向量中删除某些元素,而使用retain我总是不得不颠倒这个逻辑。

但是retain()已经稳定了,所以我们必须忍受它.. 所以我的直觉已经习惯了..

@Boscopdrain也是retain的倒数,但也返回删除的元素和后缀的用法,如_until_while用于制作可用的功能只是现有功能的略微修改版本。

我的意思是,如果我要描述排水,它会是这样的:

_删除并返回“以某种方式”指定的所有元素,保留所有其他元素_
其中 _"in some way"_ 是 _"by slicing"_ 用于所有可切片的集合类型,_"all"_ 用于其余的。

这里讨论的函数的描述是_the same_,除了
_"以某种方式"_ 是 _" ,其中给定谓词返回 true"_。

另一方面,我要保留的描述是:
_仅保留(即保留)给定谓词返回 true 的元素,丢弃其余元素_

(是的,retain 可以以不丢弃其余部分的方式使用,遗憾的是它不是)


我确实认为如果retain会有
&mut T传递给谓词,并可能返回删除的值。
因为我认为retain是一个更直观的名称库。

但除此之外,我还认为drain_filter / drain_retain都是次优的
因为他们不清楚谓词是否必须返回 true/false 才能保留/排出条目。
(drain 表示 true 确实将其删除,因为它谈到在过滤时删除元素
再培训谈论要保留哪些元素,最后生锈)


最后,使用哪个名称并不重要,如果能稳定下来就好了。

进行民意调查和/或让语言团队的某个人决定可能是推动思想前进的最佳方式?

我认为像drain_wheredrain_ifdrain_when之类的东西比drain_filter清晰得多。

@tmccombs在这 3 个中,我认为drain_where最有意义。 (因为if意味着either do the whole thing (in this case draining) or not并且when是暂时的。)
drain_filter相比,闭包返回值与drain_where相同( true用于删除元素),但名称更清楚/明确,因此它消除了意外错误解释闭包返回值含义的风险。

我认为现在不仅仅是稳定的时间。 本帖总结:

  • 是否应该添加R: RangeArgument参数?
  • 是否应该反转布尔值? (我认为当前的逻辑是有道理的:从回调返回true会导致该项目包含在迭代器中。)
  • 命名。 (我喜欢drain_where 。)

@Gankro ,你怎么看?

libs 团队对此进行了讨论,并且一致认为目前不要稳定更多类似drain的方法。 (现有的drain_filter方法可以在 Nightly 中保持不稳定。) https://github.com/rust-lang/rfcs/pull/2369建议添加另一个在丢弃时不执行任何操作的类似排水的迭代器(而不是将迭代器消耗到最后)。

我们希望看到尝试将各种排空组合推广到较小的 API 表面的实验:

  • 子范围(通过RangeArgument aka RangeBounds )与整个集合(尽管后者可以通过传递..来实现, RangeFull类型的值)。
  • 排空所有内容(可能在该范围内)与仅匹配布尔谓词的元素
  • 放弃与不放弃(将其余元素留在集合中)。

可能性可能包括通过使其通用或构建器模式来“重载”方法。

一个限制是drain方法是稳定的。 它可以被推广,但只能以向后兼容的方式。

我们希望看到尝试将各种排空组合推广到较小的 API 表面的实验:

团队如何以及在哪里预见到这种类型的实验会发生?

方法:提出并提出具体的 API 设计,可能带有概念验证实现(可以通过至少Vec::as_mut_ptrVec::set_len在树外完成)。 去哪里也无所谓。 可能是新的 RFC 或https://internals.rust-lang.org/c/libs中的线程,并从此处链接。

我一直在玩这个。 我将在接下来的几天内打开一个关于内部的线程。

我认为像这样工作的通用 API 是有意义的:

    v.drain(a..b).where(pred)

所以它是一个构建器风格的 API:如果没有附加.where(pred) ,它将无条件地耗尽整个范围。
这涵盖了当前.drain(a..b)方法以及.drain_filter(pred)的功能。

如果名称drain因为已被使用而无法使用,则它应该是类似的名称,例如drain_iter

where方法不应命名为*_filter以避免与过滤结果迭代器混淆,尤其是当wherefilter像这样组合使用时:

    v.drain(..).where(pred1).filter(pred2)

在这里,它将使用pred1来决定什么将被排出(并在迭代器中传递),并且pred2用于过滤生成的迭代器。
pred1返回truepred2返回false的任何元素仍将从v中耗尽,但不会被这个组合迭代器。

您如何看待这种构建器风格的 API 方法?

一秒钟我忘记了where不能用作函数名,因为它已经是一个关键字:/

而且drain已经稳定了所以这个名字也不能用了..

然后我认为第二好的整体选择是保留当前的drain并将drain_filter重命名为drain_where ,以避免与.drain(..).filter()混淆。

(正如jonhoo上面所说的:)

命名这个 drain_filter 而不是 drain_where 的原因是什么? 对我来说,前者意味着整个 Vec 将被耗尽,但我们也会对结果运行过滤器(当我第一次看到它时,我想:“这不仅仅是 .drain(..).filter() ?”)。 另一方面,前者表明我们只在某些条件成立的情况下排出元素。

在 internals 上开了一个帖子
TLDR 是我认为非自耗竭是比一般情况下预期的更大的蠕虫罐,我们应该使用RangeBounds参数尽快稳定drain_filter 。 除非有人有解决那里列出的问题的好主意。

编辑:我已经上传了我的实验代码:排水实验
还有排水和清理工作台和一些测试,但不要指望干净的代码。

完全错过了这个线程。 我有一个旧的 impl,我已经修复了一些并复制粘贴以反映该线程中描述的一些选项。 我认为没有争议的关于 impl 的一件好事是它实现DoubleEndedIterator在这里查看。

@Emerentius但是我们至少应该将drain_filter重命名为drain_where ,以表明闭包必须返回true才能删除元素!

@Boscop两者都暗示true => 收益率的相同“极性”。 我个人不在乎它被称为drain_filter还是drain_where

@Popog您能总结一下差异和优缺点吗? 理想的是在内螺纹处。 我认为DoubleEndedIterator功能可以向后兼容地添加零或低开销(但我还没有测试过)。

drain_or_retain怎么样? 这是一个在语法上有意义的动作,它表明它做了一个或另一个。

@askeksa但这并不清楚从闭包返回true是否意味着“流失”或“保留”。
我认为像drain_where这样的名称,很明显返回true会耗尽它,并且每个人都应该清楚保留未耗尽的元素。

如果有某种方法可以限制/停止/取消/中止排水管,那就太好了。 例如,如果我想排空前 N 个偶数,那么能够只做vec.drain_filter(|x| *x % 2 == 0).take(N).collect() (或它的一些变体)会很好。

正如它当前实现的那样, DrainFilterdrop方法将始终运行到完成; 它不能被中止(至少我还没有想出任何可以做到这一点的技巧)。

如果你想要这种行为,你应该关闭一些跟踪你见过的状态并开始返回 false 的状态。 在 drop 时运行完成对于使适配器行为合理是必要的。

我刚刚注意到drain_filter目前实施的方式并不安全,但
实际上是安全隐患。 放松+恢复安全。 此外,它很容易导致中止,两者
其中是 std 中的方法确实不应该有的行为。 在写这篇文章时,我注意到它当前的实现是不安全的

我知道Vec默认情况下不是安全展开的,但是drain_filer的行为在
谓词恐慌非常令人惊讶,因为:

  1. 它将继续调用在 drop 时出现恐慌的闭包
    如果关闭再次出现恐慌,这将导致登机,而有些人
    像所有恐慌一样,与错误内核模式一起使用其他工作并为他们
    最终得到一个船上是相当糟糕的
  2. 如果将无法正确继续排出潜在的一个值
    并且包含一个已经丢弃的值,可能导致在释放后使用

这种行为的一个例子在这里:
play.rust-lang.org

虽然 2. 点应该是可以解决的,但我认为第一点本身应该
导致重新考虑DrainFilter的行为以运行到完成
在下降时,更改此设置的原因包括:

  • 迭代器在 rust 中是惰性的,在删除时执行迭代器是一种意想不到的行为
    源自通常的预期
  • 传递给drain_filter的谓词在某些情况下可能会出现恐慌(例如锁
    中毒)在这种情况下,当在 dropleading 期间被调用时,它很可能会再次恐慌
    双重恐慌,因此登机,这对任何使用错误内核的人来说都是非常糟糕的
    模式或最终想要以受控方式关闭,如果您使用 panic=aboard 就可以了
  • 如果您在谓词中有副作用并且不运行DrainFilter完成您可能会得到
    令人惊讶的错误,然后在删除时运行完成(但您可能已经完成
    其他人认为在将其耗尽到某个点和它被丢弃之间)
  • 您不能在不修改传递给它的谓词的情况下选择退出此行为,您
    可能无法不包装它,另一方面,您可以随时选择运行
    只需运行迭代器即可完成它(是的,最后一个参数有点
    手摇)

运行到完成的参数包括:

  • drain_filter类似于ratain是一个函数,所以人们可能会感到惊讶
    “只是” 删除DrainFilter而不是运行它完成

    • 这个论点在其他 RFC 中被多次反驳,这就是为什么#[unused_must_use]

      存在的,在某些情况下已经建议使用.for_each(drop)具有讽刺意味的是

      恰好是DrainFilter在 drop 时所做的

  • drain_filter通常只用于它的副作用,所以它是冗长的

    • 以这种方式使用它使其大致等于retain



      • 但保留使用&T , drain_filter 使用&mut T



  • 其他??
  • [编辑,稍后添加,THX @tmccombs ]:当与findallany等适配器结合使用时,不完成丢弃可能会非常令人困惑,我有充分的理由保持当前行为。

可能只是我,或者我错过了一些点,但改变了Drop行为和
添加#[unused_must_use]似乎更可取?

如果.for_each(drop)太长,我们可能会考虑为迭代器添加一个 RFC
有副作用向迭代器添加类似complete()的方法(或者drain()但是这个
是完全不同的讨论)

其他??

我找不到最初的推理,但我记得使用DrainFilter的适配器也存在一些问题,它没有运行完成。

另请参阅https://github.com/rust-lang/rust/issues/43244#issuecomment -394405057

好点,例如find会导致流失直到它击中第一个
匹配,类似allany做短路,这可能会很混乱
wrt。 流走。

嗯,也许我应该改变我的看法。 通过这可能是一个普遍的问题
迭代器有副作用,也许我们应该考虑一个通用的解决方案
(独立于这个跟踪问题)就像一个.allways_complete()适配器。

我个人没有找到任何安全原因需要运行完成,但正如我在这里写的上面的几篇文章, next()上的副作用以次优方式与适配器(例如take_while )交互peekableskip_while

这也带来了与我关于非自耗尽排水及其配套自耗尽迭代器适配器 RFC的 RFC 相同的问题。

确实drain_filter很容易导致中止,但你能举个例子说明它在哪里违反安全吗?

是的,我已经这样做了: play.rust-lang.org

这是:

#![feature(drain_filter)]

use std::panic::catch_unwind;

struct PrintOnDrop {
    id: u8
}

impl Drop for PrintOnDrop {
    fn drop(&mut self) {
        println!("dropped: {}", self.id)
    }
}

fn main() {
    println!("-- start --");
    let _ = catch_unwind(move || {
        let mut a: Vec<_> = [0, 1, 4, 5, 6].iter()
            .map(|&id| PrintOnDrop { id })
            .collect::<Vec<_>>();

        let drain = a.drain_filter(|dc| {
            if dc.id == 4 { panic!("let's say a unwrap went wrong"); }
            dc.id < 4
        });

        drain.for_each(::std::mem::drop);
    });
    println!("-- end --");
    //output:
    // -- start --
    // dropped: 0    <-\
    // dropped: 1       \_ this is a double drop
    // dropped: 0  _  <-/
    // dropped: 5   \------ here 4 got leaked (kind fine)  
    // dropped: 6
    // -- end --

}

但这是一个实现内部的想法,它出错了。
基本上,悬而未决的问题是如何处理谓词函数的panic

  1. 跳过它恐慌的元素,泄漏它并增加 del 计数器

    • 需要某种形式的恐慌检测

  2. 在调用谓词之前不要提前 idx

    • 但这意味着在 drop 时将使用相同的谓词再次调用它

另一个问题是运行可以被视为 api 用户输入的函数是否是个好主意
一般来说,但这是不使findany等行为混乱的唯一方法。

也许考虑可能是这样的:

  1. 输入next时设置一个标志,从next返回之前取消设置
  2. 如果标志仍然设置,则在丢弃时我们知道我们恐慌并因此泄漏
    剩余物品或丢弃所有剩余物品

    1. 如果您泄漏 Arc,可能会产生相当大的泄漏并产生意想不到的副作用

    2. 如果你有 Arc 和 Weak's 可能会非常令人惊讶

也许有更好的解决方案。
无论通过哪种方式,一旦实施,都应该记录在 rustdoc 中。

@dathiab

是的,我已经做到了

泄漏是不可取的,但很好,在这里可能很难避免,但绝对不是双滴。 接得好! 您要报告有关此安全问题的单独问题吗?

drain_filter每次从集合中删除一个项目时都会重新分配吗? 或者它只重新分配一次并且像 C++ 中的std::removestd::erase (成对)一样工作? 我更喜欢这样的行为,因为只有一个分配:我们只是将我们的元素放在集合的末尾,然后删除将其缩小到适当的大小。

另外,为什么没有try_drain_filter ? 如果我们应该停止,哪个返回Option类型和None值? 我有一个非常大的收藏,当我已经得到我需要的东西时,继续对我来说毫无意义。

上次我在代码中做了类似的事情:创建了一个“差距”
当移出元素并移动未排入的元素时
当它找到一个间隙时开始。 有了这个,每个元素都必须是
已移动(移出或移至阵列中的新位置)仅移动一次。
也像remove它不会重新分配。 被释放的部分就变成了
部分未使用的容量。

2018 年 8 月 10 日星期五 07:11 Victor Polevoy [email protected]写道:

drain_filter 是否在每次从中删除项目时进行重新分配
收藏? 或者它只重新分配一次并且像 std::remove 一样工作
和 C++ 中的 std::erase (成对)? 我更喜欢这种行为,因为
正好一个分配:我们只是把我们的元素放在集合的末尾
然后删除将其缩小到适当的大小。

另外,为什么没有 try_drain_filter ? 返回选项类型,以及
如果我们应该停止,没有价值? 我有一个非常大的收藏,它是
当我已经得到我需要的东西时,继续对我来说毫无意义。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-411977001
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kdOZm4bj6iR9Hj831Qh72d36BxQSks5uPRYNgaJpZM4OY1me
.

@rustonaut谢谢。 您对try_drain_filter有何看法? :)

PS 刚刚也查看了代码,它看起来就像我们想要的那样工作。

它在迭代时逐个元素地推进,所以通常你可以期待
它在被丢弃时停止迭代,但它被认为是太
令人困惑,因此它实际上在掉落时会流到最后。
(这大大增加了双重恐慌和东西的可能性
像那样)。

因此,您不太可能获得与您一样的试用版本
预计。

为了公平起见,在迭代时提前停止确实会令人困惑
在某些情况下,例如thing.drain_where(|x| x.is_malformed()).any(|x| x.is_dangerus())不会耗尽所有格式错误的情况,而是直到其中之一
发现这也很危险。 (当前的 Impl. 确实耗尽了所有格式错误的
通过在下降时继续排水)。

2018 年 8 月 10 日星期五 10:52,Victor Polevoy [email protected]写道:

@rustonaut https://github.com/rustonaut谢谢。 你有什么意见
关于 try_drain_filter? :)


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-412020490
或使线程静音
https://github.com/notifications/unsubscribe-auth/AHR0kcEMHCayqvhI6D4LK4ITG2x5di-9ks5uPUnpgaJpZM4OY1me
.

我认为这将更加通用:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

嗨,我正在搜索HashMapdrain_filter功能,但它不存在,当我找到这个时被要求打开一个问题。 它应该在一个单独的问题中吗?

目前有什么东西阻碍了它的稳定吗? 如上所述,它仍然是 unwind-unsafe 吗?

这似乎是一个很小的功能,并且已经存在一年多了。

我认为这将更加通用:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

我认为这并不比drain_filtermap的组合更好。

如上所述,它仍然是 unwind-unsafe 吗?

如果迭代过早停止,则不排出所有匹配元素,或者如果在DrainFilter下降时完成过滤和排出到最后,则在展开期间可能出现恐慌,这似乎是一个艰难的选择。
我认为无论哪种方式,此功能都存在很多问题,因此不应稳定。

让它在放松时表现不同有什么特别的问题吗?

可能性:

  • 它可以正常运行到完成,但在展开时会留下匹配的元素(因此假定所有剩余的元素都不匹配)。
  • 它可以正常运行完成,但在展开的最后一个写入位置之后截断向量(以便假定所有剩余元素匹配)。
  • 它可以正常运行完成,但在展开时将向量截断为长度 0。

我能想到的最容易理解的反论点是drop代码取决于drain_filter通常提供的不变量(最后,vec 中的元素将正是那些条件失败)可能与使用drain_filter的代码(很可能是正常的安全代码)有任意距离。

但是,假设有这样的情况。 无论用户如何编写,这段代码都会出错。 例如,如果他们编写了一个反向循环并交换删除元素的命令式循环,那么如果他们的条件可能会恐慌并且他们的 drop impl 严重依赖于过滤条件为假,那么代码仍然存在错误。 拥有像drop_filter这样的函数,其文档可以引起对这种边缘情况的注意,相比之下似乎是一种改进

另外,谢谢,我在线程的前面发现了这个操场示例,它表明当前的实现仍然是双删除元素。 (所以它肯定不能按原样稳定!)

可能值得为健全性错误打开一个单独的问题? 然后可以将其标记为 I-unsound。

据我所知,您无法标记或像双重恐慌一样不健全_is sound_
只是非常不方便,因为它中止了。 另外据我记得
双重恐慌的可能性不是一个错误,而是隐含的行为,但是
故意选择。

选项基本上是:

  1. 不要在下降时运行完成。
  2. 运行到完成,但可能由于双重恐慌而中止
  3. 在恐慌期间删除所有“未检查”的元素。
  4. 不要在恐慌期间完成掉落。

问题是:

  1. => 许多用例中的意外行为。
  2. => 如果谓词可以恐慌,则意外中止,特别是如果您使用它
    “只是”删除元素,即您不使用返回的迭代器。
  3. => 陷入恐慌和陷入恐慌之间的意外差异。 只是
    考虑某人在 drop 函数中_using_ drain_filter。
  4. => 见 3。

或者换句话说 1.在正常使用情况下导致混乱,2.可以导致
如果谓词可以恐慌 3.,4 则中止。 让你不能真的
在 drop 方法中使用它,但是你现在如何使用你在那里使用的函数
内部不使用它。

由于此选项 3.,4。 是不行的。 选项 2. 的问题是
比 1. 中的更稀有,所以选择了 2.。

恕我直言,最好有一个不运行的 drain+drain_filter API
在 drop 上完成 + 一个通用的迭代器组合器,它运行到
删除完成 + 一个完成迭代器但只是删除所有的方法
剩余物品。 问题是drain已经稳定了,迭代器
组合器增加了开销,因为它需要融合内部迭代器和排水器
可能不是最合适的名称。

2019 年 5 月 20 日星期一 09:28 Ralf Jung [email protected]写道:

可能值得为健全性错误打开一个单独的问题? 这样可以
然后被标记为 I-unsound。


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244?email_source=notifications&email_token=AB2HJEL7FS6AA2A2KF5U2S3PWJHK7A5CNFSM4DTDLGPKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVX5RWA#issuecomment-4938
或使线程静音
https://github.com/notifications/unsubscribe-auth/AB2HJEMHQFRTCH6RCQQ64DTPWJHK7ANCNFSM4DTDLGPA
.

是不健全的。

为健全性问题创建#60977

谢谢,我觉得将双重下降读作双重恐慌很愚蠢 :man_facepalming: 。

  1. => 陷入恐慌和陷入恐慌之间的意外差异。 只是
    考虑某人在 drop 函数中_using_ drain_filter。

3.,4. 让你不能真的
在 drop 方法中使用它,但是你现在如何使用你在那里使用的函数
内部不使用它。

这对我来说仍然不是什么大问题。

如果有人在 Drop impl 中使用drain_filter的条件可能会出现恐慌,那么问题不在于他们选择使用drain_filter 同样,方法是否在内部使用drain_filter也没有关系;

抱歉,我回复得太早了。 我想我现在明白你的意思了。 我会再考虑一下。

好的,所以您的论点是,如果在展开期间碰巧运行,使用drain_filterdrop impl 中的代码可能会神秘地中断。 (这不是关于drain_filter恐慌,而是关于导致drain_filter运行的其他代码恐慌):

impl Drop for Type {
    fn drop(&mut self) {
        self.vec.drain_filter(|x| x == 3);

        // Do stuff that assumes the vector has no 3's
        ...
    }
}

这个 drop impl 会在 unwind 期间突然出现异常。

这确实是一个令人信服的论点,反对让 DrainFilter 天真地检测当前线程是否处于恐慌状态。

drain_filter术语对我来说是最重要的。 鉴于我们已经有drain来删除所有项目,选择要删除的项目将是filter 。 当配对在一起时,命名感觉非常一致。

在谓词出现恐慌的情况下,双重恐慌健全性问题的修复保留了Vec的其余部分。 这些项目被向后移动以填补空白,但否则会被单独留下(并在展开期间通过Vec::drop丢弃,或者如果捕捉到恐慌则由用户以其他方式处理)。

过早删除vec::DrainFilter会继续表现得就像它被完全消耗掉一样(这与vec::Drain相同)。 如果谓词在vec::Drain::drop期间发生恐慌,则其余项目将正常后移,但不会删除更多项目并且不会再次调用谓词。 无论是在正常消费期间发生谓词恐慌还是在删除vec::DrainFilter时,它的行为本质上都是相同的。

假设对健全性漏洞的修复是正确的,还有什么阻碍了此功能的稳定性?

Vec::drain_filter可以独立于LinkedList::drain_filter稳定吗?

drain_filter术语的问题在于,对于drain_filter ,实际上有两个输出:返回值和原始集合,并且不清楚“过滤”的哪一侧项目继续。 我认为即使是filtered_drain也更清楚一点。

目前还不清楚“过滤”项目在哪一边

Vec::drain开创了先例。 您指定要_remove_ 的项目范围。 Vec::drain_filter的操作方式相同。 您确定要_删除_的项目。

并且不清楚“过滤”项目在哪一边

我认为这对于Iterator::filter来说已经是正确的,所以我已经让自己不得不查看文档/每当我使用它时编写一个测试。 我不介意drain_filter


我希望我们选择了 Ruby 的selectreject术语,但那艘船早就航行了。

这方面有什么进展吗? 名字是唯一仍然悬而未决的东西吗?

有什么阻止它稳定下来吗?

看起来DrainFilterDrop impl 将泄漏项目,如果它们的任何析构函数恐慌谓词恐慌。 这是https://github.com/rust-lang/rust/issues/52267 的根本原因。 我们确定要稳定这样的 API 吗?

没关系,这显然是由https://github.com/rust-lang/rust/pull/61224修复的。

还要稍微探讨一下这个跟踪问题,希望看到这个功能稳定运行 :D 是否有任何拦截器?

抄送@Dylan-DPC

是否曾经做出过支持或反对让drain_filter采用RangeBounds参数的决定,就像drain那样? 当你想过滤整个向量时,传递..似乎很容易,所以我可能会赞成添加它。

我认为这将更加通用:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F> where F: FnMut(T) -> Option<T>

几乎,更通用的版本需要FnMut(T) -> Option<U> ,就像Iterator::{filter_map, find_map, map_while}一样。 我不知道以这种方式概括filter_map是否值得,但它可能值得考虑。

我来到这里是因为我或多或少地看起来与上面建议的@jethrogb方法完全一致:

fn drain_filter_map<F>(&mut self, f: F) -> DrainFilterMap<T, F>
    where F: FnMut(T) -> Option<T>

与我的想法(我在脑海中称之为update )的唯一区别是我没有想过让它返回一个耗尽的迭代器,但这似乎是一个明显的改进,因为它提供了一个单一的、相当简单的接口,它支持就地更新项目、删除现有项目以及将删除的项目交付给调用者。

几乎,更通用的版本会采用FnMut(T) -> Option<U> ,就像Iterator::{filter_map, find_map, map_while}一样。 我不知道以这种方式概括filter_map是否值得,但它可能值得考虑。

该函数必须返回Option<T> ,因为它产生的值存储在Vec<T>中。

@johnw42我不确定我是否遵循,迭代器不会立即产生Some值吗?

实际上,我猜该函数的输入值仍然需要是&T&mut T而不是T以防您不想耗尽它。 或者,函数可能类似于FnMut(T) -> Result<U, T> 。 但我不明白为什么项目类型不能是其他类型。

@timvermeulen我认为我们对提案的解释不同。

我解释它的方式, Some值存储回VecNone意味着原始值由迭代器产生。 这允许闭包更新适当的值,或者将其移出Vec 。 在写这篇文章时,我意识到我的版本并没有真正添加任何东西,因为你可以用drain_filter来实现它:

fn drain_filter_map<F>(
    &mut self,
    mut f: F,
) -> DrainFilter<T, impl FnMut(&mut T) -> bool>
where
    F: FnMut(&T) -> Option<T>,
{
    self.drain_filter(move |value| match f(value) {
        Some(new_value) => {
            *value = new_value;
            false
        }
        None => true,
    })
}

相反,我认为您的解释不是很有用,因为它相当于映射drain_filter的结果,但我尝试编写它,但它不是,出于同样的原因filter_map is' t 相当于调用filter后跟map

@johnw42啊,是的,我以为您希望None表示该值应保留在Vec中。

所以看起来FnMut(T) -> Result<U, T>是最通用的,虽然它可能不是很符合人体工程学。 FnMut(&mut T) -> Option<U>实际上并不是一个真正的选择,因为在一般情况下,这不允许您获得T的所有权。 我认为FnMut(T) -> Result<U, T>FnMut(&mut T) -> bool是唯一的选择。

@timvermeulen我早些时候开始谈论“最通用”的解决方案,我的“最通用”解决方案与您的不同,但我得出了相同的结论,即试图使函数过于通用会导致您实际上并不想使用。

尽管制作一个非常通用的方法可能仍然有一些价值,高级用户可以在其上构建更好的抽象。 据我所知, draindrain_filter的意义不在于它们是特别符合人体工程学的 API——它们不是——而是它们支持发生在练习,如果没有大量冗余动作(或使用不安全的操作),就无法以任何其他方式编写。

使用drain ,您可以获得以下不错的属性:

  • 可以删除任何连续选择的元素。
  • 删除删除的项目就像丢弃返回的迭代器一样简单。
  • 被移除的项目不必被丢弃; 调用者可以单独检查每一个并选择如何处理它。
  • Vec的内容不需要支持CopyClone
  • Vec本身不需要分配或释放内存。
  • Vec中的值最多移动一次。

使用drain_filter ,您可以从Vec中删除任意一组项目,而不仅仅是一个连续的范围。 一个不太明显的优势是,即使删除了连续的项目范围,如果找到传递给drain的范围将涉及对Vec进行单独传递, drain_filter仍然可以提供性能提升Vec检查其内容。 因为闭包的参数是&mut T ,所以甚至可以更新Vec中留下的项目。 万岁!

以下是您可能希望使用drain_filter之类的就地操作执行的其他一些操作:

  1. 在通过迭代器返回它们之前转换已删除的项目。
  2. 提前中止操作并报告错误。
  3. 不只是删除项目或将其留在原处(可能会改变它),而是添加删除项目并将其替换为新值(可能是原始值的克隆,或完全是其他东西)的能力。
  4. 用多个新项目替换已删除的项目。
  5. 对当前项目进行操作后,跳过一些后续项目,将它们留在原处。
  6. 在对当前项目进行操作后,删除一些后续项目而不先检查它们。

这是我对每一个的分析:

  1. 这并没有添加任何有用的东西,因为调用者已经可以在迭代器返回时转换项目。 这也有点违背了迭代器的目的,即只有在将值从Vec中删除后才将它们传递给调用者,从而避免克隆值。
  2. 在某些情况下,能够提前中止可能会提高渐近复杂性。 作为 API 的一部分报告错误并没有增加任何新内容,因为您可以通过让闭包改变捕获的变量来做同样的事情,甚至不清楚如何去做,因为直到迭代器之后才会产生值被消耗。
  3. 我认为,这增加了一些真正的普遍性。
  4. 这就是我最初打算提出的“厨房水槽”选项,但我认为它没有帮助,因为如果单个项目可以被多个项目替换,则无法维护Vec中的项目的属性Vec更有效,而且可能会更糟。
  5. 如果Vec的组织方式使您可以跳过大部分项目而无需停下来检查它们,这可能会有所帮助。 我没有在下面的示例代码中包含它,但可以通过更改闭包来支持它,以返回一个额外的usize ,指定在继续之前要跳过以下项目的数量。
  6. 这似乎是对第 5 项的补充,但如果您仍需要通过迭代器返回已删除的项,则它不是很有用。 但是,如果您要删除的项目没有析构函数并且您只想让它们消失,它可能仍然是一个有用的优化。 在这种情况下,上面的usize可以替换为Keep(usize)Drop(usize)的选择(其中Keep(0)Drop(0)在语义上是相等的)。

我认为我们可以通过让闭包返回一个包含 4 个案例的枚举来支持基本用例:

fn super_drain(&mut self, f: F) -> SuperDrainIter<T>
    where F: FnMut(&mut T) -> DrainAction<T>;

enum DrainAction<T>  {
    /// Leave the item in the Vec and don't return anything through
    /// the iterator.
    Keep,

    /// Remove the item from the Vec and return it through the
    /// iterator.
    Remove,

    /// Remove the item from the Vec, return it through the iterator,
    /// and swap a new value into the location of the removed item.
    Replace(T),

    /// Leave the item in place, don't return any more items through
    /// the iterator, and don't call the closure again.
    Stop,
}

我想提出的最后一个选项是完全摆脱迭代器,按值将项目传递给闭包,并允许调用者通过将其替换为自身来保持项目不变:

fn super_drain_by_value(&mut self, f: F)
    where F: FnMut(T) -> DrainAction<T>;

enum DrainAction<T>  {
    /// Don't replace the item removed from the Vec.
    Remove,

    /// Replace the item removed from the Vec which a new item.
    Replace(T),

    Stop,
}

我最喜欢这种方法,因为它很简单并且支持所有相同的用例。 潜在的缺点是即使大部分项目都留在原地,它们仍然需要移动到闭包的堆栈框架中,然后在闭包返回时移回。 人们希望当闭包返回它的参数时,这些动作可以可靠地优化掉,但我不确定这是否是我们应该指望的东西。 如果其他人足够喜欢它来包含它,我认为update将是一个好名字,因为如果我没记错的话,它可以用于实现任何单通道就地更新Vec的内容。

(顺便说一句,我完全忽略了上面的链表,因为我完全忘记了它们,直到我查看了这个问题的标题。如果我们谈论的是链表,它会改变第 4-6 点的分析,所以我认为不同API 适用于链表。)

@johnw42你已经可以做 3. 如果你有一个可变引用,使用mem::replacemem::take

@johnw42 @jplatte

(3) 只有当我们允许返回的Iterator的项目类型与集合的项目类型不同时才真正有意义。
(3) 是一种特殊情况,因为您既从Iterator中返回元素,又将新元素放回Vec中。

Bikeshedding:我有点反转Replace(T)的功能并将其替换为PushOut(T) ,目的是将PushOut的内部值“提交”到迭代器,同时保持Vec中的原始(参数)项。

Stop应该可能具有返回Error类型的能力(或者有点像try_fold ?)。

我昨晚实现了我的super_drain_by_value函数,我学到了一些东西。

标题项目可能应该是,至少在Vec方面,我们在这里谈论的所有内容都属于“很高兴拥有”类别(而不是添加一个全新的功能),因为Vec本质上已经通过现有 API 提供了对其所有字段的直接读写访问权限。 在稳定版本中,有一个小警告,您无法观察空Vec的指针字段,但不稳定的into_raw_parts方法消除了该限制。 我们真正谈论的是扩展可以由安全代码有效执行的操作集。

在代码生成方面,我发现在简单的情况下(例如Vec<i32> ),进出Vec的冗余移动不是问题,并且调用相当于简单的事情,例如无操作或截断Vec被转换为太简单而无法改进的代码(分别为零和三个指令)。 坏消息是,对于更难的情况,我的建议和drain_filter方法都做了很多不必要的复制,很大程度上违背了这些方法的目的。 我通过查看为Vec<[u8; 1024]>生成的汇编代码对此进行了测试,在这两种情况下,每次迭代都有两个未优化的memcpy调用。 即使是无操作调用最终也会复制整个缓冲区两次!

在人机工程学方面,我的 API 乍一看还不错,但在实践中并不那么好; 除了最简单的情况外,从闭包返回枚举值变得非常冗长,而我提出的闭包返回一对枚举值的变体甚至更丑陋。

我还尝试扩展DrainAction::Stop以携带从super_drain_by_value返回的R值作为Option<R> ,这更糟糕,因为在(可能是典型的)在不需要返回值的情况下,编译器无法推断R并且您必须显式注释您甚至不使用的值的类型。 出于这个原因,我认为支持将一个值从闭包返回给super_drain_by_value的调用者不是一个好主意; 这大致类似于为什么loop {}构造可以返回一个值,但任何其他类型的循环的计算结果都是()

关于一般性,我意识到实际上有两种提前终止的情况:一种是Vec的其余部分被丢弃,另一种是保留原位。 如果提前终止不带值(因为我认为它不应该),它在语义上等同于返回Keep(n)Drop(n) ,其中n是尚未审查的项目。 但是,我确实认为过早终止应该被视为一个单独的案例,因为与使用Keep / Drop相比,它更容易使用通过更简单的代码路径。

为了使 API 更友好一点,我认为更好的选择是让闭包返回()并传递一个帮助对象(我将在此将其称为“更新程序”),它可以是用于检查Vec的每个元素并控制它发生的情况。 这些方法可以有熟悉的名称,如borrowborrow_muttake ,以及额外的方法,如keep_next(n)drop_remainder() 。 使用这种 API,闭包在简单的情况下要简单得多,而在复杂的情况下不会更复杂。 通过让大多数更新程序的方法按值获取self ,很容易防止调用者执行诸如多次调用take之类的事情,或者在后续迭代中给出相互冲突的指令。

但我们仍然可以做得更好! 今天早上我意识到,通常情况下,这个问题类似于已经用函数式语言明确解决的问题,我们可以用类似的解决方案来解决它。 我说的是“拉链”API,首先在这篇简短的论文中使用 OCaml 中的示例代码进行了描述,并在此处使用 Haskell 代码和其他相关论文的链接进行了描述。 拉链提供了一种非常通用的方法来遍历数据结构并使用特定数据结构支持的任何操作“就地”更新它。 另一种思考方式是,拉链是一种涡轮增压迭代器,具有用于对特定类型的数据结构执行操作的额外方法。

在 Haskell 中,通过将 zipper 设置为 monad 来获得“就地”语义; 在 Rust 中,您可以使用生命周期来做同样的事情,方法是让拉链持有对Vecmut引用。 Vec的拉链与我上面描述的更新程序非常相似,只是Vec只是提供了一种在自身上创建拉链的方法,而不是将其重复传递给闭包,并且只要Vec存在,拉链就可以独占访问它。 然后调用者负责编写一个循环来遍历数组,在每个步骤中调用一个方法来从Vec中删除当前项目,或者将其保留在原处。 提前终止可以通过调用使用拉链的方法来实现。 因为循环在调用者的控制之下,所以可以在循环的每次迭代中处理多个项目,或者在根本不使用循环的情况下处理固定数量的项目。

这是一个非常人为的例子,展示了拉链可以做的一些事情:

/// Keep the first 100 items of `v`.  In the next 100 items of `v`,
/// double the even values, unconditionally keep anything following an
/// even value, discard negative values, and move odd values into a
/// new Vec.  Leave the rest of `v` unchanged.  Return the odd values
/// that were removed, along with a boolean flag indicating whether
/// the loop terminated early.
fn silly(v: &mut Vec<i32>) -> (bool, Vec<i32>) {
    let mut odds = Vec::new();
    // Create a zipper, which get exclusive access to `v`.
    let mut z = v.zipper();
    // Skip over the first 100 items, leaving them unchanged.
    z.keep_next(100);
    let stopped_early = loop {
        if let Some(item /* &mut i32 */) = z.current_mut() {
            if *item < 0 {
                // Discard the value and advance the zipper.
                z.take();
            } else if *item % 2 == 0 {
                // Update the item in place.
                *item *= 2;

                // Leave the updated item in `v`.  This has the
                // side-effect of advancing `z` to the next item.
                z.keep();

                // If there's another value, keep it regardless of
                // what it is.
                if z.current().is_some() {
                    z.keep();
                }
            } else {
                // Move an odd value out of `v`.
                odds.push(z.take());
            }
            if z.position() >= 200 {
                // This consumes `z`, so we must break out of the
                // loop!
                z.keep_rest();
                break true;
            }
        } else {
            // We've reached the end of `v`.
            break false;
        }
    }
    (stopped_early, odds)

    // If the zipper wasn't already consumed by calling
    // `z.keep_rest()`, the zipper is dropped here, which will shift
    // the contents of `v` to fill in any gaps created by removing
    // values.
}

为了比较,这里使用drain_filter或多或少是相同的功能,只是它只是假装提前停止。 它的代码量大致相同,但恕我直言,它更难阅读,因为闭包返回的值的含义并不明显,并且它使用可变布尔标志将信息从一个迭代传递到下一个迭代,其中拉链版本通过控制流实现了同样的目标。 因为删除的项目总是由迭代器产生,我们需要一个单独的过滤步骤来从输出中删除负值,这意味着我们需要在两个地方而不是一个地方检查负值。 它必须跟踪v中的位置也有点难看; drain_filter的实现具有该信息,但调用者无权访问它。

fn drain_filter_silly(v: &mut Vec<i32>) -> (bool, Vec<i32>) {
    let mut position: usize = 0;
    let mut keep_next = false;
    let mut stopped_early = false;
    let removed = v.drain_filter(|item| {
        position += 1;
        if position <= 100 {
            false
        } else if position > 200 {
            stopped_early = true;
            false
        } else if keep_next {
            keep_next = false;
            false
        } else if *item >= 0 && *item % 2 == 0 {
            *item *= 2;
            false
        } else {
            true
        }
    }).filter(|item| item >= 0).collect();
    (stopped_early, removed)
}

@johnw42你之前的帖子让我想起了scanmut crate ,特别是Remover struct ,你提到的“拉链”概念似乎非常相似! 这似乎比在您想要完全控制时关闭的方法更符合人体工程学。

无论哪种方式,这可能与drain_filter是否应该稳定不太相关,因为我们以后总是可以换掉内部结构。 drain_filter本身总是非常有用的,因为它非常方便。 在稳定之前我仍然希望看到的唯一变化是RangeBounds参数。

@timvermeulen我认为添加RangeBounds参数是有意义的,但保留当前的闭包签名( F: FnMut(&mut T) -> bool )。
您始终可以使用filter_map或您想要的任何内容对耗尽的元素进行后处理。
(对我来说,闭包允许改变元素非常重要,因为retain不允许它(在发现这个错误之前它已经稳定了)。)

是的,这似乎是便利性和实用性之间的完美平衡。

@timvermeulen是的,我偏离了主要话题。

我注意到与原始主题相关的一件事是,很难记住闭包的返回值是什么意思——它是说是保留项目还是删除它? 我认为文档指出v.drain_filter(p)等效于具有副作用的v.iter().filter(p)会很有帮助。

对于filter ,为了清晰起见,使用布尔值仍然不太理想,但它是一个非常知名的函数,恕我直言,谓词回答“我应该保留这个吗?”这个问题至少有点直观。 而不是“我应该丢弃这个吗?” 对于drain_filter ,如果您从迭代器的角度考虑,同样的逻辑适用,但如果您从输入Vec的角度考虑,问题就变成“我应该不拿着吧?”

至于确切的措辞,我建议将filter参数重命名为predicate (以匹配Iterator::filter )并在说明中的某处添加这句话:

要记住predicate的返回值是如何使用的,请记住drain_filterIterator::filter相同,但有额外的副作用是删除选定的self中的项目。

@johnw42是的,好点。 我认为像drain_where这样的名称会更清晰。

如果您要命名自行车脱落; 确保您已阅读所有评论; 甚至隐藏的。 已经提出了许多变体,例如https://github.com/rust-lang/rust/issues/43244#issuecomment -331559537

但是……它必须命名为draintain() ! 没有其他名字这么漂亮!

对这个问题挺感兴趣的,也看了整个帖子,不妨试着总结一下大家说的,希望能帮助稳定一下。 在此过程中,我添加了一些我自己的评论,但我尽量保持中立。

命名

以下是我看到的提议名称的非主观摘要:

  • drain_filter :当前实现中使用的名称。 与其他名称一致,例如filter_map 。 具有类似于drain().filter()的优点,但有更多的副作用。
  • drain_where :具有指示true是否会导致耗尽 _out_ 或过滤 _in_ 的好处,这对于其他名称可能很难记住。 std中没有_where后缀的先例,但类似后缀的先例很多。
  • drain().where()的变体,因为where已经是关键字。
  • drain_retain :与retain一致,但retaindrain对闭包返回的布尔值有相反的解释,这可能会造成混淆。
  • filtered_drain
  • drain_if
  • drain_when
  • remove_if

参数

为了与drain保持一致,可能值得添加一个范围参数。

建议使用两种闭包格式, FnMut(&mut T) -> boolFnMut(T) -> Result<T, U> 。 后者更灵活,但也更笨拙。

讨论了反转布尔条件( true表示“保留在Vec中”)以与retain一致,但随后与drain不一致true的意思是“从Vec中排出”)。

放卷

当过滤器关闭恐慌时, DrainFilter迭代器被丢弃。 然后迭代器应该完成Vec的耗尽,但要这样做,它必须再次调用过滤器闭包,冒着双重恐慌的风险。 有一些解决方案,但都是妥协:

  • 不要在下降时完成排水。 当与诸如findall之类的适配器一起使用时,这是非常违反直觉的。 此外,它使v.drain_filter(...);习语无用,因为迭代器是惰性的。

  • 总是在下降时完成排水。 这有双重恐慌(导致中止)的风险,但会使行为保持一致。

  • 如果当前未展开,则仅在下降时完成排水。 这完全修复了双重恐慌,但使drain_filter的行为变得不可预测:在析构函数中删除DrainFilter可能_有时_实际上并没有完成它的工作。

  • 如果过滤器关闭没有恐慌,则仅在滴下时完成排水。 这是drain_filter当前做出的妥协。 这种方法的一个很好的特性是过滤器关闭“短路”中的恐慌,这可以说是非常直观的。

请注意,只要DrainFilter结构被删除,当前的实现就不会泄漏(尽管它可能会导致中止)。 不过,以前的实现并不安全/无泄漏。

滴漏

DrainIter可以在源向量被丢弃时完成耗尽,或者它只能在调用next时耗尽(惰性迭代)。

支持滴漏的论据:

  • drain的行为一致。

  • 与其他适配器交互良好,例如allanyfind等...

  • 启用vec.drain_filter(...);成语。

  • 可以通过drain_lazy样式的方法或 $#$ DrainIter $#$ 上的lazy()适配器显式启用惰性功能(甚至在Drain上,因为它向后兼容添加方法)。

支持惰性迭代的论点:

  • 与几乎所有其他迭代器一致。

  • “drain-on-drop”功能可以通过DrainIter上的适配器显式启用,甚至可以通过通用的Iterator::exhausting适配器(参见 RFC #2370 )。

我可能错过了一些东西,但至少我希望它在浏览线程时对新手有所帮助。

@negamartin

drain-on-drop选项不会要求迭代器返回对项目的引用而不是拥有的值吗? 我认为这将使得不可能使用drain_filter作为一种机制来删除和获取符合特定条件的项目(这是我最初的用例)的所有权。

我不这么认为,因为当前实现的行为在产生拥有值的同时精确地漏电。 无论哪种方式,我都看不出drain-on-drop 需要借用元素,所以我认为我们对drain-on-drop 的含义有两种不同的想法。

为了清楚起见,当我说 drain-on-drop 时,我仅指迭代器未完全消耗时的行为:即使迭代器未完全消耗,是否应该排出与闭包匹配的所有项目? 还是只到被消耗的元素,其余的保持不变?

特别是,它是以下之间的区别:

let mut v = vec![1, 5, 3, 6, 4, 7];
v.drain_where(|e| *e > 4).find(|e| *e == 6);

// Drain-on-drop
assert_eq!(v, &[1, 3, 4]);

// Lazy
assert_eq!(v, &[1, 3, 4, 7]);

只是抛出一个想法,但另一个可能的 API 可能是这样的:

 fn drain_filter_into<F, D>(&mut self, filter: F, drain: D)
        where F: FnMut(&mut T) -> bool, 
                   D: Extend<T>
    { ... }

它不如其他选项灵活,但确实避免了当DrainFilter被删除时要做什么的问题。

在我看来,整件事在我看来越来越不像retain_mut()retain()带有传递给闭包的可变引用),这就是它首先要提供的. 除了设计过滤排水器之外,我们现在可以提供retain_mut()吗? 还是我错过了什么?

@巴特梅西

这是它首先要提供的。

我认为情况并非如此。 我专门使用drain_filter根据过滤条件获取项目的所有权。 DrainDrainFilter产生了 as retain 没有的项目。

@negamartin

为了清楚起见,当我说滴漏时,我只是指迭代器未完全消耗时的行为

喔好吧。 那是我的错误。 我误解了你的定义。 我将其解释为“在删除之前没有从 vec 中删除任何内容”,这实际上没有任何意义。

支持惰性迭代的论据

我认为它需要与drain保持一致。 Iterator::exhausting RFC 未被接受,如果draindrain_filter具有看似相反的流失行为,那将是非常奇怪的。

@negamartin

drain_filter:当前实现中使用的名称。 与其他名称一致,例如 filter_map。 具有类似于drain().filter()的优点,但有更多的副作用。

它不是类似的(这就是我们需要retain_mut / drain_filter的原因):
drain().filter()甚至会耗尽过滤器闭包返回的那些元素false

我刚刚注意到 lib 团队在#RFC 2870中的评论中的一小行:

可能性可能包括通过使其通用或构建器模式来“重载”方法。

如果一个方法仍然接受以前的具体类型,它是否向后兼容? 如果是这样,我相信这将是最好的前进方式。

(构建器模式对迭代器有点不直观,因为迭代器上的方法通常是适配器,而不是行为改变器。此外,没有先例,例如chunkschunks_exact是两个独立的方法,而不是chunks().exact()组合。)

不,就我所知的类型推断而言,不是当前的设计
由于类型不明确,以前工作过现在可能会失败。 默认的遗传学
函数类型会有所帮助,但要正确执行非常棘手。

2020 年 6 月 12 日星期五 21:21,negamartin [email protected]写道:

我刚刚注意到 lib 团队在 #RFC 2870 中的评论中有一小行
https://github.com/rust-lang/rfcs/pull/2369

可能性可能包括通过使其通用来“重载”方法,
或建造者模式。

如果一个方法仍然接受,它是否向后兼容?
以前的具体类型? 如果是这样,我相信这将是最好的方法
向前。

(构建器模式对于迭代器来说有点不直观,因为方法
迭代器通常是适配器,而不是行为改变者。 此外,还有
没有先例,例如 chunks 和 chunks_exact 是两个独立的
方法,而不是 chunks().exact() 组合。)


你收到这个是因为你被提到了。
直接回复此邮件,在 GitHub 上查看
https://github.com/rust-lang/rust/issues/43244#issuecomment-643444213
或退订
https://github.com/notifications/unsubscribe-auth/AB2HJELPWXNXJMX2ZDA6F63RWJ53FANCNFSM4DTDLGPA
.

如果一个方法仍然接受以前的具体类型,它是否向后兼容? 如果是这样,我相信这将是最好的前进方式。

不,因为它在某些情况下会破坏类型推断。 例如, foo.method(bar.into())将与具体类型参数一起使用,但不适用于泛型参数。

我认为现在实现的 drain_filter 非常有用。 能不能稳定下来? 如果将来发现更好的抽象,也没有什么能阻止它们被引入。

我需要启动什么过程来尝试添加retain_mut() ,而与drain_filter()发生的任何事情无关? 在我看来,要求已经出现了分歧,无论drain_filter()发生什么, retain_mut()仍然有用。

@BartMassey对于新的不稳定库 API,我认为制作一个带有实现的 PR 应该没问题。 在https://rustc-dev-guide.rust-lang.org/implementing_new_features.html 和 https://rustc-dev-guide.rust-lang.org/getting-started.html实现该功能的说明

今天我一直在HashMapBTreeMap之间的 API 差异而苦苦挣扎,我只想分享一个警告,我认为各种集合努力保持一致的 API 非常重要感觉,在这一点上并非总是如此。

例如 String、Vec、HashMap、HashSet、BinaryHeap 和 VecDeque 有retain方法,但 LinkedList 和 BTreeMap 没有。 我觉得这特别奇怪,因为对于 LinkedList 或 Map 来说, retain似乎比随机删除是一项非常昂贵的操作的向量更自然。

当您深入挖掘时,它会更加令人困惑: HashMap::retain的闭包传递了可变引用中的值,但其他集合获得了不可变引用(而 String 获得了简单的char )。

现在我看到像drain_filter这样的新 API 正在被添加,其中 1/ 似乎与retain重叠,并且 2/ 对于所有集合都不是同时稳定的:

  • HashMap::drain_filter在上游存储库中,但尚未随 Rust 的 std AFAIK 一起提供(它没有出现在文档中)
  • BTreeMap::drain_filter , Vec::drain_filter , LinkedList::drain_filter在 Rust 的标准中,但功能有门控
  • VecDeque::drain_filter似乎根本不存在,它没有出现在文档中
  • String::drain_filter也不存在

我对实现这些功能的最佳方式没有强烈的意见,或者我们是否需要drain_filterretain或两者兼而有之,但我非常坚信这些 API 应该在集合之间保持一致.

也许更重要的是,不同集合的相似方法应该具有相同的语义。 retain的当前实现违反了 IMO。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

thestinger picture thestinger  ·  234评论

nikomatsakis picture nikomatsakis  ·  259评论

nikomatsakis picture nikomatsakis  ·  331评论

Mark-Simulacrum picture Mark-Simulacrum  ·  681评论

cramertj picture cramertj  ·  512评论