Dart 基本语法 (二)

一、Dart运算符

1.1. 除法、整除、取模运算

void main(List<String> args) {
  var num = 7;
  print(num / 3); // 除法操作, 结果2.3333..
  print(num ~/ 3); // 整除操作, 结果2;
  print(num % 3); // // 取模操作, 结果1;
}

1.2. ??=赋值操作

赋值运算符 ??=
当变量为null时,使用后面的内容进行赋值。
当变量有值时,使用自己原来的值。


  // ??=
  // 当原来的变量有值时,那么??= 不执行
  // 当原来的变量为null时,那么将值赋值给这个变量
  var name1 = "why";
  name1 ??= "lilei";
  print(name1);

  var name2 = null;
  name2 ??= "lilei";
  print(name2);


1.3. 条件运算符 ??

条件运算符:expr1 ?? expr2
如果expr1是null,则返回expr2的结果;
如果expr1不是null,直接使用expr1的结果

  // ??
  // ?? 前面的数据有值,那么就使用??前面的数据
  // ?? 前面的数据为null,那么就使用后面的值
  var name1 = "why";
  var name2 = name1 ?? "lilei";
  print(name2);

  var name3 = null;
  var name4 = name3 ?? "lilei";
  print(name4);

1.4. 级联语法:..

某些时候,我们希望对一个对象进行连续的操作,这个时候可以使用级联语法

void main(List<String> args) {
  final p1 = Person();
  p1.name = 'why';
  p1.run();
  p1.eat();

  final p2 = Person()
    ..name = 'why'
    ..run()
    ..eat();
}

class Person {
  String? name;
  void run() {
    print("$name runing");
  }

  void eat() {
    print("$name eating");
  }
}

1.5. 流程控制

1.5.1 if esle

使用isEmpty来检测是否为空,但是isEmpty不能用来判断null会报错。要判断是否为null,直接 a == null就好了

  String? name = "sdsd";
  print(name?.isEmpty);
  print(name?.isNotEmpty);
  print(name?.length);
  if (name != null && name.isNotEmpty) {
    print("name is $name");
  } else {
    print("name is null");
  }

1.5.2 循环操作

  • 基本的for循环
  for (var i = 0; i < 5; i++) {
    print(i);
  }
  • for - in
  List<int> numbers = [1, 2, 3, 2, 1];
  for (var num in numbers) {
    print(num);
  }

  var numSet = numbers.toSet();

  for (var num in numSet) {
    print(num);
  }

  Map<String, Object> info = {"name": "why", "age": 18, "height": 1.88};
  for (var item in info.entries) {
    print('$item, ${item.key}, ${item.value}');
  }
  • do - while & while
  var i = 10;
  do {
    print(i);
    i += 1;
  } while (i > 10 && i < 20);

  while (i > 0) {
    print(i);
    i -= 1;
  }
  • break & continue
  for (var i = 0; i < 10; i++) {
    if (i > 0 && i < 5) {
      continue;
    }
    print(i);

    if (i == 7) {
      break;
    }
  }
  • switch - case
  var direction = "south";
  switch (direction) {
    case 'east':
      print("东面");
      break;
    case "south":
      print("南面");
      break;
    case "west":
      print("西面");
      break;
    case "north":
      print("北面");
      break;
    default:
      print("其他方向");
  }

二、类和对象

2.1. 类的定义

在Dart中,定义类用class关键字。
类通常有两部分组成:成员(member)和方法(method)。
定义类的伪代码如下:

class 类名 {
  类型 成员名;
  返回值类型 方法名(参数列表) {
    方法体
  }
}
class Person {
  String? name;
  void eat() {
    print('$name eat');
  }
}

注意: 我们在方法中使用属性(成员/实例变量)时,并没有加this;
Dart的开发风格中,在方法中通常使用属性时,会省略this,但是有命名冲突时,this不能省略;

  // 1.创建类的对象 
  // 从Dart2开始,new关键字可以省略。
  var p =  new Person(); // 直接使用Person()也可以创建
  // 2.给对象的属性赋值
  p.name = 'why';
  // 3.调用对象的方法
  p.eat();

注意:从Dart2开始,new关键字可以省略。

2.2. 类的构造方法

2.2.1 普通构造方法

