Flutter之Dart语法(二)

254 阅读11分钟

一. 运算符

1.1 除法、整除、取模运算

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

1.2 ??= 赋值操作

dart有一个很多语言都不具备的赋值操作符:

  • 当变量为null时,使用后面的内容进行赋值。

  • 当变量有值时,使用自己原来的值。

var num = 7;
var num2 = null;
num2 ??= num;
print(num2);   //7

1.3 条件运算符

Dart中包含一直比较特殊的条件运算符:exp1 ?? exp2

  • 如果exp1是null,则返回exp2的结果。
  • 如果exp1不是null,则返回exp1的结果。
var temp = null;
var name = temp ?? 'kobe';
print(name);  //kobe

1.4级联语法..

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

class Person {
  String? name;
  void run() {
    print('$name can run');
  }

  void eating() {
    print('$name can eat');
  }

  void swim() {
    print('$name can swim');
  }
}

main(List<String> args) {
  final p = Person();
  p.name = 'ooo';
  p.run();
  p.eating();
  p.swim();

  final p2 = Person()
    ..name = 'hhh'
    ..run()
    ..eating()
    ..swim();
}
//注意末尾才能带;分号

二. 流出控制

2.1 if和else

和其他语言用法一样

这里有一个注意点,不支持非空即真或者非0即真,必须有明确的bool类型。

final a = null;
if (a) {
    print(a);
}

image.png

2.2 循环操作

基本的for操作

for(var i = 0;i < 5; i++){
    print(i);
}

for in 遍历List和Set类型

var numbers = [111, 222, 333];
for (var item in numbers) {
    print(item);
}

while和do while 和其他语言一致

break和continue用法也是一致

2.3 switch case

普通的switch使用

  • 注意:每一个case语句,默认情况下必须以一个break结尾
var fruit = '西瓜';
switch (fruit) {
    case '橙子':
      print('i like eat $fruit');
      break;
    case '西瓜':
      print('i like eat $fruit');
      break;
    default:
      print('i like banner');
}

三. 类和对象

3.1 类的定义

在Dart中,定义类使用 class 关键字

类通常有两部分组成:成员(member)和方法(method)。

定义类的伪代码如下:

class 类名{
    类型 成员名;
    返回值类型 方法名(参数列表){
        方法体;
    }
}

编写一个简单的Person类:

  • 这里有一个注意点:我们在方法中使用属性(成员/实例变量)时,并没有加this;
  • Dart的开发风格中,在方法中使用属性时,会省略this,但是有命名冲突时,this不能省略。
class Person{
    String? name;
    eating(){
        print('$name 在吃东西');
    }
}

我们来使用这个类,创建对应的对象:

  • 注意:从Dart2开始,new关键字可以省略;
main(List<String> args) {
  //1.创建类的对象
  var p = new Person();   //可以省略new
  
  //2.给对象的属性赋值
  p.name = 'oooo';
  
  //3.调用对象的方法
  p.eating();
}

3.2 构造方法

3.2.1 普通构造方法

我们知道,当通过类创建一个对象时,会调用这个类的构造方法。

  • 当类中没有明确指定构造方法时,将默认拥有一个 无参的构造方法;
  • 前面的Person中,我们就是在调用这个构造方法。

我们也可以根据自己的需求,定义自己的构造方法:

  • 注意一:当有了自己的构造方法时,默认的构造方法将失效,不能使用;
    • 当然,你可能希望明确的写一个默认的构造方法,但是会和我们自定义的构造方法冲突;
    • 这是因为Dart本身不支持函数的重载(名称相同,参数不同的方式)。
  • 注意二:这里还实现了toString方法
class Person {
  String? name;
  int? age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  @override
  String toString() {
    return 'name=$name age=$age';
  }
}

另外,在实现构造方法时,通常做的事情就是通过参数 给 属性赋值

为了简化这一过程,Dart提供了语法糖形式:

Person(String name, int age) {
    this.name = name;
    this.age = age;
}
//等同于
Person(this.name,this.age);

