Rust迭代器和C++迭代器有哪些主要区别?

19

C++ 迭代器的典型例子是指针,可用于指向 C 数组中的元素,如下所示:

int array[] = {1, 2, 3, 4};
int* begin = std::begin(array); //Starting iterator
int* end = std::end(array) //Ending iterator

for(int* i = begin; i < end; i++)
{
    std::cout << *i << ',';
}

//Prints 1, 2, 3, 4

这很简单。来自cplusplus.com的迭代器定义为:

迭代器是指向一些元素(如数组或容器)中某个元素的任何对象,具有使用一组运算符遍历该范围元素的能力...

这很有道理;在上面的代码中有两个迭代器(即`begin`和`end`迭代器),它使用了一个`for`循环并进行了递增。

在Rust中,可以这样使用迭代器:

let vect = vec![1, 2, 3, 4];

let vect_iter = vect.iter();

什么?要迭代它,您需要执行以下操作:

vect_iter.next();
vect_iter.next();

在 Rust 的文档中我找不到指针的确切定义,但是看一下 Iterator trait,似乎迭代器是一个包装容器的东西,通过以某种方式标准化逻辑来更轻松地进行处理(如果这样说有意义的话)。

我主要的问题是:

  1. 它们的主要区别是什么?
  2. Rust为什么会以这种方式使用迭代器,它们为什么表现得如此不同?
  3. 在 C++ 中是否有类似 Rust 的迭代器?
  4. 在 Rust 中是否有类似 C++ 的迭代器?
  5. 它们被称为特定的名称吗?(内部/外部?)


6
两种不同的语言,语法不同等。 - PaulMcKenzie
1
Python的方法与Rust非常相似。您可以在迭代器中包装更多逻辑,因此它知道何时结束而无需将其与哨兵值(C ++中的结束迭代器)进行比较。 C ++的方法可能有点繁琐,因为实用程序函数最终需要单独为开始和结束迭代器提供参数,而不是仅接收一个知道自己结束点的迭代器。 - ShadowRanger
1
那么在C++中是否有这些类型的迭代器呢?因为我同意@ShadowRanger的观点,它非常繁琐,而且我认为Rust的方法更好...但我认为这并不是事实,因为很多函数需要单独拥有开始和结束迭代器...此外,我已经进行了一些研究,并看到这些可能被称为内部和外部迭代器...但这完全正确吗? - The_Babu
2
C++11添加了范围for:http://en.cppreference.com/w/cpp/language/range-for 至少朝着正确的方向前进。 - loganfsmyth
  1. 在Rust中,像我这样的业余新手可以在学习了一两个小时后为自己的自定义数据类型创建一个迭代器,一旦编译成功,它通常就能够“正常工作”。然后,我可以使用内置于语言中的数十种不同的迭代器适配器(filter、map、fold),这要归功于其从函数式和不可变语言(如lisp和haskell)继承而来的特性。
  2. 在C++中,迭代器是庞大的头文件模板,严格使用时,可以避免编写可怕的for(){}块并且在循环过程中意外修改索引而导致崩溃。
- don bright
显示剩余3条评论
3个回答

23

迭代器是编程语言中的一个概念,用于指代允许在元素集合或序列上进行迭代操作的结构。该概念本意是模糊的,它只是一个概念!它不规定任何特定的实现方式。

为了更容易区分C++和Rust,我将使用不同的名称:

  • C++ 迭代器将被命名为cursors
  • Rust 迭代器将被命名为streams

是的,这些完全是任意的。请注意,如果您查看像Java或C#这样的语言,您会发现它们也使用流(streams)。


C++

首先,请不要使用cplusplus.com。cppreference.com 更好。

迭代器是指向范围(例如数组或容器)中的某个元素的任意对象,具有使用一组运算符遍历该范围元素的能力...

简单,但错误

游标可以指向一个元素,也可以是奇异的(singular),即不指向任何元素。

通常,奇异值用于表示:

  • 要迭代的序列的末尾:vec.end()
  • 元素的缺失:std::find(...)

您可以增加(有时减少)游标。如果这样做,您通常需要一个游标对来知道何时停止。

C++为什么要使用这种表示方法?因为这就是C的实现方式,并且它运行得非常好...尽管容易出错。


Rust

Rust致力于安全并倾向于易于使用的API。这排除了一对游标:

  • 游标对不安全:您很容易迭代超出范围,以及您可以获得别名引用,
  • 一对游标容易出错:很容易将两个不同序列的游标配对错误。

为了控制边界、别名和避免配对不匹配,您必须使用单个对象;因此流式API。

Rust中的Iterator API类似于Java和C#的迭代器API,尽管Rust通过使用Option<T>改进了它,因此它提供了一个单一的方法next(),既可以推进流也可以在结束时发出信号,而不是笨拙的hasNext()/next()调用对。


结论

Rust和C++都具有迭代集合元素的方法:

  • C++提供了类似C语言的方法,灵活但容易出错,
  • Rust提供了现代化的方法,安全但不太灵活。