当通过类创建一个对象时,会调用这个类的构造方法。
当类中没有明确指定构造方法时,将默认拥有一个无参的构造方法。

定义自己的构造方法:
注意:
当有了自己的构造方法时,默认的构造方法将会失效,不能使用
当然,你可能希望明确的写一个默认的构造方法,但是会和我们自定义的构造方法冲突

这是因为Dart本身不支持函数的重载(名称相同, 参数不同的方式)

class Person {
  String? name;
  int? age;
  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
}

另外,在实现构造方法时,通常做的事情就是通过参数给属性赋值
为了简化这一过程, Dart提供了一种更加简洁的语法糖形式.
上面的构造方法可以优化成下面的写法:

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  // 等同于
  Person(this.name, this.age);
void main(List<String> args) {
  var p1 = Person1("why", age: 18, height: 1.88);
  var p2 = Person2('why', 18, 1.88);
}

class Person1 {
  String? name;
  int? age;
  double? height;
  // 必须参数 name,命名可选参数 age height
  Person1(this.name, {this.age, this.height});
}

class Person2 {
  String? name;
  int? age;
  double? height;
  // 必须参数 name,位置可选参数 age height
  Person2(this.name, [this.age, this.height]);
}

2.2.2 命名构造方法

但是在开发中, 我们确实希望实现更多的构造方法,怎么办呢?
因为不支持方法(函数)的重载,所以我们没办法创建相同名称的构造方法。
我们需要使用命名构造方法

void main(List<String> args) {
  var p1 = Person1('why');
  print(p1);
  var p2 = Person1.withNameAndAge('why', 18);
  print(p2);
}

class Person1 {
  String? name;
  int? age;
  double? height;
  Person1(this.name);
  Person1.withNameAndAge(this.name, this.age);

  // 重写toString方法
  @override
  String toString() {
    return "$name - $age - $height";
  }
}

在之后的开发中, 我们也可以利用命名构造方法,提供更加便捷的创建对象方式:
比如开发中,我们需要经常将一个Map转成对象,可以提供如下的构造方法

void main(List<String> args) {
  Map<String, dynamic> map = {"name": "why", "age": 18, "height": 1.88};
  var p = Person2.formMap(map);
  print(p);
}

class Person2 {
  String? name;
  int? age;
  double? height;
  Person2.formMap(Map<String, dynamic> map) {
    this.name = map["name"];
    this.age = map["age"];
    this.height = map["height"];
  }

  // 重写toString 方法
  @override
  String toString() {
    return "my name is $name, age is $age, height is $height";
  }
}

dynamic 和 Object 区别

Object调用方法时,编译时会报错
dynamic调用方法时,编译时不报错,但运行时会存在安全隐患

  // dynamic 明确声明
  dynamic obj1 = "name";
  print("${obj1.runtimeType}"); // String
  print(obj1.substring(1)); // 编译 不报错
  
  // Object
  Object obj2 = "name";
  print("${obj2.runtimeType}"); // String
  print(obj2.substring(1)); // 编译 会报错

2.2.3 初始化列表

类成员变量赋初始值

class Person {
  String? name;
  int? age;
  double? height;
  Person(String? name, {int? age, double? height}) {
    this.name = name;
    this.age = age ?? 18;
    this.height = height ?? 1.88;
  }
}

但如果成员变量是用final修饰,上述赋值会报错

// 错误写法 错误 错误
class Person {
  final String? name;
  final int? age;
  final double? height;
  Person(String? name, {int? age, double? height}) {
    this.name = name; // 报错  'name' can't be used as a setter because it's final.
    this.age = age ?? 18; // 报错 'age' can't be used as a setter because it's final.
    this.height = height ?? 1.88; // 报错 'height' can't be used as a setter because it's final.
  }
}

原因:final 修饰的是一个常量。可以在运行前赋值或运行期间赋值。对于类,在运行期间只会执行Person(String? name, {int? age, double? height})这段语句,并不会执行后续{}中的语句。也就是说在运行期间final修饰的变量没有赋值

在构造函数中,对final修饰的成员变量赋值

class Person {
  final String? name;
  final int? age;
  final double? height;
  Person(this.name, {this.age = 10, this.height = 1.88});
}