3.2.2 命名构造方法

但是在开发中,我们确实希望实现更多的构造方法,怎么办呢?

  • 因为不支持方法(函数)的重载,所以我们没办法创建相同名称的构造方法。

我们需要使用命名构造方法:

class Person {
  String? name;
  int? age;

  Person(this.name,this.age);
  Person.withArgments(this.name, this.age);
  @override
  String toString() {
    // return super.toString();
    return 'name=$name age=$age';
  }
}

main(List<String> args) {
  var p = new Person();
  print(p);  //name=00000 age=18

  var p2 = new Person.withArgments('ooooo', 20);
  print(p2);   //name=ooooo age=20
}

在之后的开发中,我们也可以利用命名构造方法,提供更加便捷的创建对象的方式:

  • 比如开发中,我们需要经常将一个Map转成对象,可以提供如下的构造方法
class Person {
  String? name;
  int? age;
  Map? map;
  Person.fromMap(Map<String, dynamic> map) {
    this.name = map['name'];
    this.age = map['age'];
    this.map = map;
  }
  @override
  String toString() {
    return '$name,$age,$map';
  }
}


main(List<String> args) {
  Map<String, Object> map1 = {'name': 'ooo', 'age': 19};
  var p = Person.fromMap(map1);
  print(p);
}

3.2.3 初始化列表

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

import 'dart:math';

class Point {
  final num x;
  final num y;
  final num distance;

  // 错误写法
  // Point(this.x, this.y) {
  //   distance = sqrt(x * x + y * y);   //distance必须在初始化的时候赋值
  // }

  // 正确的写法
  Point(this.x, this.y) : distance = sqrt(x * x + y * y);
  @override
  String toString() {
    return '$distance';
  }
}

main(List<String> args) {
  var p = Point(10, 20);
  print(p);   //22.360679774997898
}

上面这种初始化变量的方法,我们称之为 初始化列表(Initializer list)

3.2.4 重定向构造方法

在某些情况下,我们希望在一个构造方法中去调用另外一个构造方法,这个时候可以使用重定向构造方法:

  • 在一个构造函数中,去调用另外一个构造函数(注意:是在冒号后面使用this调用)
main(List<String> args) {
  var p = Person.fromName('ooo');
  print(p);   //ooo,100
}

class Person {
  String? name;
  int? age;
  Person(this.name, this.age);
  Person.fromName(String name) : this(name, 100);

  @override
  String toString() {
    return '$name,$age';
  }
}

3.2.5 常量构造方法

在某些情况下,传入相同值时,我们希望返回同一个对象,这个时候,我们使用常量构造方法。

默认情况下,创建对象时,即使传入相同的参数,创建出来的也不是同一个对象,看下面的代码:

  • 这里我们使用 identical(对象1,对象2)函数来判断两个对象是否他一个对象:
main(List<String> args) {
  var p1 = Person('ooo');
  var p2 = Person('ooo');

  print(identical(p1, p2));  //false
}

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

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

main(List<String> args) {
  var p1 = const Person('ooo');
  var p2 = const Person('ooo');

  print(identical(p1, p2)); //true
}

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

常量构造方法有一些注意点:

  • 拥有常量构造方法的类中,所有的成员变量必须时final修饰的。
  • 为了可以通过常量构造方法,创建出相同的对象,不在使用new关键字,而是使用const关键字
    • 如果是将结果赋值给const修饰的标识符,const可以省略。 如:
main(List<String> args) {
    const p1 = Person('ooo'); //赋值给p1常量,Person类可以省略const
    const p2 = Person('ooo');
    print(identical(p1, p2)); //true
}

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

3.2.6 工厂构造方法

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

main(List<String> args) {
  var p1 = Person('why');
  var p2 = Person('why');
  print(identical(p1, p2)); // true
}

class Person {
    String name;
    static final Map<String, Person> _cache = <String, Person>{};

    factory Person(String name) {
        if (_cache.containsKey(name)) {
          return _cache[name];
        } else {
          final p = Person._internal(name);
          _cache[name] = p;
          return p;
        }
    }
    Person._internal(this.name);
}

