Dart入门?一篇文章就够了!

近期公司准备启动新项目,经过技术团队一番调研,决定采用 Flutter 来开发第一版App,故作此文,常来回顾温习。由于项目采用敏捷开发模式,故本文主要总结和记录 Dart 常用语法,更多高级和生僻用法将在后面开发过程中不定期更新。

First of all

在我们正式接触 Dart 语法之前,需要铭记以下内容,这将会对后续 Dart 语法的学习、理解和应用有很大帮助:

  • 万物皆对象, 每个对象都是一个类的实例。在 Dart 中,甚至连数字、方法和 null 都是对象,并且所有的对象都继承于 Object 类。
  • 尽管 Dart 语言是一种强类型语言,但你在类型声明时仍然可以不指定类型,因为 Dart 可以自动进行类型推断。如在代码 var number = 5; 中,number 变量的类型就被推断为 int,当你并不想显式地声明类型时,你可以使用特有的类型 dynamic 来标识。
  • Dart 语言同样支持泛型,如 List<int>List<dynamic>(同 Java 中的 List<Object>)。
  • Dart 语言支持顶级方法(即不与类绑定的方法,如上的 main 方法),以及绑定类和实例的方法(分别对应静态方法和实例方法),而且还支持方法嵌套(同 Python 和 JS)。
  • 同样,Dart 还支持顶级变量,以及在类中定义的变量(如静态变量和实例变量)。
  • 和 Java 不同的是,Dart 没有 publicprotected 以及 private 关键字。它通过给变量或者方法名加上 _ 前缀来将其标识为私有域。

变量

你可以通过 var 或者特定的类型来修饰变量,如果变量的类型不是固定的,也可以通过 dynamic 或者 Object 来修饰。如:

// 类型推断
var name = 'Bob';
// 指定类型
String name = 'Bob';
// 类型不局限于string
dynamic name = 'Bob';

默认值

Dart 中声明时如果未初始化,会被自动初始化为null

int lineCount;
assert(lineCount == null);

注:assert 只在开发环境有效,在实际的生产环境无效。

final 与 const

finalconst 标识该变量只能被赋值一次,区别在于 final 是运行时赋值,const 是编译时赋值。

final String nickname = 'Jack';
// final修饰时, 可省略类型声明
final name = 'Jack';

const不仅可以用来声明变量,还能用来创建常量值,如:

// 此时不能在对列表a进行更新操作
var a = const [1, 2];
// 注意以下 const 修饰的区别
var foo = const [];
const baz = []; // Equivalent to `const []`
...
foo = [1, 2, 3]; // Was const []
baz = [42]; // Error: Constant variables can't be assigned a value.

数据类型

Dart 语言支持的数据类型与 Java 类似,但也有所不同,下面我们来按照“求同存异”的法则来分别认识它们。

数字类型

Dart 中的数字类型只有两种:Intdouble。与 Java 不同的是,它并没有提供 float 类型。例如,我们可以通过以下方式来定义数字:

// 定义整型
var x = 123;
var hex = 0xDEADBEEF;
// 定义双精度类型
var y = 1.199;
var exponents = 1.42e5;

再来看下常用的数据转换该如何执行:

// String -> int
var a = int.parse('1');
assert(a == 1);

// String -> double
var b = double.parse('1.1');
assert(b == 1.1);

// int -> String
String c = 1.toString();
assert(c == '1');

// double -> String
String d = 3.14159.toStringAsFixed(2);
assert(d == '3.14');

int 类型可以执行传统的按位移位( <<>> ),AND( & )和OR( | )运算符,如:

assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 >> 1) == 1); // 0011 >> 1 == 0001
assert((3 | 4) == 7);  // 0011 | 0100 == 0111

字符串类型

字符串的声明可以通过单引号或者双引号来引入:

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

字符串拼接可以通过 + 或者 换行 来实现,如:

var a = '123' + '456'
var b = '123'
    '456';
assert(a == b)
print(a);
// Out: 123456

另外,可以通过三重引号来修饰多行字符串(维持特定格式):

var s1 = '''
You can create
multi-line strings like this one.
''';

var s2 = """This is also a
multi-line string.""";

布尔类型

Dart 中的 bool 类型可以通过 if-else 或者是 assert 来检查,如:

// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);

// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);

// Check for null.
var unicorn;
assert(unicorn == null);

// Check for NaN.
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

数组类型