这两种语言还提供了外部内部迭代:

  • 外部:用户控制迭代(调用++next()),
  • 内部:迭代器控制用户代码(请参见std::foreachIterator::foreach)。

注意:现在Rust中有异步操作,因此“Stream”一词的含义完全不同。 - jhpratt

2

Rust 和 C++ 中的迭代器在概念上有很大不同。

C++

在 C++ 中,迭代器类似于指针。迭代器引用一个对象,可以通过增加操作引用到下一个对象,并且可以与其他迭代器进行比较以检查相等性。迭代器也可以不引用任何对象,它们可以引用序列中的“结束”元素,或者它们可以是“奇异”的(类似于空指针)。一些迭代器支持额外的操作,例如向前和向后移动、随机访问和复制。

C++ 中的指针是有效的迭代器,但也有其他类型的迭代器。

迭代器并不表示元素序列,至少在约定上不是这样。在 C++ 中,如果你需要一个元素序列,你需要一对迭代器*:一个用于开头,一个用于结尾。你不必按顺序迭代元素,可以做各种其他事情。例如,在 C++ 中反转数组,可以使用迭代器实现:

#include <algorithm>
#include <iterator>
#include <cstdio>
#include <utility>

template <typename T, std::size_t N>
void reverse_array(T (&arr)[N]) {
    using std::swap;
    auto left = std::begin(arr), right = std::end(arr);
    while (left < right) {
        --right;
        swap(*left, *right);
        ++left;
    }
}

int main() {
    int x[] = {1, 2, 3, 4, 5};
    reverse_array(x);
    for (const auto it : x) {
        std::printf("%d\n", it);
    }
    return 0;
}

但是您可以快速将其推广到使用双向迭代器的任何容器:

#include <algorithm>
#include <iterator>
#include <list>
#include <cstdio>
#include <utility>

template <typename Iterator>
void reverse_any(Iterator left, Iterator right) {
    using std::swap;
    while (left != right) {
        --right;
        if (left == right)
            break;
        swap(*left, *right);
        ++left;
    }
}

int main() {
    std::list<int> list{1, 2, 3, 4, 5};
    reverse_any(std::begin(list), std::end(list));
    for (const auto it : list) {
        std::printf("%d\n", it);
    }
    return 0;
}

Rust

在Rust中,迭代器类似于一个切片。迭代器指向一系列对象,并且可以使用next()方法从迭代器访问元素。从某种意义上说,这意味着Rust中的迭代器同时具有beginend迭代器。在Rust中重新实现上面的C++代码,则会得到如下结果:

fn reverse_any<'a, T: 'a, Iter>(mut iter: Iter)
where
    Iter: DoubleEndedIterator<Item = &'a mut T>,
{
    while let Some(left) = iter.next() {
        if let Some(right) = iter.next_back() {
            std::mem::swap(left, right);
        }
    }
}

fn main() {
    let mut v = [1, 2, 3, 4, 5];
    reverse_any(v.iter_mut());
    println!("{:?}", v);
}

这种方法的附加好处是安全性。迭代器失效是C++程序中最常见的错误源之一,但Rust完全消除了这个问题。

代价是,如果您想要改变元素,则在Rust中只能使用单个(可能是双端)迭代器,而在C++中,您可以有许多迭代器与同一个容器一起工作。虽然单向和双向范围是迭代器的最常见情况,但有些算法使用了C++提供的额外灵活性。

我能想到的一个简单例子是C++的std::remove_if。一个简单的remove_if实现需要使用三个迭代器:两个用于跟踪正在扫描的元素范围的迭代器,以及一个用于跟踪正在编写的元素的第三个迭代器。您可以将std::remove_if转换为Rust,但它将无法使用普通的Rust迭代器并仍然原地修改容器。

另一个简单的例子是荷兰国旗问题,通常使用三个迭代器。解决这个问题的方法经常用于快速排序的元素分区,因此这是一个重要的问题。

摘要

Rust迭代器几乎等价于C++起点+终点迭代器对。C++允许您使用多个迭代器并将迭代器向前和向后移动。Rust保证您不会意外使用无效的迭代器,但您一次只能使用一个迭代器,并且它只能向一个方向移动。

我不知道有何术语来区分这些类型的迭代器。请注意,Rust风格的迭代器更为常见,C#,Python,Java等中的迭代器工作方式相同,但可能具有略微不同的名称(在C#中称为“枚举器”)。

脚注

*:技术上这不是真的。您只需要一个迭代器,但是传统上要使用一对迭代器,并且库函数通常操作一对迭代器(因此,如果要使用这些函数,则“需要”两个迭代器)。拥有(start,end)对并不意味着序列有界,结束迭代器可以是无限远。想象一下在数学上有一个区间(0,∞)……∞不是真正的数字,它只是一个占位符,让您知道该范围在右侧是无界的。

:记住,仅因为C++中存在“end”迭代器并不意味着序列实际上有一个结束。 C++中的某些结束迭代器就像无穷大一样。 它们不指向有效的元素,无论您向前迭代多少次,都不会到达无穷大。 在Rust中,等效构造是一个永远不返回None的迭代器。


