dart如何有效的进行数据转换 - Fri, Mar 1, 2019
在不同的数据格式之间进行转换,是计算机工程的常规作业。Dart语言也没有例外,使用dart:convert
核心库,能够提供一系列转换器和有用的工具生成新的转换器。库已经提供了一些常用到的转换例子,例如:JSON
和UTF-8
。在这篇文章中,我们展示了Dart的转换方法是怎样工作的,还有在Dart的世界中,你可以怎样创建自己高效的转换器。
宏图
Dart的转换架构师基于converters
(转换器),能够从一种格式转换为另一种格式。当转换是可逆时,两种转换器能够被合并到一起形成codec
(编-解码器)。编解码器适应于音视频的处理,但是也适应于字符串编码,例如:UTF8
和JSON
.
按照约定,Dart中使用到的所有转换器都需要使用dart:convert
中提供的抽象方法。这能为开发者提供一致的API,并能够确保转换器之间能够协同运行。例如,如果转换器(或编码器)的类型一致的话,能够将他们合并到一起,合并后的转换器能够形成单独的单元。此外,这些合并后的转换器使用起来比单独的转换器更有效。
编解码器
一个编解码器结合了两个转换器,一个编码器,一个解码器。
abstract class Codec<S, T> {
const Codec();
T encode(S input) => encoder.convert(input);
S decode(T encoded) => decoder.convert(encoded);
Converter<S, T> get encoder;
Converter<T, S> get decoder;
Codec<S, dynamic> fuse(Codec<T, dynamic> other) { .. }
Codec<T, S> get inverted => ...;
}
如上所示,编解码器提供了方便的方法,例如用编码器和解码器表达的encode()
和decode()
。fuse()
方法和inverted
取得方法分别允许你去合并转换器和改变编码的方向。编解码器的基本实现,为这两个成员提供了固定默认的实现和成员,通常不需要担心他们。
encode()
和decode()
方法也可以不用动,但是他们可以添加新的参数。例如,JsonCodec
向encode()
和decode()
追加了命名参数来让他们的方法更有用:
dynamic decode(String source, {reviver(var key, var value)}) { … }
String encode(Object value, {toEncodable(var object)}) { … }
这个编解码器可以使用默认参数进行实例化,除非在调用encode()
/decode()
期间,被命名参数覆盖。
const JsonCodec({reviver(var key, var value), toEncodable(var object)})
...
常规:如果编解码器能够被确认,他应该向encode()
/decode()
方法追加命名参数,并允许他们在构造时设为默认值。如果可能,编解码器应该是const
类型的构造函数。
Converter
转换器,尤其是他们的convert()
方法,是真正转换发生的地方。
T convert(S input); // where T is the target and S the source type.
最小转换器只需要继承Convert
类,实现convert()
方法。与编解码器类似,转换器能够通过继承构造函数和追加命名参数到convert()
方法进行配置。
这种最小转换器运行在同步设置中,不能在块(同步或异步)环境中运行。尤其是,这种简单的转换器不能用作变形器(一种更好的转换器特性)。一个完全实现的转换器实现了SteamTransformer
接口,从而能够有Steam.transform()
方法。
可能最常用的用例是使用UTF8.decoder
进行UTF-8解码
File.openRead().transform(UTF8.decoder).
分块转换
分块转换的概念令人困惑,但是从它的核心来看,它也是相对简单的。当分块转换(包括流转换)开始时,转换器的startChunkedConversion
方法将会使用输出接收器作为参数进行调用。然后这个方法将会返回一个让调用者放数据的输入接收器。
提示:图中带星号的表示多次调用。
图中,第一步创建一个填充要转换数据的outputSink
。然后用户调用带有输出接收器的转换器的startChunkedConversion()
方法。结果是带有add()
和close()
方法的输入接受器。
下一个点,代码将会开始分块转换调用,可能发生多次,add()
方法会携带数据。数据会被输入接收器转换。如果转换的数据已经准备好了,输入接收器会把它发送到输出接收器,可能add()
方法会被调用多次。最终用户会通过调用close()
结束转换。在这个点上,任何保存的转换后的数据会从输入接收器发送到输出接收器,并且输出接收器会被关闭。
基于转换器的输入接收器可能需要缓存输入数据的部分。例如,行分割器接受ab\ncd
作为块,能够安全的调用含有ab
的输出接收器,但是需要等待下一个数据(或者)。如果下一个数据是e\nf
,输入接收器必须串联cd
和e
并且调用带有cde
的输出接收器,同时缓存f
作为下一个数据的事件(或者调用close
)。
有趣的是,分块转换的类型不能从它同步转换中识别出来。例如,HtmlEscape
转换器同步转换字符串到字符串,和同步转换字符块到字符块(字符串到字符串)。行分割器同步转换字符串到列表(分割后的行).尽管同步的签名不同,行分割器的块版本与HtmlEscape有相同的签名:String→String。在这种情况下,每个分割出来的块都是一行。
import 'dart:convert';
import 'dart:async';
main() async {
// HtmlEscape synchronously converts Strings to Strings.
print(const HtmlEscape().convert("foo")); // "foo".
// When used in a chunked way it converts from Strings
// to Strings.
var stream = new Stream.fromIterable(["f", "o", "o"]);
print(await (stream.transform(const HtmlEscape())
.toList())); // ["f", "o", "o"].
// LineSplitter synchronously converts Strings to Lists of String.
print(const LineSplitter().convert("foo\nbar")); // ["foo", "bar"]
// However, asynchronously it converts from Strings to Strings (and
// not Lists of Strings).
var stream2 = new Stream.fromIterable(["fo", "o\nb", "ar"]);
print("${await (stream2.transform(const LineSplitter())
.toList())}");
}
通常来说,当按照StreamTransformer进行使用时,分块转换的类型由最有用的用例决定。
分块转换接收器
ChunkedConversionSink
是用来向转换器追加数据或者作为转换器的输出。最基本的分块转换接收器有两个方法:add()
和close()
。在所有的系统接收器里例如StringSinks
或StreamSinks
都有相同的功能。
分块转换接收器的语义类似于IOSinks
:数据添加到接收器之后必须不能编辑,除非可以保证数据已被处理。对于字符串是没有问题的(因为他们是不可改变的),但是对于字节列表,它经常意味着申请一块列表的备份。这可能是低效的,dart:convert
库附带了分块转换器的子类支持更有效的数据传输。
例如,ByteConversionSink
有额外的方法
addSlice(List<int> chunk, int start, int end, bool isLast)
从语义上来讲,它接受一个列表(可能不会保存),转换器能够操作的子范围,和一个可以代替close()
的bool型的isLast
。
import 'dart:convert';
main() {
var outSink = new ChunkedConversionSink.withCallback((chunks) {
print(chunks.single); // 𝅘𝅥𝅯
});
var inSink = UTF8.decoder.startChunkedConversion(outSink);
var list = [0xF0, 0x9D];
inSink.addSlice(list, 0, 2, false);
// Since we used `addSlice` we are allowed to reuse the list.
list[0] = 0x85;
list[1] = 0xA1;
inSink.addSlice(list, 0, 2, true);
}
作为分块转换接收器的使用者(它既可以输入和输出转换器),它提供了更多的选择。事实上,列表不会被保存,意味着你可以使用缓存并每次调用时重用该缓存。拼接add()
和close()
可以帮助接收器避免缓存数据。接收字列表避免对SubList()
的调用(复制数据)。
该接口的缺点是实现起来复杂。为了减轻开发人员的痛苦,每个改进的dart:convert
分块转换接收器都有一个基类,它实现了除了一个方法(抽象方法)之外的所有方法。然后,转换接收器的实现者能够决定是否利用其它方法。
注意:分块转换接收器 必须 扩展相应的基类。这确保了向现有的接收器接口添加功能而不会破坏扩展接收器。
例子
本节介绍创建简单加密转换器的所有步骤,以及怎样提高自定义分块转换器的效率。
让我们从简单的同步转换器开始,其加密历程只是简单的按照给定的值旋转字节:
import 'dart:convert';
/// A simple extension of Rot13 to bytes and a key.
class RotConverter extends Converter<List<int>, List<int>> {
final _key;
const RotConverter(this._key);
List<int> convert(List<int> data, { int key }) {
if (key == null) key = this._key;
var result = new List<int>(data.length);
for (int i = 0; i < data.length; i++) {
result[i] = (data[i] + key) % 256;
}
return result;
}
}
相应的编解码类也是很简单:
class Rot extends Codec<List<int>, List<int>> {
final _key;
const Rot(this._key);
List<int> encode(List<int> data, { int key }) {
if (key == null) key = this._key;
return new RotConverter(key).convert(data);
}
List<int> decode(List<int> data, { int key }) {
if (key == null) key = this._key;
return new RotConverter(-key).convert(data);
}
RotConverter get encoder => new RotConverter(_key);
RotConverter get decoder => new RotConverter(-_key);
}
我们能够(也应该)避免新的
内存申请,但是为了简单起见,我们每次需要时我们都会申请新的RotConverter
句柄。
这里是我们怎样使用Rot编解码器:
const Rot ROT128 = const Rot(128);
const Rot ROT1 = const Rot(1);
main() {
print(const RotConverter(128).convert([0, 128, 255, 1])); // [128, 0, 127, 129]
print(const RotConverter(128).convert([128, 0, 127, 129])); // [0, 128, 255, 1]
print(const RotConverter(-128).convert([128, 0, 127, 129]));// [0, 128, 255, 1]
print(ROT1.decode(ROT1.encode([0, 128, 255, 1]))); // [0, 128, 255, 1]
print(ROT128.decode(ROT128.encode([0, 128, 255, 1]))); // [0, 128, 255, 1]
}
我们做的挺对的。编解码器运行正常,但是它还缺少分块编码部分。因为每一字节的编码都是分离的,我们回到同步不转换方法:
class RotConverter {
...
RotSink startChunkedConversion(sink) {
return new RotSink(_key, sink);
}
}
class RotSink extends ChunkedConversionSink<List<int>> {
final _converter;
final ChunkedConversionSink<List<int>> _outSink;
RotSink(key, this._outSink) : _converter = new RotConverter(key);
void add(List<int> data) {
_outSink.add(_converter.convert(data));
}
void close() {
_outSink.close();
}
}
现在我们可以使用转换器进行分块转换或者流转换:
// Requires to import dart:io.
main(args) {
String inFile = args[0];
String outFile = args[1];
int key = int.parse(args[2]);
new File(inFile)
.openRead()
.transform(new RotConverter(key))
.pipe(new File(outFile).openWrite());
}
特殊的分块转换接收器
出于很多原因,当前版本的Rot就足够了。也就是说,复杂代码和测试要求的成本将超过改进的收益。但是,我们假设转换器的性能至关重要(它在繁忙路径和配置文件中)。我们进一步假设为每一块列表快分配内存将会使性能崩溃(合理的假设)。
首先,我们是内存消耗更少:使用typed byte-list
,我们能够减少分批给列表的内存8倍大小(在64位机器上)。这样做虽然不能去掉内存分配,但是会让它分配的更少。
我们可以避免分配内存,如果我们能够重写输入。在下一个版本的RotSink,我们加了一个addModifiable()
方法,如下所示:
class RotSink extends ChunkedConversionSink<List<int>> {
final _key;
final ChunkedConversionSink<List<int>> _outSink;
RotSink(this._key, this._outSink);
void add(List<int> data) {
addModifiable(new Uint8List.fromList(data));
}
void addModifiable(List<int> data) {
for (int i = 0; i < data.length; i++) {
data[i] = (data[i] + _key) % 256;
}
_outSink.add(data);
}
void close() {
_outSink.close();
}
}
为了简单起见,我们追加了一个消耗完整列表的新方法。一个更高级的方法(例如,addModifiableSliece()
)会携带范围参数(from
, to
)和一个boolean的isLast
最为参数。
这是一个新的方法还没有被变换器使用,但是我们已经能够显示的通过调用startChunkedConversion
进行使用。
main() {
var outSink = new ChunkedConversionSink.withCallback((chunks) {
print(chunks); // [[31, 32, 33], [24, 25, 26]]
});
var inSink = new RotConverter(30).startChunkedConversion(outSink);
inSink.addModifiable([1, 2, 3]);
inSink.addModifiable([250, 251, 252]);
inSink.close();
}
在这个小例子中,性能没有明显不同,但是在内部,分块转换避免了为各个块分配新列表。对于两个小块,他没有什么区别,但如果我们为流转换器实现了这点,加密大的文件就能更快。
为此,我们可以利用IOStream提供的可修改列表的未记录功能。现在,我们可以简单的重写add()
并把它指向addModifiable()
.通常,这是不安全的,并且这样的转换器将会成为难以追踪错误的来源。相反,我们写一个转换器,明确的进行不可修改到可修改的转换,然后融合两个转换器。
class ToModifiableConverter extends Converter<List<int>, List<int>> {
List<int> convert(List<int> data) => data;
ToModifiableSink startChunkedConversion(RotSink sink) {
return new ToModifiableSink(sink);
}
}
class ToModifiableSink
extends ChunkedConversionSink<List<int>, List<int>> {
final RotSink sink;
ToModifiableSink(this.sink);
void add(List<int> data) { sink.addModifiable(data); }
void close() { sink.close(); }
}
ToModifiableSink
只是向下一个接收器发送信号,表明传入的块可以修改。我们能够使用它来提高我们的管道效率。
main(args) {
String inFile = args[0];
String outFile = args[1];
int key = int.parse(args[2]);
new File(inFile)
.openRead()
.transform(
new ToModifiableConverter().fuse(new RotConverter(key)))
.pipe(new File(outFile).openWrite());
}
在我的机器上,这个小修改将11MB文件的加密时间从450ms降低到260ms。我们实现了这种加速,没有丢失现有编码器的兼容性(关于fuse()
方法),并且转换器始终是流转换器。
重用输入可以很好地与其他转换器配合使用,而不仅仅是适用我们的Rot密码。因此,我们应该创建一个概括概念的接口。简单起见,我们将它命名为CipherSink ,当然它可以在加密世界之外使用。
abstract class CipherSink
extends ChunkedConversionSink<List<int>, List<int>> {
void addModifiable(List<int> data) { add(data); }
}
我们可以是我们的RotSink私有,并把CipherSink暴露出去。其它的开发者可以重用我们的工作(CipherSink和ToModifiableConverter)并从中受益。
但是我们还没完事呢。
尽管我们不能是加密更快了,我们可以提高Rot转换器的输出端。例如,融合两种加密方式:
main(args) {
String inFile = args[0];
String outFile = args[1];
int key = int.parse(args[2]);
// Double-strength cipher running the Rot-cipher twice.
var transformer = new ToModifiableConverter()
.fuse(new RotConverter(key)) // <= fused RotConverters.
.fuse(new RotConverter(key));
new File(inFile)
.openRead()
.transform(transformer)
.pipe(new File(outFile).openWrite());
}
由于第一个RotConverter调用了outSink.add()
,假如第二个RotConverter输入不能被编辑和复制数据。我们可以在两个加密之间插入ToModifiableConverter
来解决这个问题:
var transformer = new ToModifiableConverter()
.fuse(new RotConverter(key))
.fuse(new ToModifiableConverter())
.fuse(new RotConverter(key));
这有用,但是太傻。我们希望RotConverter在没有中间转换器的情况下进行运行。第一个密码应该查看他的输出接收器并确定他是否是CipherSink。无论何时我们想添加新块或者在我们开始分块转换时,我们都可以这么做。我们更喜欢后一种方法:
/// Works more efficiently if given a CipherSink as argument.
CipherSink startChunkedConversion(
ChunkedConversionSink<List<int>> sink) {
if (sink is! CipherSink) sink = new _CipherSinkAdapter(sink);
return new _RotSink(_key, sink);
}
_CipherSinkAdapter很简单:
class _CipherSinkAdapter implements CipherSink {
ChunkedConversionSink<List<int>, List<int>> sink;
_CipherSinkAdapter(this.sink);
void add(data) { sink.add(data); }
void addModifiable(data) { sink.add(data); }
void close() { sink.close(); }
}
我们现在只需要更改_RotSink以利用它始终接收CipherSink作为其构造函数的参数这一事实:
class _RotSink extends CipherSink {
final _key;
final CipherSink _outSink; // <= always a CipherSink.
_RotSink(this._key, this._outSink);
void add(List<int> data) {
addModifiable(data.toList());
}
void addModifiable(List<int> data) {
for (int i = 0; i < data.length; i++) {
data[i] = (data[i] + _key) % 256;
}
_outSink.addModifiable(data); // <= safe to call addModifiable.
}
void close() {
_outSink.close();
}
}
通过这些更改,我们的超级安全双密码将不会分配任何新列表,我们的工作也已完成。
果然够难,够复杂。