在 Dart 语言中,数组即列表,列表即数组。

void main() {
  var list = [1, 2, 3];
  list.add(4);
  list.remove(3);
  list[1] = 5;
  var last = list[list.length - 1];
  print('list is $list');
  print('last is $last');
}

另外,你可以通过 或者 ...?(避免空异常) 来批量插入多个元素:

var list1 = [1, 2, 3];
var list2 = [0, ...list1];

var list3;
var list24 = [0, ...?list3];

除此以外,还可以通过 if 或者 for 条件运算符来插入元素:

var nav = [
  'Home',
  'Furniture',
  'Plants',
  if (promoActive) 'Outlet'
];

var listOfInts = [1, 2, 3];
var listOfStrings = [
  '#0',
  for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');

Set 和 Map

Dart中的Set是无序的唯一项的集合。要创建一个空集合,有以下两种方式:

var names = <String>{};
Set<String> names = {}; // This works, too.
// var names = {}; // Creates a map, not a set.

通过以下方式创建常量 Set:

final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // Uncommenting this causes an error.

至于 Map 存储的是键值对数据,keyvalue 可以是任何类型。可以通过以下方式创建一个 Map:

var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

// 或者通过以下方式
var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

函数

Dart 是一门完全面向对象的语言,即使它的函数也是一个对象,并且有自己的类型—— Function。这就意味着函数可以被赋值为一个变量,或者作为一个参数在其他函数之间传递

对于只包含一个表达式的方法体,你也可以使用 => 的简写形式,如:

void say() {
    print('123');
}

// 该写法与以上有同样效果
void say2() => print('123');

此外,函数同样支持类型推断,如:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

// 与下面的效果一样
isNoble(atomicNumber) => _nobleGases[atomicNumber] != null;

可选参数

与 kotlin 语言类似,Dart 的 函数同样支持可选参数这一功能,主要分为命名可选参数位置可选参数,它们不能在一个函数中同时使用。但它们都支持默认参数设置

  • 命名可选参数

    命名可选参数通过花括号 {} 来指定可选的命名参数,如:

    void main() {
      hello(name: 'Tom');
    }
    
    void hello({@required String name='Jack', int age=18}) {
      print('Hello, my name is $name, i\'m $age years old.');
    }
    

    我们可以为命名可选参数设置默认值,在使用的时候如果不指定该可选参数的值,那么它就会自动使用默认值。另外,我们可以通过 @required 来标注那些不想被忽略的参数。

  • 位置可选参数

    顾名思义,可选位置参数就允许我们在使用某个函数时可以忽略某个参数,如:

    void main() {
      sayHello('Tom', 19);
    }
    
    void sayHello(String name, int age, [String hobby = null]) {
      var result = 'Hello, my name is $name, i\'m $age years old';
      if (hobby != null) {
          result += ', my bobby is $hobby.';
      }
      print(result);
    }
    

匿名函数

不管什么语言,好像都能看到匿名函数的身影,我们可以通过以下方式来简单声明一个匿名函数:

var loge = (msg) => Logger.print(Logger.Error, msg);

void main() {
  loge("there has some errors");
}

Dart 中 list 就提供了匿名函数 —— forEach,如:

var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
  print('${list.indexOf(item)}: $item');
});

常用操作符

Dart 提供了很多功能强大的操作符,这里列举几个常用的:

  • 类型判断

    如:is 相当于 Java 中的 instanceof

  • 整除

    如:a ~/ b 等价于 (a /b) as int

  • 非空调用

    如:a?.b 相当于 a == null ? null : a.b

  • 三目运算

    如:a??b 相当于 a == null ? b : a

  • 三目赋值运算

    如:a ??= b 相当于 a = a == null ? b : a

  • 类型转换

    如:a as int 相当于 Java 中的 (int) a

  • 级联操作符

    级联操作符用.. 来表示,主要用来对一个对象进行连续操作,有点类似 kotlin 中的 apply 和 let,如:

    var button = Button(this);
    button.text = 'Confirm';
    button.color = Color.parse("#f3f355");
    button.onClick.listen((v) => toast('Confirmed'));
    
    // 等价于下面写法
    Button(this) 
      ..text = 'Confirm' // Use its members.
      ..color = Color.parse("#f3f355")
      ..onClick.listen((v) => toast('Confirmed'));
    

异常处理

抛异常