3.3 setter和getter

默认情况下。Dart中类定义的属性是可以直接被外界访问的。

但是某些情况下,我们希望监控这个类的属性被访问的过程,这个时候就可以使用setter和getter了。

main(List<String> args) {
  final d = Dog('yellow');
  d.setColor = 'red';
  print(d.color);
}

class Dog {
  String color;
  String get getColor {
    return color;
  }
  
  set setColor(String color) {
    this.color = color;
  }
  Dog(this.color);
}

也可以改成箭头函数

main(List<String> args) {
  final d = Dog('yellow');
  d.setColor = 'red';
  print(d.color);
}

class Dog {
  String color;
  get getColor => color;
  set setColor(String color) => this.color = color;

  Dog(this.color);
}

3.4 类的继承

面向对象的其中一大特性就是继承,继承不仅仅可以减少我们的代码量,也是多态的使用前提。

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

父类中的所有成员变量和方法都会被继承,但是构造方法除外。

子类可以拥有自己的成员变量,并且可以对父类的方法进行重写

main(List<String> args) {
  final d = Dog('小狗', 20);
  print(d); //小狗,20岁

  d.run("小狗");
}

class Dog extends Animal {
  // 继承至父类Animal的变量和方法
  Dog(String name, int age) : super(age, name);
  @override
  void run(name) {
    super.run(name);   //调用父类run方法
    print('$name在跑,$name$age岁啦');  //小狗在跑,小狗有20岁啦
  }

  @override
  String toString() {
    return '$name,$age岁';
  }
}

class Animal {
  int age;
  String name;
  void run(name) {
    print('$name在跑');   //小狗在跑
  }

  Animal(this.age, this.name);
}

子类中可以调用父类的构造方法,对某些属性进行初始化:

  • 子类对构造方法在执行前,将隐含调用父类的无参默认构造方法(没有参数且与类同名的构造方法)。
  • 如果父类没有无参默认构造方法,则子类的构造方法必须在初始化中通过super显示调用父类的某个构造方法。
main(List<String> args) {
  final d = Dog('花花', 2);
  print(d);   // age=2
  d.run();   // 在蹦跑中...
}

class Dog extends Animal {
  String name;
  Dog(name, age) : name = name, super(age);
}

class Animal {
  int age;
  Animal(this.age);

  run() {
    print('在蹦跑中...');
  }

  @override
  String toString() {
    return 'age=$age';
  }
}

3.5 抽象类

我们知道。继承是多态使用的前提。

所以在定义很多通用的调用接口时,我们通常会让调用者传入父类,通过多态来实现更加灵活的调用方式。

但是,父类本身可能并不需要对某些方法进行具体的实现,所以父类中定义的方法,我们可以定义为抽象方法。

什么事抽象方法?在Dart中没有具体实现的方法(没有方法体),就是抽象方法。

  • 抽象方法,必须存在于抽象类中。
  • 抽象类是使用abstract声明的类。

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

main(List<String> args) {
  final s = Circle(10.2);
  final r = Reactangle(10, 20);
  print(s.getArea()); //326.68559999999997
  print(r.getArea()); //200.0

  print(s);   //1111
  print(r);   //1111
}

//Shape 抽象类
abstract class Shape { 
  getArea(); //抽象方法,这里没有实现具体的方法,即没有实现体,必须被子类实现
  
  @override
  String toString() {
    return '1111';
  }
}

class Circle extends Shape {
  double r;
  Circle(this.r);

  @override   //重写父类Shape的getArea方法
  getArea() {
    return r * r * 3.14;
  }
}

class Reactangle extends Shape {
  double w;
  double h;
  Reactangle(this.w, this.h);
  @override   //重写父类Shape的getArea方法
  getArea() {
    return w * h;
  }
}

注意事项:

  • 抽象类不能实例化
  • 抽象类中的抽象方法必须被子类实现,抽象类中已经被现实的方法,可以不被子类重写