或者

class Person {
  final String? name;
  final int? age;
  final double? height;

  Person(this.name, {int? age, double? height})
      : this.age = age ?? 10,
        this.height = height ?? 1.88;
}

我们重新定义一个类Point, 传入x/y,可以得到它们的距离distance:

class Point {
  final int x;
  final int y;
  final double distance;

  // 错误写法
  // Point(this.x, this.y) {
  //   distance = sqrt(x * x + y * y);
  // }

  // 正确写法
  Point(this.x, this.y) : this.distance = sqrt(x * x + y * y);
}

2.2.4 重定向构造函数

在某些情况下, 我们希望在一个构造方法中去调用另外一个构造方法, 这个时候可以使用重定向构造方法:
在一个构造函数中,去调用另外一个构造函数(注意:是在冒号后面使用this调用)

class Person {
  final String? name;
  final int? age;

  Person(String? name): this._internal(name, 10);
  Person._internal(this.name, this.age);
}

2.2.5 常量构造方法

在某些情况下,传入相同值时,我们希望返回同一个对象,这个时候,可以使用常量构造方法.
默认情况下,创建对象时,即使传入相同的参数,创建出来的也不是同一个对象

如果将构造方法前加const进行修饰,那么可以保证同一个参数,创建出来的对象是相同的,这样的构造方法就称之为常量构造方法。

void main(List<String> args) {
  const p1 = Person('why');
  const p2 = Person('why');
  print(identical(p1, p2)); // true 是同一个对象

  final p3 = Person('why');
  final p4 = Person("why");
  print(identical(p3, p4)); // false 不是同一个对象

  var p5 = const Person('why');
  var p6 = const Person('why');
  print(identical(p5, p6)); // true 是同一个对象
}

class Person {
  final String? name;
  const Person(this.name);
}

注意1:拥有常量构造方法的类中,所有的成员变量必须是final修饰的.
注意二: 为了可以通过常量构造方法,创建出相同的对象,不再使用 new关键字,而是使用const关键字
如果是将结果赋值给const修饰的标识符时,const可以省略

2.2.6 工厂构造方法

Dart提供了factory关键字, 用于通过工厂去获取对象

普通的构造函数:会自动返回创建出来的对象,不能手动返回
工厂构造函数最大的特点:可以手动返回一个对象

void main(List<String> args) {
  var p1 = Person.withName("why");
  var p2 = Person.withName("why");
  print(identical(p1, p2));

  var p3 = Person.withColor("red");
  var p4 = Person.withColor("red");
  print(identical(p3, p4));
}

class Person {
  String? name;
  String? color;
  
  static final Map<String, Person> _nameCache = {};

  // 普通的构造函数:会自动返回创建出来的对象,不能手动返回
  // 工厂构造函数最大的特点:可以手动返回一个对象
  factory Person.withName(String name) {
    if (_nameCache.containsKey(name)) {
      return _nameCache[name]!;
    } else {
      var p = Person(name, "defaultColor");
      _nameCache[name] = p;
      return p;
    }
  }

  static final Map<String, Person> _colorCache = {};
  factory Person.withColor(String color) {
    if (_colorCache.containsKey(color)) {
      return _colorCache[color]!;
    } else {
      var p = Person("defaultName", color);
      _colorCache[color] = p;
      return p;
    }
  }

  Person(this.name, this.color);
}

2.3. setter和getter

默认情况下,Dart中类定义的属性是可以直接被外界访问的。
但是某些情况下,我们希望监控这个类的属性被访问的过程,这个时候就可以使用setter和getter了

直接访问属性,不走get/set方法

void main(List<String> args) {
  final p = Person('why');

  // 直接访问属性
  p.name = "wang";
  print(p.name);

  // 通过getter和setter访问
  p.setName = "lilei";
  p.setName = 'why';
  print(p.getName);
}

class Person {
  String? name;
  set setName(String name) {
    this.name = name;
    print("person set name");
  }

  String? get getName {
    print("person get name");
    return name;
  }

  Person(this.name);
}

2.4. 类的继承

Dart中的继承使用extends关键字,子类中使用super来访问父类

  • 父类中的所有成员变量和方法都会被继承,,但是构造方法除外