在 Dart 中可以抛出非空对象(不仅仅是 ExceptionError)作为异常。抛出异常的方式很简单,只需要这样:

throw FormatException('Expected at least 1 section');

throw 'Out of llamas!';

void distanceTo(Point other) => throw UnimplementedError();

捕获异常

我们可以通过 try-on 来捕获某个特定异常,如:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}

当然,你可以抛出多个类型的 Exception,但是由第一个 catch 到的分句来处理
如果 catch 分句没有指定类型,那么它可以处理任何类型的异常:

try {
      throw 'This a Exception!';
  } on Exception catch(e) {
    print('Unknown exception: $e');
  } catch(e) {
    print('Unknown type: $e');
  }

无论是否发生异常,如果想要执行某段代码,可以通过 finally 实现:

try {
      throw 'This a Exception!';
  } catch(e) {  // 如果去掉 catch 语句,那么异常将会在finally代码块之后传递。
    print('Catch Exception: $e');
  } finally {
    print('Close');
  }

类和对象

Dart 中,所有的对象都是类的实例,并且所有的类都是 Object 的子类。

类的定义和构造函数

类的定义用 class 关键字,如果未显式定义构造函数,会默认一个空的构造函数,这一点与 Java 不谋而合。另外,Dart 还提供 命名构造函数这一功能,格式为:Class.costructorName(var params)。这种用法在某些场景下很实用,比如:

class Person{
  String name;
  int age;
  bool sex;
  String hobby;

  Person(this.name, this.age, this.sex, this.hobby);

  /// 命名构造函数
  Person.fromJson(Map json){
    print("Person constructor...");
    this.name  = json['name'];
    this.age   = json['age'];
    this.sex   = json['sex'];
    this.hobby = json['hobby'];
  }
}

通过命名构造函数,我就可以直接通过多种方式来生成当前类的实例,这里通过 Json 格式数据生成 Person 对象。另外,如果构造函数只是简单的参数传递,也可以直接在构造函数后加 : 来对参数进行赋值,多个参数可通过 , 相连:

class Point {
    num x;
    num y;
    num z;
    
    Point(this.x, this.y, z) { //第一个值传递给this.x,第二个值传递给this.y
            this.z = z;
    }
    
    Point.fromeList(var list): //命名构造函数,格式为Class.name(var param)
            x = list[0], y = list[1], z = list[2]{//使用冒号初始化变量
    }

    //当然,上面句你也可以简写为:
    //Point.fromeList(var list): this(list[0], list[1], list[2]);

     String toString() => 'x:$x  y:$y  z:$z';
}

如果你要创建一个不可变的对象,你可以定义编译时常量对象
需要在构造函数前加 const

class ImmutablePoint {
    final num x;
    final num y;
    const ImmutablePoint(this.x, this.y); // 常量构造函数
    static final ImmutablePoint origin = const ImmutablePoint(0, 0); // 创建一个常量对象不能用new,要用const
}

属性访问器

即我们常说的 Setter/Getter,主要是用来读写一个属性的方法。每个字段都对应一个隐式的 GetterSetter,但是调用的时候是 obj.name,而不是 obj.name()。当然,你可以通过 getset 关键字扩展功能
来实现自己的 Setter/Getter , 如果字段为 final 或者 const 的话,那么它只能有一个 getter 方法。如:

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  num get right => left + width;
  set right(num value) => left = value - width;
  num get bottom => top + height;
  set bottom(num value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

工厂构造函数

并不是任何时候我们都需要创建一个新的对象,如:可以返回一个已经缓存好的对象或者该类的子类对象。我们以返回该类的一个缓存对象为例:

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

注意:factory 构造函数不能通过 this 关键字来获取类的成员。

抽象类

Dart 中并没有 interface 关键字,只有 abstract 来修饰"抽象类",但是,这里的抽象类既可以被继承(extends),也可以被实现(implements)。如:

abstract class Person {  // 可以不用 abstract 修饰,如果加上 abstract 的话 Person 不能被实例化 
    String greet(who);   
}

class Student implements Person { 
    String name; 
    Student(this.name); 
     
    String greet(who) => 'Student: I am $name!'; 
} 

class Teacher implements Person { 
    String name; 
    Teacher(this.name); 
   
    String greet(who) => 'Teacher: I am $name!'; 
} 

void main() { 
    Person student = new Student('Wang'); 
    Person teacher = new Teacher('Lee'); 
    
    print( student.greet('Chen')); 
    print(teacher.greet('Chen')); 
}

同样地,Dart 中的类是单继承,多实现。这里使用的 implements 后,子类 Student 无法去访问父类 Persion 中的变量,但是抽象方法必须显式实现。相反,如果使用 extends 继承,子类就可以直接使用父类的非私有变量,倘若父类不是抽象类,那么子类同样不需要显式实现里面方法。如:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

运算符重载

以下的运算符支持重载功能:

| < | + | | | [] |
| ---- | ---- | ---- | ----- |
| > | / | ^ | []= |
| <= | ~/ | & | ~ |
| >= | * | << | == |
| | % | >> | |

这一点与 kotlin 语言中的运算符重载功能类似。我们以下面这个表示平面向量的类为例,为它增加两个向量相加和相减功能:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

  // Operator == and hashCode not shown. For details, see note below.
  // ···
}