main(List<String> args) {
  final s = Circle(10.2);
  final r = Reactangle(10, 20);

  //子类中没有重写该方法,去父类中查找
  print(s.getArea()); //被实现了
  print(r.getArea()); //被实现了
}

abstract class Shape {
  //Shape 抽象类
  getArea() {
    return '被实现了'; //实现了方法体
  }

  void run() {
    print('object');
  }

  @override
  String toString() {
    return '1111';
  }
}

class Circle extends Shape {
  double r;
  Circle(this.r);

  //  抽象类Shape中的已经实现getArea方法, 可以不被子类重写.
  // @override
  // getArea() {
  //   return r * r * 3.14;
  // }
}

class Reactangle extends Shape {
  double w;
  double h;
  Reactangle(this.w, this.h);
  //  抽象类Shape中的已经实现getArea方法, 可以不被子类重写.
  // @override
  // getArea() {
  //   return w * h;
  // }
}

3.6 隐式接口

Dart中的接口比较特殊,没有一个专门的关键字来声明接口

默认情况下,定义的每个类都相当于默认也声明了一个接口,可以由其他的类来实现(因为Dart不支持多继承)

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

main(List<String> args) {
  final s = Superman();
  print(s);
  s.run(); //超人在跑
  s.fly(); //超人在飞
}

abstract class Runner {
  run();
}

abstract class Flyer {
  fly();
}

class Superman implements Runner, Flyer {
  @override
  run() {
    print('超人在跑');
  }

  @override
  fly() {
    print('超人在飞');
  }

  @override
  String toString() {
    return 'hahhaha';
  }
}

3.7 Mixin混入

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

但是某些情况下,一个类可能希望直接复用之前类的原有实现方案。怎么做呢?

  • 使用继承?但是Dart只支持单继承,那么意味着你只能复用一个类的实现。

Dart提供了另外一种方案:Mixin混入的方式

  • 除了可以通过class定义类之外,也可以通过mixin关键字来定义一个类

  • 只是通过mixin定义的类用于被其他类混入使用,通过with关键字来进行混入

main(List<String> args) {
  final s = SuperMan();
  s.run();  //runer
  s.fly();  //fly
}

mixin Runner {
  run() {
    print('runer');
  }
}

mixin Flying {
  fly() {
    print('fly');
  }
}

//implements的方式要求必须对其中的方法进行重新实现
// class SuperMan implements Runner, Flying {
//   @override
//   run() {}

//   @override
//   fly() {}
// }

class SuperMan with Runner, Flying {}

3.8 类成员和方法

前面我们在类中定义的成员和方法都属于对象级别的,在开发中,我们有时候也需要定义类级别的成员和方法

在Dart中我们使用static关键字来定义:

main(List<String> args) {
  var stu = Student();
  stu.name = 'oooo';   //对象成员
  stu.sno = 18;
  stu.study();    //对象方法

  Student.time = '早上8点';   //调用类成员
  Student.attendClass();    //调用类方法
}

class Student {
  String? name;
  int? sno;
  static String? time;

  study() {
    print('$name在学习');
  }

  static attendClass() {
    print('去上课');
  }
}

3.9 枚举类型

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

3.9.1 枚举的定义

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

main(List<String> args) {
  print(Color.values); //[Color.red, Color.yellow, Color.green]
  print(Color.red); //Color.red
  print(Color.red.index);  //0
}

enum Color { red, yellow, green }

3.9.2 枚举的属性

枚举类型中有两个比较常见的属性:

  • index:用于表示每个枚举常量的索引。从0开始
  • values:包含每个枚举值的List 枚举类型的注意事项:
  • 不能子类化,混合或实现枚举
  • 不能显式实例化一个枚举

四. 泛型

4.1 为什么使用泛型?

出现原因:泛型是在jdk1.5新增的技术,其是为解决代码通用性而出现的。

例如:

假设我现在要处理int、String两个类型的数据,其处理的逻辑是一模一样,即除了类型不同,其他完全一样。