void main(List<String> args) {
  final p = Person();
  p.age = 18;
  p.run();
}

class Animal {
  int? age;
  void run() {
    print("running");
  }
}

class Person extends Animal {
  
}
  • 子类可以拥有自己的成员变量, 并且可以对父类的方法进行重写
void main(List<String> args) {
  final p = Person();
  p.name = "why";
  p.run(); // 打印 Person Runing
}

class Animal {
  int? age;
  void run() {
    print("Animal Running");
  }
}

class Person extends Animal {
  String? name;
  @override
  void run() {
    print("Person Runing");
  }
}

  • 子类中可以调用父类的构造方法,对某些属性进行初始化:
    • 子类的构造方法在执行前,将隐含调用父类的无参默认构造方法(没有参数且与类同名的构造方法)
    • 如果父类没有无参默认构造方法,则子类的构造方法必须在初始化列表中通过super显式调用父类的某个构造方法
class Animal {
  int? age;
  Animal(this.age);
}

class Person extends Animal {
  String? name;
  //Person(this.name, int age) : super(age);
  Person(String name, int age) : name = name, super(age);
}

2.5. 抽象类

我们知道,继承是多态使用的前提。
所以在定义很多通用的调用接口时, 我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。
但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,,我们可以定义为抽象方法。

什么是 抽象方法? 在Dart中没有具体实现的方法(没有方法体),就是抽象方法
抽象方法,必须存在于抽象类中
抽象类是使用abstract声明的类

下面的代码中, Shape类就是一个抽象类, 其中包含一个抽象方法.

abstract class Shape {
  // 定义一个抽象方法
  getArea();
}

// 继承抽象类后,必须实现抽象类中的抽象方法
class Circle extends Shape {
  double? r;
  Circle(this.r);

  @override
  getArea() {
    return r! * r! * 3.14;
  }
}

// 继承抽象类后,必须实现抽象类中的抽象方法
class Reactangle extends Shape {
  double? w;
  double? h;
  Reactangle(this.w, this.h);

  @override
  getArea() {
    return w! * h!;
  }
}

注意事项:
注意一:抽象类不能直接通过普通构造函数(Shape())实例化,但可以通过工厂构造函数实例化
注意二:抽象类中的抽象方法必须被子类实现, 抽象类中的已经被实现方法, 可以不被子类重写

2.6. 隐式接口

Dart中的接口比较特殊, 没有一个专门的关键字来声明接口.
默认情况下,定义的每个类都相当于默认也声明了一个接口,可以由其他的类来实现(因为Dart不支持多继承)

当将一个类做接口使用时,那么实现这个接口的类,必须实现这个接口中的所有方法

在开发中,我们通常将用于给别人实现的类声明为抽象类:

class Runner {
  void running() {}
}

class Flyer {
  void flying() {}
}

// 当将一个类做接口使用时,那么实现这个接口的类,必须实现这个接口中的所有方法
class Superman implements Runner, Flyer {
  @override
  void running() {}

  @override
  void flying() {}
}
abstract class Runner {
  void running() {}
}

abstract class Flyer {
  void flying();
}

// 当将一个类做接口使用时,那么实现这个接口的类,必须实现这个接口中的所有方法
class Superman implements Runner, Flyer {
  @override
  void running() {}

  @override
  void flying() {}
}

2.7. Mixin混入

在通过implements实现某个类时,类中所有的方法都必须被重新实现(无论这个类原来是否已经实现过该方法)。

但是某些情况下,一个类可能希望直接复用之前类的原有实现方案,怎么做呢?
使用继承吗?但是Dart只支持单继承,那么意味着你只能复用一个类的实现。

Dart提供了另外一种方案: Mixin混入的方式
除了可以通过class定义类之外,也可以通过mixin关键字来定义一个类。
只是通过mixin定义的类用于被其他类混入使用,通过with关键字来进行混入

void main(List<String> args) {
  final p = SuperMan();
  p.running();
  p.eating();
  p.flying();
}

mixin Runner {
  void running();
}

mixin Flyer {
  void flying() {}
}

class Animal {
  eating() {}
}

// 混入的语法:1.定义可混入的类时,不能使用class,要使用mixin 2.with进行混入
class SuperMan extends Animal with Runner, Flyer {
  // Runner 没有实现 running 方法,此处需要实现
  @override
  void running() {}
}