void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

枚举

枚举的使用比较简单,和 Java 中的用法类似,声明的元素 index 值从 0 开始计算。

enum Color { red, green, blue }
......
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);

Mixin 混合模式

Mixins 是一种在多个类的层级中复用类中代码的一种方式。在类名后添加 with 关键字,并在 with 后紧跟类的标识符列表,通过逗号分隔,如:

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

如果想要声明一个混合模式,可以通过 mixin 来代替 class 定义一个不含构造方法的类:

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

当然,我们也可以指定某个类来使用定义的混合模式,通过 on 关键字:

mixin MusicalPerformer on Musician {
  // ···
}

静态变量和函数

定义静态变量和函数的方法与 Java 类似:

class Point {
  num x, y;
  Point(this.x, this.y);
  static const originalPoint = Point(0, 0);

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

// usage
void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

异步操作

Dart 是单线程执行的,如果进行 I/O 操作或进行耗时的操作时,程序可能会发生阻塞。Dart 库中的很多函数都返回 Future 或者 Stream 对象,这些方法都是异步执行的,它们可以在建立一个耗时操作之后返回,而无需等待耗时操作执行完成。

我们可以通过两种方式来实现异步操作:

  • 通过 asyncawait 实现
  • 借助于 Future API 来实现

async 和 await

通过 asyncawait 关键字,我们可以轻松实现异步操作。要使用 await,必须要将函数用 async 修饰,同时函数会返回一个 Future 对象。简单用法如下:

Future checkVersion() async {
  try {
    version = await lookUpVersion();
  } catch (e) {
    // React to inability to look up the version
  }
}

生成器

  • 同步生成器 — sync*

    值得注意的是,同步生成器会返回 Iterable 对象:

    Iterable<int> naturalsTo(int n) sync* {
      int k = 0;
      while (k < n) yield k++;
    }
    
  • 异步生成器 — async*

    异步生成器返回 Stream 对象:

    Stream<int> asynchronousNaturalsTo(int n) async* {
      int k = 0;
      while (k < n) yield k++;
    }
    
  • yield*

    通过 yield* 在递归中提升性能:

    Iterable<int> naturalsDownFrom(int n) sync* {
      if (n > 0) {
        yield n;
        yield* naturalsDownFrom(n - 1);
      }
    }
    

将类对象作为函数调用

Dart 允许我们将一个类的对象作为函数来调用,只需要实现 call() 函数即可:

class WannabeFunction {
  call(String a, String b, String c) => '$a $b $c!';
}

main() {
  var wf = new WannabeFunction();
  var out = wf("Hi","there,","gang");
  print('$out');
}

未完待续

此文只是 Dart 征程的一个起点,亦是 Flutter 中的一块基石,后续还有更多进阶用法和细节 API 等着我们去挖掘,keep learning!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,585评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,283评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,760评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,461评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,280评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,268评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,656评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,322评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,629评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,691评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,445评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,299评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,694评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,982评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,244评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,642评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,829评论 2 335

推荐阅读更多精彩内容

  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,713评论 2 9
  • Dart重要概念:1,在变量中可以放置的所有东西都是对象,而每个对象都是类的实例。无论数字、函数、和null都是对...
    哥哥是欧巴Vitory阅读 783评论 0 1
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,068评论 1 32
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young阅读 3,757评论 1 10
  • 是或者否,你曾为我停留 有或者没有,为我动心过 够还是不够,冲动的理由 对还是错,相遇的时刻 低头是浮游,抬头见苍...
    范有米阅读 237评论 0 0