3
@Tim: 同样的情况也适用于C++。仅仅因为end被赋值并不意味着你可以通过end访问元素。举个简单具体的例子,看看std::forward_list,除了将其与其他迭代器进行比较之外,你基本上什么也做不了。另一个例子是std::istream_iterator,它只是从流中读取值,直到流结束。end可能有一个值,但它并不会给你任何额外的功能。 - Dietrich Epp
3
@Tim:同样适用于C++。唯一的要求是你可以比较startend迭代器,但并不要求它们会相等。 - Dietrich Epp
2
@Tim:基本迭代器概念的文档可以在这里找到:http://en.cppreference.com/w/cpp/concept/Iterator。这个文档可能会澄清你对C++迭代器工作方式的任何疑问。 - Dietrich Epp
4
@Tim:就像在Rust中,不是所有的迭代器都是来自容器一样,在C++中,不是所有的��代器都来自于 std::begin()std::end()。此外,并不能保证 std::end() 已被定义,即使已经定义了 std::end(),也不能保证从 std::begin() 可到达 std::end()。实际上,没有“独特”的 iteratoriterator concept,单词“iterator”指的是符合迭代器概念的对象。 - Dietrich Epp
2
成本是你在Rust中被限制为单个(可能是双端)迭代器。这是不正确的。也许你意思更微妙? - Shepmaster
显示剩余6条评论

-2

我看到这里有三件事情。让我们来分解一下。

迭代器的概念

当你在你的例子中调用C++的std::begin和Rust的.iter()时,你会收到两个“类型相同”的对象:一个迭代器。

如果我们暂时忘记实现细节,我们可以看到迭代器的目的和可用性在两种语言中都是相似的。我们发现两个迭代器:

  • 是可以从集合(“可迭代类型”)创建的“对象”
  • 可以使用C++的std::advance和Rust的.next()进行推进
  • 有一个“结束”,由C++的std::end和Rust的.next()的输出确定。

当然,这只是一个粗略的概述,它们在许多其他方面上是相似和不同的,但这可能是您正在寻找的一般概述。

迭代器的实现

尽管共享共同主题,C++和Rust是非常不同的语言,并自然地以不同的方式实现单个想法。迭代器也不例外。

在Stack Overflow上,"为什么"太过宽泛,无法得到真正的答案。这就像问为什么橙子是橙色的而香蕉不是一样 :)

但是,鉴于您的评论,您似乎对如何使用Rust的迭代器实现有些困惑:

我在Rust文档中找不到指针的确切定义

现在不要像C++程序员那样思考。如果您还没有查看The Book,请查看并探索借用和所有权的概念;这是您将处理数据的更加典型的方式,并且了解Rust的迭代器如何工作是必需的。

迭代器的语法糖

无论是C++还是Rust都在其for循环中拥有"魔法",让您轻松地使用迭代器"类型"。

与您的问题相反,这不是 Rust 独有的概念。在 C++ 中,如果对象实现了特殊方法,则可以使用现代的 for (item : collection) 语法,类似于您指出的 Iterator 特征。

摘要

主要区别是什么?

从概念上讲没有太大区别。

为什么 Rust 以这种方式拥有迭代器,它们为什么表达得如此不同?

就像它是因为它是这样的。它们比您想象的更相似。

C++ 中是否有 Rust 类型的迭代器?Rust 中是否有 C++ 类型的迭代器?

从概念上讲它们是相同的。

它们被称为某些特定的名称吗?(内部/外部?)

可能有一些高端学术术语来描述实现差异,但我不知道。一个迭代器就是一个迭代器。


5
在概念上,C++中的迭代器与Rust中的迭代器有很大的不同。C++中的迭代器更像是广义指针,你可以编写诸如std::sortstd::find_if之类的函数。在Rust中,你不能使用迭代器来表示范围,因此许多这些来自C++的技术必须被重写。 - Dietrich Epp
在问题进行编辑之前,似乎缺少迭代器的基本概念;将事情简化到基本水平似乎是合适的。我同意现在这个答案过于简单化了。 - Litty
2
@Oliv:你能否举一个在C++中可以实现最优效率但在Rust中不行的例子? - Matthieu M.
1
@DietrichEpp,您能详细说明一下“在Rust中,您根本无法使用迭代器来表示范围”这句话的意思吗?例如,这是我认为遍历集合范围的方式。您也可以直接在范围上进行迭代(for i in 0..10 {})。 - Shepmaster
@Shepmaster:你可以这样做,但是在Rust中,迭代器缺少我认为用于处理范围的许多标准操作。您无法比较它们的位置,测试一个范围是否包含在另一个范围内,或者扩展该范围。因此,迭代器可以遍历范围,但实际上不能用于表示范围本身。C++支持迭代器之间的比较,并允许您在两个方向上移动一些迭代器,这为您提供了表示范围的所有必要元素。 - Dietrich Epp
显示剩余2条评论

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