前言

学了点 dart 且写了一段时间的 flutter 后,发现遇到大多数的错误都和null有关系,

一个不小心if语句就报null错误了,为了避免这个错误,不得不写成下面这种形式

if( true == (boolVar ?? false)){
    // balabala....
}

为何会这样呢?因为if(null == true)它会报错的。而你一个不留神就可能导致写出这样的代码,你以为它不会变成null当它某一个时刻变成了null而真实的报错在重重wrapper中丢失了原本的信息,这就导致debug变成了一件非常痛苦的事情。

Dio 踩坑

最近写的代码埋雷了,如下它的作用是将Dio请求内容转换为 Debug 记录, 最后通过interceptor打印出 log 来。

String getResponseDebug(Response resp) {
  if (resp == null) return 'getResponseDebug()->null';
  //debug
  var debug = getRequestDebug(resp.request);
  debug += '\nresponse--> ${resp.statusCode} ${resp.statusMessage}';
  if (resp.data is Map) {
    debug += '\n\tdata-->\n${prettyJson(resp.data)}\n';
  } else if (resp.data is List) {
    var data = (resp.data as List);
    var json = data.length >= 1 ? [data.first] : data;
    debug += '\n\tdata-->\n${prettyJson(json)}\n';
  } else {
    debug += '\n\tdata-->\n${resp.data}\n';
  }
  if (resp.isRedirect) {  // 请注意这一行!!!
    debug += '\n\tredirects-->\n${prettyJson(resp.redirects)}\n';
  }
  return debug;
}

看上去没有啥问题的代码,却在我使用dio_http_cache进行缓存的时候出问题了。

为了定位这个问题,我还顺便弄清除了Diointerceptor的机制,可谓是收获颇丰啊。

好家伙,进去isRedirect属性一看

  /// Whether this response is a redirect.
  /// ** Attention **: Whether this field is available depends on whether the
  /// implementation of the adapter supports it or not.
  final bool isRedirect;

大大的attention摆在哪里,可惜等我注意到它的时候问题都已经修好了。

下次是不是该留一个心眼,明明这么明显属性,居然会有null的可能性。只能说明我太想当然了吧(too young,too simple)。

关于 null

最早接触null的时候,就是 C 语言的NULL空指针,它在 C 与 C++有着不同的区别。C 语言中它只是一个0罢了,代表了一个指向地址0的指针;而在 C++中,它才真的是null(带类型的)。

所以,后面关于null我都会把它当初 C 语言中的指针来理解。

而在dartlanguage tutorial中,就有这么一句话

Everything you can place in a variable is an object, and every object is an instance of a class. Even numbers, functions, and null are objects. All objects inherit from the Object class.

dart中,任何可以放在变量里面的都是一个Object, 而这个Object只是一个类的实例,所有的对象都是继承于Object,甚至是数值,函数和null

//这里nullStr可以理解成一个指向String指针
//声明之后没有赋值它的值是一个null
String nullStr;

为了让nullStr变得可以使用,我们需要手动让它指向一个对象,例如nullStr="Hello",这个时候nullStr不在为null可以安全的使用。

所有我可以理解dart是一门纯粹面向对象语言,你可以发现任何类型都有toString()这个方法,因为它是继承于Object里面的方法,也因此你可以将null赋值给任何一个类型的变量。

这样做的好处不言而喻,个人理解,它减少了程序员的心智负担,再也不用区分函数传参中的按值传递按引用传递,可以将注意力集中在具体的业务上面。

但是实际情况可能不是我想的这样美好,例如下面这段代码

//exchange a and b, does it work?
void exchange(int a,int b){
    var c = a;
    a = b;
    b = c;
}

void exchange2(int a){
    a = 100;
}

事实上,它并不能正常工作,我甚至找不到一个办法来实现a,b之间的交换。

但是下面这段代码可以正常工作

class Student {
  String no;
  String name;

  Student({this.no, this.name});

  String toString() {
    return '{ no: $no, name: $name }';
  }
}

void updateName(Student s, String newName) {
  s.name = newName;
}

void main(List<String> args) {
  var stu = Student(
    no: '1001',
    name: 'Jack Li',
  );

  print('Student: $stu');
  updateName(stu, 'New Name');
  print('Student: $stu');
}

/* output
Student: { no: 1001, name: Jack Li }
Student: { no: 1001, name: New Name }
*/

从上面的结果可以看到,转递过去的对象可以被修改其内部属性。所以我的理解是按引用传参,也就是函数updateNamesstu是指向同一个对象的,也就是持有这个对象的引用。如果在函数里面尝试对引用进行交换,则是无用的。因为交换的只是函数内部变量持有的引用,不会产生交换的效果。

对于如int类型,在其它函数进行修改无非就是让它指向一个新的值罢了,并不会影响之前的那个值,所以它表现得就如同按值传递这个行为了。

stackoverflow 上有关这个的讨论

Interceptors

关于 Dio 这个库,最舒服的一点就是它的interceptor设计, 字面上理解就是拦截器。

通过它,可以实现在请求发送之前进行拦截,修改请求参数或者改变请求的流程直接进入response

还可以在数据返回的时候进行拦截,判断返回的数据是否异常等。

例如下面通过规定API返回类型,来统一处理API异常数据。

class ApiResponseInterceptor extends InterceptorsWrapper {
  @override
  Future onResponse(Response response) async {
    //如果数据来自缓存,直接返回
    if ((response.headers.value(DIO_CACHE_HEADER_KEY_DATA_SOURCE) ?? '') ==
        'from_cache') {
      return response;
    }
    var resp = ApiResponse.fromJson(response.data);
    if (resp.code != 200) {
      throw DioError(
        request: response.request,
        response: response,
        error: resp,
      );
    }
    response.data = resp.data ?? [];
    return response;
  }
}

之后的还可以拦截错误,类似catch的效果。

可以说,有了interceptorDio库变得非常的好用。

而我在代码中使用的dio-http-cache就是通过interceptor来实现对请求进行缓存。

理解了这点,去看dio-http-cache如何处理response的数据

Response _buildResponse(
    CacheObj obj, int statusCode, RequestOptions options) {
    Headers headers;
    if (null != obj.headers) {
    headers = Headers.fromMap((Map<String, List<dynamic>>.from(
            jsonDecode(utf8.decode(obj.headers))))
        .map((k, v) => MapEntry(k, List<String>.from(v))));
    }
    if (null == headers) {
    headers = Headers();
    options.headers.forEach((k, v) => headers.add(k, v ?? ""));
    }
    // add flag
    headers.add(DIO_CACHE_HEADER_KEY_DATA_SOURCE, "from_cache");
    dynamic data = obj.content;
    if (options.responseType != ResponseType.bytes) {
    data = jsonDecode(utf8.decode(data));
    }
    return Response(
        data: data,
        headers: headers,
        extra: options.extra..remove(DIO_CACHE_KEY_TRY_CACHE),
        statusCode: statusCode ?? 200);
}

很容易发现最后返回Response的时候并没有处理isRedirect这个属性,因此这个属性自然而然变成了null

最后

经过这番挖掘,对dart中的null有了多一点的认识,也意识到平时不认真看文档后果的严重性;更重要的是,发现了阅读别人代码的好处,就是可以更加的理解其实现原理,更好的避免坑。

Last modification:January 7th, 2021 at 10:34 pm
要饭啦~