2.8. 类成员和方法

在Dart中我们使用static关键字来定义类成员,类方法

void main(List<String> args) {
  final p = Person();
  p.name = "why";
  Person.courseTime = "80";

  p.eating();
  Person.gotoCourse();
}

class Person {
  // 成员变量
  String? name;
  // 类属性(静态属性)
  static String? courseTime;

  // 对象方法
  void eating() {}

  // 类方法(静态方法)
  static void gotoCourse() {}
}

2.9. 枚举

枚举在开发中也非常常见, 枚举也是一种特殊的类, 通常用于表示固定数量的常量值

9.1. 枚举的定义

枚举使用enum关键字来进行定义:

enum Colors {
  red,
  green,
  blue,
}

2.9.2 枚举的属性

枚举类型中有两个比较常见的属性:
index: 用于表示每个枚举常量的索引, 从0开始.
values: 包含每个枚举值的List.

void main(List<String> args) {
  print(Colors.red.index);
  print(Colors.values);
}

enum Colors {
  red,
  green,
  blue,
}

枚举类型的注意事项:
注意一: 您不能子类化、混合或实现枚举。
注意二: 不能显式实例化一个枚举

三、泛型

3.1. 为什么要使用泛型

在 API 文档中你会发现基础数组类型的实际类型是 List<E> 。 <…> 符号将 List 标记为 泛型 (或 参数化) 类型。 这种类型具有形式化的参数。 通常情况下,使用一个字母来代表类型参数, 例如 E, T, S, K, 和 V 等。

泛型:通俗的理解: 就是解决类的接口, 方法, 复用性, 以及对不特定数据类型的支持(类型校验),
在通俗一点,就是对于类型的约束.

3.2 List和Map的泛型

List使用时的泛型写法:

  // 创建List的方式
  var name1 = ['why', 'james', 111];
  print(name1.runtimeType); // List<Object>

  // 限制类型
  var name2 = <String>['why', 'james', 111]; // 最后一个报错
  List<String> name3 = ['why', 'james', 111]; // 最后一个报错

Map使用时的泛型写法:

void main(List<String> args) {
  // 创建Map的方式
  var info = {1: 'one', 'name': 'why', 'age': 18};
  print(info.runtimeType); // _InternalLinkedHashMap<Object, Object>

  var info2 = <String,String>{'name': 'why', 'age': 18};  // / 18不能放在value中
  Map<String,String> = {'name': 'why', 'age': 18};  // / 18不能放在value中
}

3.3 类定义的泛型

如果我们需要定义一个类, 用于存储位置信息Location, 但是并不确定使用者希望使用的是int类型,还是double类型, 甚至是一个字符串, 这个时候如何定义呢?
一种方案是使用Object类型, 但是在之后使用时, 非常不方便
另一种方案就是使用泛型

Location类的定义: Object方式

void main(List<String> args) {
  Location l1 = Location(10, "20.5");
  print(l1.y.runtimeType); // 类型是 String,但执行String类型的方法会报错
  print(l1.y.substring(1)); // 编译报错
}

class Location {
  Object? x;
  Object? y;
  Location(this.x, this.y);
}

Location类的定义: 泛型方式

void main(List<String> args) {
  Location l1 = Location<int>(10, 20);
  print(l1.x.runtimeType); // int

  Location l2 = Location<String>("x", "xyx");
  print(l2.y.runtimeType); // String
  print(l2.y.substring(1)); // yx 不报错

  Location l3 = Location(10, "1000");
  print(l3.x.runtimeType); // int
  print(l3.y.runtimeType); // String
  print(l3.y.substring(1)); // 000 不报错
  
}

class Location<T> {
  T? x;
  T? y;
  Location(this.x, this.y);
}

如果我们希望类型只能是num类型, 怎么做呢?

void main(List<String> args) {
  var l1 = Location<int>(10, 10);
  print(l1.x.runtimeType); // int

  var l2 = Location<String>("10", "20"); // 错误的写法, 类型必须继承自num
  var l3 = Location(10, "20"); // 错误的写法, 类型必须继承自num
  
}

