Dart中最优雅的Json类型表示

1,028 阅读4分钟

Json的使用在前后端中都极其普遍,但用过dart和flutter的人都会有一个感受:dart语言中表示json对象很麻烦。因为dart语言中json反序列化后的对象表示就是一个Map<String, dynamic>类型的对象,严格的说,这只能是一个数据结构,不是一个具备方法和行为的类对象。在平常的开发中,还需要定义一个真正的数据类,将反序列化中后Map中的各个数据一个一个的添加到对象的指定属性中,如:

class User {
  final String name;
  final String email;
  
  User(this.name, this.email);

  factory User.fromJson(Map<String, dynamic> json) {
    return User(json['name'], json['email']);
  }
}

一旦属性很多,对象关联比较复杂,写起来非常繁琐。所以一般都是在开发阶段借助依赖库,以代码生成的方式来完成这一过程,但这个操作本身没有变化。另外,有时我们的方法只需要接收json类型数据,于是方法参数类型不得不声明Map<String, dynamic>,看着非常累赘,虽然在最新的dart语言中实现了给类型声明一个别名,但依然需要写一大坨的['property']这种属性操作。

前几天在网上看到一篇文章,实现了一种令人拍案叫绝的方式来表示Json类,这种方式不仅极其简单还做到了数据类对象与反序列化对象的一体化,与以往的方式有本质的不同,实在应该让更多的人了解和使用。

首先明确,dart是一种静态类型的同时也是类型安全的语言,对象自身的类型及各个方法和属性在编译时就是可以确定的,如果调用一个不存在的属性和方法,或者类型出现不一致就会报错。比如直接调用user.username,在编译阶段就无法通过,因为编译器已经知道user的类型是User, 这个类型没有username这个属性。由于各种各样的原因dart加入了dynamic关键字,可以让编译过程忽略对象类型和不存在的方法,这是这个语言本身具有的一种特性。做如下更改,就不会再有编译报错:

  dynamic user = User('John', 'john@163.com');
  user.username;

但是编译时绕过的方在运行时终究还是不存在的,这时语言上下文就会抛弃异常,程序崩溃。但是这个过程其实另有玄机:在调用了一个对象实例不存在的方法时,首先是调用了该对象的noSuchMethod方法,然后再交由虚拟机处理的,而基类ObjectnoSuchMethod是一个dart-sdk实现方法:

@pragma("vm:entry-point")
external dynamic noSuchMethod(Invocation invocation);

在dart中,任何类型都会带一个noSuchMethod的方法,也是语言的一种特性。在调用 noSuchMethod的方法时,同时会传一个Invocation对象作为参数,这个参数包含了这次失败调用的全部信息,比如被调用的方法名及其参数,这个操作当然也包括各种getter和setter。

这个特殊Json对象类就是利用这一语言特性实现的!不需要定义任何属性,只需在类内部包裹一个通常的Map<String, dynamic>,在调用这个类的任意属性时(不管是否存在)就会走到这个类的noSuchMethod方法,然后在这个方法里再进行实际处理!如果是setter就给map设置值,如果是getter就走map取值。废话不多说,上代码:

dynamic jsonObject({Map<String, dynamic>? json}) => JsonObject._(json ?? {});

class JsonObject {
  final Map<String, dynamic> map;

  const JsonObject._(this.map);

  @override
  dynamic noSuchMethod(Invocation i) {
    if (i.isGetter) {
      return map[i.memberName.name];
    } else if (i.isSetter) {
      map[i.memberName.name] = i.positionalArguments.first;
    } else {
      return super.noSuchMethod(i);
    }
  }

  @override
  String toString() => map.toString();
}

extension _SymbolExt on Symbol {
  String get name {
    final s = toString();
    final symbol = s.substring(8, s.length - 2);
    return symbol.endsWith('=') ? symbol.substring(0, symbol.length - 1) : symbol;
  }
}

还有其它一些一看就懂的小技巧。使用示例如下:

void main() {
  final o = jsonObject();
  o.property = "nice";
  print(o.property);
}

这样一个Json表示类,可谓十分的方便!根本不需要预先声明任何属性和类型,简直和javascript中内置的JsonObject一模一样!而且和现在的序列化和反序列化无缝集成,甚至直接可以将这个操作集成进一个具体的数据类型,比如我们的例子User

当然便利带来了其它问题:首当其冲的就是类型安全,因为整个类型都声明成了dynamic,所有的风险都留给了运行时,其次,一旦一个属性值写错,那问题定位起来可能非常痛苦,因为数据的传送链条可能非常长,本来想取property结果赋值的时候写成了propety,那后果不堪设想。