dart如何有效的进行数据转换

  1. 宏图
  2. 编解码器
  3. Converter
  4. 分块转换
  5. 分块转换接收器
  6. 例子
  7. 特殊的分块转换接收器

在不同的数据格式之间进行转换,是计算机工程的常规作业。Dart语言也没有例外,使用dart:convert核心库,能够提供一系列转换器和有用的工具生成新的转换器。库已经提供了一些常用到的转换例子,例如:JSONUTF-8。在这篇文章中,我们展示了Dart的转换方法是怎样工作的,还有在Dart的世界中,你可以怎样创建自己高效的转换器。

宏图

Dart的转换架构师基于converters(转换器),能够从一种格式转换为另一种格式。当转换是可逆时,两种转换器能够被合并到一起形成codec(编-解码器)。编解码器适应于音视频的处理,但是也适应于字符串编码,例如:UTF8JSON.

按照约定,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()方法也可以不用动,但是他们可以添加新的参数。例如,JsonCodecencode()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方法将会使用输出接收器作为参数进行调用。然后这个方法将会返回一个让调用者放数据的输入接收器。

chunked-conversion.png

__提示__:图中带星号的表示多次调用。

图中,第一步创建一个填充要转换数据的outputSink。然后用户调用带有输出接收器的转换器的startChunkedConversion()方法。结果是带有add()close()方法的输入接受器。

下一个点,代码将会开始分块转换调用,可能发生多次,add()方法会携带数据。数据会被输入接收器转换。如果转换的数据已经准备好了,输入接收器会把它发送到输出接收器,可能add()方法会被调用多次。最终用户会通过调用close()结束转换。在这个点上,任何保存的转换后的数据会从输入接收器发送到输出接收器,并且输出接收器会被关闭。

基于转换器的输入接收器可能需要缓存输入数据的部分。例如,行分割器接受ab\ncd作为块,能够安全的调用含有ab的输出接收器,但是需要等待下一个数据(或者)。如果下一个数据是e\nf,输入接收器必须串联cde并且调用带有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()。在所有的系统接收器里例如StringSinksStreamSinks都有相同的功能。

分块转换接收器的语义类似于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();
  }
}

通过这些更改,我们的超级安全双密码将不会分配任何新列表,我们的工作也已完成。

果然够难,够复杂。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 wind.kaisa@gmail.com

💰

×

Help us with donation