class Location<T extends num> {
  T? x;
  T? y;
  Location(this.x, this.y);
}

3.4 泛型方法的定义

最初,Dart仅仅在类中支持泛型。后来一种称为泛型方法的新语法允许在方法和函数中使用类型参数

void main(List<String> args) {
  var names = ["why", "wwq"];
  var first = getFirst(names);
  print('$first,${first.runtimeType}'); // why,String
}

T getFirst<T>(List<T> ts) {
  return ts[0];
}

四、库的使用

在Dart中,你可以导入一个库来使用它所提供的功能。
库的使用可以使代码的重用性得到提高,并且可以更好的组合代码。
Dart中任何一个dart文件都是一个库,即使你没有用关键字library声明

4.1. 库的导入

import语句用来导入一个库,后面跟一个字符串形式的Uri来指定表示要引用的库,语法如下:

import '库所在的uri';
  • 常见的库URI有三种不同的形式

来自dart标准版,比如dart:io、dart:html、dart:math、dart:core(但是这个可以省略)

//dart:前缀表示Dart的标准库,如dart:io、dart:html、dart:math
import 'dart:io';

使用相对路径导入的库,通常指自己项目中定义的其他dart文件

//当然,你也可以用相对路径或绝对路径的dart文件来引用
import 'lib/student/student.dart';

Pub包管理工具管理的一些库,包括自己的配置以及一些第三方的库,通常使用前缀package

//Pub包管理系统中有很多功能强大、实用的库,可以使用前缀 package:
import 'package:flutter/material.dart';
  • 库文件中内容的显示和隐藏

如果希望只导入库中某些内容,或者刻意隐藏库里面某些内容,可以使用show和hide关键字

show关键字:可以显示某个成员(屏蔽其他)
hide关键字:可以隐藏某个成员(显示其他)

import 'lib/student/student.dart' show Student, Person;
import 'lib/student/student.dart' hide Person;
  • 库中内容和当前文件中的名字冲突
    当各个库有命名冲突的时候,可以使用as关键字来使用命名空间
import 'lib/student/student.dart' as Stu;
Stu.Student s = new Stu.Student();

4.2 库的定义

  • library关键字

通常在定义库时,我们可以使用library关键字给库起一个名字。
但目前我发现,库的名字并不影响导入,因为import语句用的是字符串URI

library math;
  • part 关键字

在开发中,如果一个库文件太大,将所有内容保存到一个文件夹是不太合理的,我们有可能希望将这个库进行拆分,这个时候就可以使用part关键字了

目前官方不推荐使用part 抽取,建议使用export

mathUtils.dart文件

part of "utils.dart";

int sum(int num1, int num2) {
  return num1 + num2;
}

dateUtils.dart文件

part of "utils.dart";

String dateFormat(DateTime date) {
  return "2020-12-12";
}

utils.dart文件

part "mathUtils.dart";
part "dateUtils.dart";

test_libary.dart文件

import "lib/utils.dart";

main(List<String> args) {
  print(sum(10, 20));
  print(dateFormat(DateTime.now()));
}
  • export 关键字

官方不推荐使用part关键字,那如果库非常大,如何进行管理呢?
将每一个dart文件作为库文件,使用export关键字在某个库文件中单独导入

mathUtils.dart文件

int sum(int num1, int num2) {
  return num1 + num2;
}

dateUtils.dart文件

String dateFormat(DateTime date) {
  return "2020-12-12";
}

utils.dart文件

library utils; 

export "mathUtils.dart";
export "dateUtils.dart";

test_libary.dart文件

import "lib/utils.dart";

main(List<String> args) {
  print(sum(10, 20));
  print(dateFormat(DateTime.now()));
}

4.3 导入第三方库

1. 在dart公开库网站上搜索需要导入的库
2. 在pubspec.yaml 中 将相关库信息添加进去
name: mylib
description: a dart library.
environment:
  sdk: ">=2.16.1 <3.0.0"
dependencies:
  http: ^0.13.4 // 三方库 http
  html: ^0.15.0 // 三方库 http
3. 拉取三方库
  • cd 到pubspec.yaml的目录
  • 执行dart pub get 指令
4. 导入三方库
import 'package:http/http.dart' as http;
import 'package:html/dom.dart' as html;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容