在写C++代码时,首先接触的就是迭代器。甚至于设计模式都有一种模式叫迭代器模式。虽说网上到处都说迭代器用于隐藏数据结构的细节,但我却一直没有真正搞明白为什么需要迭代器去隐藏数据结构细节。
在写C++代码时,一般我每用一个数据结构都会去查一下,他大致是如何实现的(不然用起来不太放心:D)。
因此一般情况下我在c++下都是使用类似类似for(size_t i = 0; i < vector.size(); i++)的方式去遍历vector的每个元素。
直到最近的一次重构我才大概明白什么时候去使用迭代器模式。
首先大致说一下zproto的作用。
zproto是一个序列化/反序列化的工具,类似google protobuf,但是实现更简单,更轻量级。
在实现之初,我是希望zproto.c可以实现syntax和serialize/unserialize的核心功能,然后再为每种不同的语言写一组操作本语言数据结构的函数,即可通过zproto.c来实现bind功能。但我又不想做成callback接口。
由于zproto所有字段都是可选的,因此某一字段可能是不存在的,在encode和decode时,zproto.c必须知道上一次有效的字段是哪一个。
因此接口就变成了这个样子:
void zproto_encode_tag(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_field *field, int32_t count);
void zproto_encode(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_field *field, const char *data, int32_t sz);
struct zproto_field *zproto_decode_tag(struct zproto_buffer *zb, struct zproto_field *last,
struct zproto_record *proto, int32_t *sz);
从接口就可以看出,在写bind代码时,每次都需要维护上一个有效有字段,这其实从一定程度上暴漏了zproto.c作为core的实现细节而且加重了写bind代码的负担,这一度让我觉得很恶心。
几次想把last放入zproto_buffer字段中,但这样就需要在zproto_buffer中维护一个栈的结构,因为record(结构体)中有field(字段),field又可能是record类型。 这样一来就增加了实现复杂度,与我的初衷不符。因此也就迟迟没有动手。
直到最近在一次写C++的代码时,再一次用到迭代器(遍历map中的所值)时,突然觉得这个问题可以用迭代器来解决。
使用迭代器实现后的接口如下:
void zproto_encode_array(struct zproto_buffer *zb, struct zproto_field_iter *iter,
int32_t count);
void zproto_encode(struct zproto_buffer *zb, struct zproto_field_iter *iter,
const char *data, int32_t sz);
int zproto_decode_field(struct zproto_buffer *zb, struct zproto_record *proto,
struct zproto_field_iter *iter, int32_t *sz);
int zproto_decode(struct zproto_buffer *zb, struct zproto_field_iter *iter,
uint8_t **data, int32_t *sz);
明显可以看出,接口和内聚性都提高了很多。在写bind代码时再也不需要维护上一次有效的字段了,因为在encode时已经被记入iter了。
当然代价肯定了也是有的,就是多实现了一组迭代器的函数,不过我认为这代代价是值得的。
由于zproto已经完全屏蔽了细节,因此在写bind代码时可以只关心本语言的数据结构的存取,而不会由于不懂zproto的实现细节导致传入错误的last_field造成zproto.c工作异常,大大降低了心理的包袱。
可以结总出迭代器一般用于提供给其他模块遍历时才使用(其实从标准库就可以看出,只是我自己没领会到^_^!),迭代器数据结构也不一定只存储用于遍历的数据,只要方便达到目的也可以存一些冗余数据,比如zproto.c中的迭代器会存储上一次有效的字段。
现在再反思为什么以前写代码都不会用到迭代器模式其实都很清楚了。
在写C语言时,模块一般都是提供某种属性或某种行为的而不会提供遍历的特性,这种情况下也就没有必要去为此模块提供一个迭代器。
而在模块内部的数据结构, 其实也没有必要去实现一个迭代器,有时候直接操作成员去访问可以更直观更高效。
同样在C++开发时也并非说,实现了一个容器类数据结构就需要去为他实现一个迭代法器,如果此数据结构仅仅为优化特定模块而生。那么直接去访问有时后反而代码更少更简洁,也并不会造成什么坏的影响。
还是那句话,没有银弹,什么时候合适什么时候不合适还是要靠自己判断。