简单的做法即:分别写出int对应的处理代码,string 对应的处理代码,但是这个解决方案很蠢,那有没有更好的解决方案呢?

  • 使用object声明类型,因为object是所有类的父类,因此 可以接受所有的类型;使用该方法确实能解决上述的问题,但是带来的问题是类型不安全的问题:假设我们现在有一个方法function(List a),这里的a,我们希望是int类型,float类型,但是它同样可以接受String类型,因此会出现类型不安全的问题。那如何解决呢,这就是泛型出现的原因。

  • 使用泛型解决:list,先指定T的类型,指定为int,则后面的所有数据都为int型,指定为float,则都为float类型,则不会出现我们没出现的类型,达到类型安全。另外,指定类型以后不需要强制转换,性能更高

  • 相比object实现,泛型有两大优势:类型安全、不需要强制转换。

  • 4.2 List和Map的泛型

    List使用时的泛型写法:

    // 创建List的方式
      var names1 = ['why', 'kobe', 'james', 111];
      print(names1.runtimeType); // List<Object>
    
      // 限制类型
      var names2 = <String>['why', 'kobe', 'james', 111]; // 最后一个报错
      List<String> names3 = ['why', 'kobe', 'james', 111]; // 最后一个报错
    

    Map使用时的泛型写法:

    // 创建Map的方式
      var names1 = {'name': 'ooo', 'age': 19};
      print(names1.runtimeType); //_InternalLinkedHashMap<String, Object>
    
      // 限制类型
      var names2 = <String, String>{'name': 'ooo', 'age': 19}; // 最后一个报错
      Map<String, String> names3 = {'name': 'ooo', 'age': 19}; // 最后一个报错
    

    4.3类定义的泛型

    如果我们需要定义一个类,用于存储位置信息Location,但是并不确定使用者希望使用的是int类型,还是double类型,甚至是一个字符串,这个时候如何定义呢?

    • 一种方案是使用Object类型,但是在之后使用时,非常不方便
    • 另一种方案就是使用泛型 Location类的定义:Object方式
    main(List<String> args) {
      Location l1 = Location(10, 10);
      print(l1.x.runtimeType);
    }
    
    class Location {
      Object? x;
      Object? y;
      Location(this.x, this.y);
    }
    

    Location类的定义:泛型方式

    main(List<String> args) {
      Location l1 = Location<int>(10, 10);
      print(l1.x.runtimeType);   //int
    
      Location l2 = Location<String>('10', '20');
      print(l2.x.runtimeType);   //String
    }
    
    class Location<T> {
      T? x;
      T? y;
      Location(this.x, this.y);
    }
    

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

    main(List<String> args) {
      Location l1 = Location<int>(10, 10);
      print(l1.x.runtimeType); //int
    
      Location l2 = Location<String>('10', '20'); //String报错
    }
    
    class Location<T extends num> {
      T? x;
      T? y;
      Location(this.x, this.y);
    }
    

    4.4 泛型方法的定义

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

    main(List<String> args) {
      var names = ['ooo', 'yyyy'];
      var getName = get(names);
      print(names); //[ooo, yyyy]
      print("$getName,${getName.runtimeType}"); //ooo,String
    
      print(args);  //[]
    }
    
    T get<T>(ts) {
      print(T); //dynamic
      return ts[0];
    }
    

    五. 库的使用

    在Dart中,你可以导入一个库来使用它所提供的功能。 库的使用可以使代码的重用性得到提高,并且可以更好的组合代码。

    5.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();
    

    5.2 库的定义

    • library关键字 通常在定义库时,我们可以使用library关键字给库起一个名字。

    但目前我发现,库的名字并不影响导入,因为import语句用的是字符串URI

    library math;
    
    • part关键字

    在之前我们使用student.dart作为演练的时候,只是将该文件作为一个库。

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

    不过官方已经不建议使用这种方式了

    • 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()));
    }
    复制代码
    

    最后,也可以通过Pub管理自己的库自己的库,在项目开发中个人觉得不是非常有必要,所以暂时不讲解这种方式。