[翻译]理解Qt容器:STL VS QTL(二)——迭代器

接上文翻译,说明在上文:理解Qt容器:STL VS QTL(一)——特性总览

 

迭代器:

在Qt关联容器中的迭代器默认指向的是关联容器的value元素,不同于STL的关联容器的迭代器默认指向一个pair<key,value>的键值相对应的pair结构。这样就取代了STL取值用的it->second这个不美观的写法,STL的接口反倒是一种象牙塔的写法。

当然,这就意味着你不能直接不经修改就用QMap去替换std::map,反之亦然。

Qt除了我们常用的类似于STL的迭代器,也提供了JAVA风格的迭代器。你可以直接根据名字来确定他们:QContainerIterator对应QContainer::const_iterator和QMutableContainerIterator 对应QContainer::iterator。

JAVA风格的迭代器和STL风格的迭代器根部的不同是,STL迭代器是指向元素的,而JAVA迭代器则是逻辑上指向相邻的元素,这种思路的设计使迭代器不会逻辑上无效:如果一个元素的周围的元素被删除或者添加新的元素,这个迭代器依然指向的是它周围的(新)元素,迭代器依然可以正常的使用,前进或者后退。

但是同样还是有缺点的:

  • JAVA风格的迭代器的效率不如STL风格的迭代器。(因为JAVA风格的迭代器的内部是用一对STL迭代器实现的,所以这就需要一个十分完美的编译器去优化才可能赶上STL风格)。
  • JAVA风格迭代器并不是真正指向相邻的元素。同样的,当一个不是通过这个迭代器被修改的时候,JAVA风格也会同STL风格的迭代器一样会失效,例如,直接修改容器,或者通过第二个java风格的迭代器。换句话说,就是JAVA风格的迭代器也是像STL风格迭代器类似(不完全一样),不会去监视容器被其他方式的修改,也没有变更协议,去实现监视变化这一点。如果你想使用JAVA风格的迭代器去替代STL风格迭代去自动更新处理容器的状态,你会失望的。
  • 基于相邻元素的迭代器,概念上是十分美好的。但是可修改迭代器提供一些函数取药违背次原则,例如:value(), setValue()和 remove()。其结果是,当不知道next() 或者previous()是否被调用之前,那些被函数更改元素的值会被混淆。上面提到的那三个函数他们总是对“最后一个返回的元素”进行操作,但是insert()函数会忽略最后返回的元素,并把迭代器定位到插入的元素之后的元素。——容器顺序(不是迭代器顺序)。这就意味着,当我们执行返回(toBack() 和previous() )操作时插入的元素会被重复读取,但我们前进读取(toFront()和next())就不会出现这个问题。

当然,不能因此就完全否认JAVA风格迭代器是不好的。无论你使用哪种迭代器,遍历的同时修改容器是个很复杂而且危险的处理,如果需要,最好是用算法修改,例如std::remove().

 

提示:不要再遍历时修改(删除或插入元素)容器。如果十分必要,用STL算法,例如:stad::remove().

提示:对于Qt容器优先使用STL风格迭代器。

提示:如果你使用JAVA风格迭代器,避免使用可修改的迭代器。

Q_FOREACH

另一个与迭代器紧密相关的概念是 Q_FOREACH(如果你编译时定义了QT_NO_KETYWORDS宏定义,你也可以使用foreach):一个宏,可以隐藏所有迭代器的操作。下面一个Q_FOREACH循环遍历的例子:

const QStringList list = ...;
Q_FOREACH( const QString & item, list )
      qDebug() << item;


当你使用 Q_FOREACH时,你要牢记下面几点:

  • Q_FOREACH,作为一个宏,不能处理两个以上参数的模板:
const QVector<QPair<QString,int>> list = ...;
Q_FOREACH( const QPair<QString,int> & item, list ) // error: Q_FOREACH requires 2 arguments, 3 given
     qDebug() << item;

这是因为C/C++预处理程序不能识别尖括号(<>)作为标示分隔符和QPair的参数分割的逗号作为了Q_FOREACH 的参数分割。所以,你需要使用typedef来解决:

const QVector<QPair<QString,int>> list = ...;
typedef QPair<QString,int> Item;
Q_FOREACH( const Item & item, list ) // ok
    qDebug() << item;
  • 你可能注意到了,我们第一个例子用的是const 引用,即我们使用Q_FOREACH( const QString & item, list )而不是Q_FOREACH( QString item, list )。这么做有两个好处:一、通过引用,我们避免了一次值的拷贝,二、使用const 引用,避免我们犯一些愚蠢的错误,例如去控制这个循环的变量(更新这个容器中的元素)。当然,一些类型你要额可以使用一般的值传递(大多数内置类型),就是这样写:
const QVector<int> list = ...;
Q_FOREACH( const int item, list )
    qDebug() << item;
  •  现在,我们知道怎么用Q_FOREACH实现一个循环,你可能试着去写一个修改数据的循环,例如这样:
QStringList list = ...;
Q_FOREACH( QString & item, list )
    item += QLatin1String( ".txt" );

(注意上面item使用的是非const的引用)。然而,这样,它将会不工作。因为Q_FOREACH内部永远是用const_iterator实现的,如果你需要修改这个特性,去转用BOOST_FOREACH吧,它同样对于Qt的容器可用(至少那些与STL兼容的容器都可用)。

 

提示:优先使用const引用作为Q_FOREACH的第一个参数,除非这个类型一般都是按值传递的。

提示:去熟悉BOOST_FOREACH和它提供的附加的特性。

(未完待续······)

《[翻译]理解Qt容器:STL VS QTL(二)——迭代器》有13个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注