流 是用于迭代元素的序列,如序列集合、文件和网络流。流可以是可读的、可写的,或两者兼而有之。读或写总是相对于流中的当前位置。流可以很容易地转换为集合,反之亦然。
15.1 两个元素序列
可以将流比喻为两个元素序列:过去的元素序列和未来的元素序列。流位于两个序列之间。理解这个模型很重要,因为Pharo中所有的流操作都依赖于它。由于这个原因,大多数流都是PositionableStream
的子类。图15-1以一个5个字符的流为例。这个流位于它的初始位置,即,“过去的元素”为空。你可以使用PositionableStream
中定义的reset
消息返回到该位置。
从概念上讲,读取元素意味着删除“未来元素序列”的第一个元素,并将其放在“过去的元素序列”的最后一个元素之后。在使用next
消息读取一个元素之后,流的状态如图15-2所示。
写入一个元素意味着用新元素替换“未来的序列”的第一个元素,并将其移到过去的位置。图15-3显示了同一个流在使用定义在Stream
中的nextPut: anElement
消息写入后的状态。
15.2 Streams vs. 集合s
集合协议支持集合元素的存储、删除和枚举,但不允许这些操作混合使用。例如,如果 Ordered集合
的元素由 do:
方法处理,则无法从 do:
块内部添加或删除元素。集合协议也不提供同时迭代两个集合的方法,选择哪个集合前进,哪个不前进。像这样的过程需要在集合本身之外维护遍历索引或位置引用:这正是 ReadStream
、WriteStream
和 ReadWriteStream
的作用。
这三个类被定义为某个集合之上的流,例如,下面的代码片段在一个Interval
上创建流,然后读取两个元素。
| r |
r := ReadStream on: (1 to: 1000).
r next.
>>> 1
r next.
>>> 2
r atEnd.
>>> false
WriteStream
可以将数据写入集合
| w |
w := WriteStream on: (String new: 5).
w nextPut: $a.
w nextPut: $b.
w contents.
>>> 'ab'
还可以创建同时支持读写协议的ReadWriteStream
.
下面几节将更深入地介绍这些协议。
流在处理集合元素时非常有用,可以用来读写这些元素.现在我们将探讨流的集合特性。
15.3 读取集合
使用流读取集合实际上为你提供了一个指向集合的指针。指针将在读取时向前移动,你可以将它放在你想要的任何地方。ReadStream
类应该用于从集合中读取元素。
定义在ReadStream
中的消息next
和next:
用于从集合中检索一个或多个元素。
| stream |
stream := ReadStream on: #(1 (a b c) false).
stream next.
>>> 1
stream next.
>>> #(#a #b #c)
stream next.
>>> false
| stream |
stream := ReadStream on: 'abcdef'.
stream next: 0.
>>> ''
stream next: 1.
>>> 'a'
stream next: 3.
>>> 'bcd'
stream next: 2.
>>> 'ef'
15.4 Peek
当你需要知道一个流里面的下一个元素是什么,而不继续前进的时候,使用PositionableStream
中定义的消息peek
。
| stream negative number |
stream := ReadStream on: '-143'.
"look at the first element without consuming it."
negative := (stream peek = $-).
negative.
>>> true
"ignores the minus character"
negative ifTrue: [ stream next ].
number := stream upToEnd.
number.
>>> '143'
这段代码根据流中数字的符号将布尔变量设置为负值,并将number
设置为其绝对值。ReadStream
中定义的消息upToEnd
返回从当前位置到流的结束之间的所有内容,并将流指针移动到末尾。这段代码可以使用PositionableStream
中定义的消息peekFor:
进行简化,如果下一个元素等于它的参数则向前移动,否则就不移动。
| stream |
stream := '-143' readStream.
(stream peekFor: $-).
>>> true
stream upToEnd
>>> '143'
peekFor:
返回一个布尔值,指示下一个元素是否等于其参数。
你可能已经注意到,在上面的示例中构造流的一种新方法:可以简单地将消息readStream
发送到序列化集合(例如字符串)。可以从特定的序列上获得一个流。
15.5 定位到索引
有专门用于定位流指针的消息。如果你知道具体的索引,你可以直接使用PositionableStream
当中定义的position:
。可以使用position
请求当前位置。请记住,流不是位于元素上,而是位于两个元素之间。流的开头的索引为0.
你可以通过下面的代码获得15-4中描述的流的状态:
| stream |
stream := 'abcde' readStream.
stream position: 2.
stream peek
>>> $c
要将流指针定位到开头或结尾,可以使用消息reset
或setToEnd
。
15.6 跳过元素
消息skip:
和skipTo:
用于向前移动到相对于当前位置的位置,skip:
接受一个数字作为参数,并跳过指定数量的元素,而skipTo:
则一直跳到与其参数相等的元素。注意,它将流指针定位在匹配的元素之后。
| stream |
stream := 'abcdef' readStream.
stream next
>>> $a
现在,流指针在$a
后面。
stream skip: 3.
stream position
>>> 4
现在,流指针在$d
后面。
stream skip: -2.
stream position
>>> 2
现在,流指针在$b
后面。
stream reset.
stream position
>>> 0
stream skipTo: $e.
stream next.
>>> $f
stream contents.
>>> 'abcdef'
消息contents
总是返回整个流的副本。
15.7 谓词
有些消息允许你测试当前流的状态:atEnd
当且仅当不能读取更多元素时返回true
,而isEmpty
当且仅当集合中根本没有元素时返回true
。
下面是一个使用atEnd
的归并算法实现,它接受两个有序集合作为参数,并将它们合并到成一个有序集合:
| stream1 stream2 result |
stream1 := #(1 4 9 11 12 13) readStream.
stream2 := #(1 2 3 4 5 10 13 14 15) readStream.
"The variable result will contain the sorted 集合."
result := OrderedCollection new.
[ stream1 atEnd not & stream2 atEnd not ]
whileTrue: [
stream1 peek < stream2 peek
"Remove the smallest element from either stream and add it to the result."
ifTrue: [ result add: stream1 next ]
ifFalse: [ result add: stream2 next ]].
"One of the two streams might not be at its end. Copy whatever remains."
result
addAll: stream1 upToEnd;
addAll: stream2 upToEnd.
result.
>>> an OrderedCollection(1 1 2 3 4 4 5 9 10 11 12 13 13 14 15)
15.8 写入集合
我们已经看到了如何使用readStream
来读取集合元素。现在,我们将学习如何使用WriteStream
创建集合。
WriteStream
对于在不同位置向集合追加大量数据非常有用。它们通常被用来构造基于静态和动态部分的字符串,如下所示:
| stream |
stream := String new writeStream.
steam
nextPutAll: 'This image contains: ';
print: Smalltalk globals allClasses size;
nextPutAll: ' classes.';
cr;
nextPutAll: 'This is really a lot.'.
stream contents.
>>> 'This image contains: 9003 classes.
This is really a lot.'
例如,在方法printOn:
的不同实现中使用了这个技术。如果你只对流的内容感兴趣,有一种更简单、更有效的方法创建字符串:
| string |
string := String streamContents:
[ :stream |
stream
print: #(1 2 3);
space;
nextPutAll: 'size';
space;
nextPut: $=;
space;
print: 3. ].
string.
>>> '#(1 2 3) size = 3'
在SequenceableCollection
上定义的消息streamContents
为你创建了一个集合,以及集合上的流。然后,它以流为参数执行你传递给它的block。当流终止,streamContents
将它的内容作为一个集合返回。
下面的WriteStream
方法在这里很有用:
nextPut:
将其参数添加到流中;nextPutAll:
将作为参数的集合中的每一个元素都添加到流中;print:
将文本表示的参数添加到流中。
还有一些方便的消息用于将有用的字符打印到流中,例如space
,tab
以及cr
(回车)。另一个有用的方法是ensureASpace
确保流中的最后一个字符是空格字符;如果最后一个字符不是空格,则添加一个空格。
15.9 关于字符串连接
在WriteStream
上使用nextPut:
和nextPutAll:
通常是连接字符的最佳方式。下面的两个示例显示了在执行相同任务时,使用逗号操作符连接字符串的效率远不如使用流的效率。
[ | temp |
temp := String new.
(1 to: 100000)
do: [ :i | temp := temp, i asString, ' ']] timeToRun
>>> 0:00:01:54.758
[ | temp |
temp := WriteStream on String new.
(1 to: 100000)
do: [ :i | temp nextPutAll: i asString; space ].
temp contents ] timeToRun
>>> 0:00:00:00.024
使用流可能比使用逗号更有效,因为最后一个创建的新字符串包含接收者和参数的连接,因此必须复制它们。当你重复连接到同一个接收者上时,它会变得越来越长,因此必须复制的字符数量会呈指数级增长。这样的字符串连接也会产生大量的垃圾,必须收集这些垃圾。使用流而非,
是一种众所周知的优化。
事实上,你可以使用定义在SequenceableCollection
类中的消息streamContents:
来帮助你做到这一点:
String streamContents: [ :tempStream |
(1 to: 100000)
do: [ :i | tempStream nextPutAll: i asString; space ]]
15.10 关于printString
让我们花点时间回顾一下printOn:
方法中的流用法。基本上,Object>>#printString
方法创建了一个流,并将该流作为printOn:
方法的参数,如下所示:
Object >> printString
"Answer a String whose characters are a description of the receiver.
If you want to print without a character limit, use fullPrintString."
^ self printStringLimitedTo: 50000
Object >> printStringLimitedTo: limit
"Answer a String whose characters are a description of the receiver.
If you want to print without a character limit, use fullPrintString."
^ self printStringLimitedTo: limit using: [:s | self printOn: s]
Object >> printStringLimitedTo: limit using: printBlock
"Answer a String whose characters are a description of the receiver
produced by given printBlock. It ensures the result will be not
bigger than given limit"
| limitedString |
limitedString := String streamContents: printBlock limitedTo: limit.
limitedString size < limit ifTrue: [^ limitedString].
^ limitedString , '...etc...'
你应该看到printStringLimitedTo:using:
方法创建了一个流并将其传递。
当你在类中重新定义方法printOn:
时,如果你在对象的实例变量上发送消息printString
,实际上你是在创建另一个流并在第一个流中复制其内容。下面是一个例子:
MessageTally >> displayStringOn: aStream
self displayIdentifierOn: aStream.
aStream
nextPutAll: ' (';
nextPutAll: self tally printString;
nextPutAll: ')'
这里的表达式self tally printString
调用了相同的机制,并创建了一个额外的流,而不是使用前一个流。这显然是适得其反。最好是将消息print:
发送给流或者printOn:
发送给实例变量,如下所示:
MessageTally >> displayStringOn: aStream
self displayIdentifierOn: aStream
aStream
nextPutAll: ' (';
print: self tally;
nextPutAll: ')'
在这个版本中,使用第一个创建的流,没有创建额外的流。
要理解print:
方法,这里是它的定义:
Stream >> print: anObject
"Have anObject print itself on the receiver."
anObject printOn: self
另外的例子
额外的流创建并不局限于printString
逻辑。这里有一个来自Pharo的例子,它表现出了完全相同的问题。
printProtocol: protocol sourceCode: sourceCode
^ String streamContents: [ :stream |
stream
nextPutAll: '"protocol: ';
nextPutAll: protocol printString;
nextPut: $"; cr; cr;
nextPutAll: sourceCode ]
你应该看到它创建了一个流,然后又创建了另外一个流,在使用表达式protocol printString
调用之后又丢弃了它。更好的实现是:
printProtocol: protocol sourceCode: sourceCode
^ String streamContents: [ :stream |
stream
nextPutAll: '"protocol: ';
print: protocol;
nextPut: $"; cr; cr;
nextPutAll: sourceCode ]
15.11 同时进行读写
可以使用流访问一个集合并同时进行读写。假设你想创建一个History
类来管理Web浏览器中的后退和前进按钮。历史记录的响应如图15-5 - 15-11 所示。
这种行为可以使用ReadWriteStream
流来实现。
Object subclass: #History
instanceVariableNames: 'stream'
classVariableNames: ''
package: 'PBE-Streams'
History >> initialize
super initialize.
stream := ReadWriteStream on: Array new
这里没有什么真正的困难,我们定义了一个包含流的新类。在执行initialize
方法时创建流。
我们需要后退和前进的方法:
History >> goBackward
self canGoBackward
ifFalse: [ self error: 'Already on the first element' ].
stream skip: -2.
^ stream next.
History >> goForward
self canGoForward
ifFalse: [ self error: 'Already on the last element' ].
^ stream next
到目前为止,代码非常简单。接下来,我们必须处理goTo:
方法,该方法应该在用户单击链接时被激活。一种可能的实现是:
History >> goTo: aPage
stream nextPut: aPage
然而,这个版本是不完整的。这是因为当用户点击链接时,不应该有更多的可以前进的页面,也就是说,前进按钮必须停用。要做到这一点,最简单的解决方案是在后面写入nil
,表示历史记录已经处于末尾:
History >> goTo: anObject
stream nextPut: anObject.
stream nextPut: nil.
stream back
现在,只有canGoBackward
和canForward
方法还没有实现。
流指针初始位于两个元素之间。要向后移动,在当前位置之前必须有两个页面:一个页面是当前页,另一个是我们想要转到的页面。
History >> canGoBackward
^ stream position > 1
History >> canGoForward
^ stream atEnd not and: [ stream peek notNil ]
让我们添加一个方法来查看流的内容:
History >> contents
^ stream contents
接下来,History
就可以如同宣称的那样工作:
History new
goTo: #page1;
goTo: #page2;
goTo: #page3;
goBackward;
goBackward;
goTo: #page4;
contents
>>> #(#page1 #page4 nil nil)
15.12 本章总结
与集合相比,流提供了一种更好的方式来增量地读写一系列元素。有一些简单的方法可以在流和集合之间来回转换。
流可以是可读的、可写的、可以同时读写的。
要将一个集合转换为流,需要在集合上定义一个流,例如:
ReadStream on: (1 to: 1000)
,或者将消息readStream
发送给集合对象。要将流转换为集合, 请向它发送消息
contents
要连接大型的集合, 与其用逗号操作符,不如更有效地创建流,使用
nextPutAll:
将集合附加到流,并通过contents
提取结果。