Flutter(四)之搞定Dart(二)

coderwhy發表於2019-09-12

前言一:接下來一段時間我會陸續更新一些列Flutter文字教程

更新進度: 每週至少兩篇;

更新地點: 首發於公眾號,第二天更新於掘金、思否等地方;

更多交流: 可以新增我的微信 372623326,關注我的微博:coderwhy

希望大家可以 幫忙轉發,點選在看,給我更多的創作動力。

一. 運算子

這裡,我只列出來相對其他語言比較特殊的運算子,因為某些運算子太簡單了,不浪費時間,比如+、-、+=、==。

你可能會疑惑,Dart為什麼要搞出這麼多特殊的運算子呢?

你要堅信一點:所有這些特殊的運算子都是為了讓我們在開發中可以更加方便的操作,而不是讓我們的編碼變得更加複雜。

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

我們來看一下除法、整除、取模運算

var num = 7;
print(num / 3); // 除法操作, 結果2.3333..
print(num ~/ 3); // 整除操作, 結果2;
print(num % 3); // 取模操作, 結果1;
複製程式碼

1.2. ??=賦值操作

dart有一個很多語言都不具備的賦值運算子:

  • 當變數為null時,使用後面的內容進行賦值。
  • 當變數有值時,使用自己原來的值。
main(List<String> args) {
  var name1 = 'coderwhy';
  print(name1);
  // var name2 = 'kobe';
  var name2 = null;
  name2 ??= 'james'; 
  print(name2); // 當name2初始化為kobe時,結果為kobe,當初始化為null時,賦值了james
}
複製程式碼

1.3. 條件運算子:

Dart中包含一直比較特殊的條件運算子:expr1 ?? expr2

  • 如果expr1是null,則返回expr2的結果;
  • 如果expr1不是null,直接使用expr1的結果。
var temp = 'why';
var temp = null;
var name = temp ?? 'kobe';
print(name);
複製程式碼

1.4. 級聯語法:..

  • 某些時候,我們希望對一個物件進行連續的操作,這個時候可以使用級聯語法
class Person {
  String name;

  void run() {
    print("${name} is running");
  }

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

  void swim() {
    print("${name} is swimming");
  }
}

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

  final p2 = Person()
              ..name = "why"
              ..run()
              ..eat()
              ..swim();
}
複製程式碼

二. 流程控制

和大部分語言的特性比較相似,這裡就不再詳細贅述,看一下即可。

2.1. if和else

和其他語言用法一樣

這裡有一個注意點:不支援非空即真或者非0即真,必須有明確的bool型別

  • 我們來看下面name為null的判斷

image-20190911085225484

2.2. 迴圈操作

基本的for迴圈

for (var i = 0; i < 5; i++) {
  print(i);
}
複製程式碼

for in遍歷List和Set型別

var names = ['why', 'kobe', 'curry'];
for (var name in names) {
  print(name);
}
複製程式碼

while和do-while和其他語言一致

break和continue用法也是一致

2.3. switch-case

普通的switch使用

  • 注意:每一個case語句,預設情況下必須以一個break結尾
main(List<String> args) {
  var direction = 'east';
  switch (direction) {
    case 'east':
      print('東面');
      break;
    case 'south':
      print('南面');
      break;
    case 'west':
      print('西面');
      break;
    case 'north':
      print('北面');
      break;
    default:
      print('其他方向');
  }
}
複製程式碼

三. 類和物件

Dart是一個物件導向的語言,物件導向中非常重要的概念就是類,類產生了物件。

這一節,我們就具體來學習類和物件,但是Dart對類進行了很多其他語言沒有的特性,所以,這裡我會花比較長的篇幅來講解。

3.1. 類的定義

在Dart中,定義類用class關鍵字

類通常有兩部分組成:成員(member)和方法(method)。

定義類的虛擬碼如下:

class 類名 {
  型別 成員名;
  返回值型別 方法名(引數列表) {
    方法體
  }
}
複製程式碼

編寫一個簡單的Person類:

  • 這裡有一個注意點: 我們在方法中使用屬性(成員/例項變數)時,並沒有加this
  • Dart的開發風格中,在方法中通常使用屬性時,會省略this,但是有命名衝突時,this不能省略
class Person {
  String name;

  eat() {
    print('$name在吃東西');
  }
}

複製程式碼

我們來使用這個類,建立對應的物件:

  • 注意:從Dart2開始,new關鍵字可以省略。
main(List<String> args) {
  // 1.建立類的物件
  var p = new Person(); // 直接使用Person()也可以建立

  // 2.給物件的屬性賦值
  p.name = 'why';

  // 3.呼叫物件的方法
  p.eat();
}

複製程式碼

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() {
    name = '';
    age = 0;
  }
	// 命名構造方法
  Person.withArgments(String name, int age) {
    this.name = name;
    this.age = age;
  }

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

// 建立物件
var p1 = new Person();
print(p1);
var p2 = new Person.withArgments('why', 18);
print(p2);

複製程式碼

在之後的開發中, 我們也可以利用命名構造方法,提供更加便捷的建立物件方式:

  • 比如開發中,我們需要經常將一個Map轉成物件,可以提供如下的構造方法
  // 新的構造方法
	Person.fromMap(Map<String, Object> map) {
    this.name = map['name'];
    this.age = map['age'];
  }

	// 通過上面的構造方法建立物件
  var p3 = new Person.fromMap({'name': 'kobe', 'age': 30});
  print(p3);

複製程式碼

3.2.3. 初始化列表

我們來重新定義一個類Point, 傳入x/y,可以得到它們的距離distance:

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

  // 錯誤寫法
  // Point(this.x, this.y) {
  //   distance = sqrt(x * x + y * y);
  // }

  // 正確的寫法
  Point(this.x, this.y) : distance = sqrt(x * x + y * y);
}

複製程式碼

上面這種初始化變數的方法, 我們稱之為初始化列表(Initializer list)

3.2.4. 重定向構造方法

在某些情況下, 我們希望在一個構造方法中去呼叫另外一個構造方法, 這個時候可以使用重定向構造方法

  • 在一個建構函式中,去呼叫另外一個建構函式(注意:是在冒號後面使用this呼叫)
class Person {
  String name;
  int age;

  Person(this.name, this.age);
  Person.fromName(String name) : this(name, 0);
}

複製程式碼

3.2.5. 常量構造方法

在某些情況下,傳入相同值時,我們希望返回同一個物件,這個時候,可以使用常量構造方法.

預設情況下,建立物件時,即使傳入相同的引數,建立出來的也不是同一個物件,看下面程式碼:

  • 這裡我們使用identical(物件1, 物件2)函式來判斷兩個物件是否是同一個物件:
main(List<String> args) {
  var p1 = Person('why');
  var p2 = Person('why');
  print(identical(p1, p2)); // false
}

class Person {
  String name;

  Person(this.name);
}

複製程式碼

但是, 如果將構造方法前加const進行修飾,那麼可以保證同一個引數,建立出來的物件是相同的

  • 這樣的構造方法就稱之為常量構造方法
main(List<String> args) {
  var p1 = const Person('why');
  var p2 = const Person('why');
  print(identical(p1, p2)); // true
}

class Person {
  final String name;

  const Person(this.name);
}

複製程式碼

常量構造方法有一些注意點:

  • 注意一:擁有常量構造方法的類中,所有的成員變數必須是final修飾的.
  • 注意二: 為了可以通過常量構造方法,建立出相同的物件,不再使用 new關鍵字,而是使用const關鍵字
    • 如果是將結果賦值給const修飾的識別符號時,const可以省略.

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("黃色");
  d.setColor = "黑色";
  print(d.getColor);
}

class Dog {
  String color;

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

  Dog(this.color);
}

複製程式碼

3.4. 類的繼承

物件導向的其中一大特性就是繼承,繼承不僅僅可以減少我們的程式碼量,也是多型的使用前提

Dart中的繼承使用extends關鍵字,子類中使用super來訪問父類。

父類中的所有成員變數和方法都會被繼承,,但是構造方法除外。

main(List<String> args) {
  var p = new Person();
  p.age = 18;
  p.run();
  print(p.age);
}

class Animal {
  int age;

  run() {
    print('在奔跑ing');
  }
}

class Person extends Animal {

}

複製程式碼

子類可以擁有自己的成員變數, 並且可以對父類的方法進行重寫

class Person extends Animal {
  String name;

  @override
  run() {
    print('$name在奔跑ing');
  }
}

複製程式碼

子類中可以呼叫父類的構造方法,對某些屬性進行初始化:

  • 子類的構造方法在執行前,將隱含呼叫父類的無參預設構造方法(沒有引數且與類同名的構造方法)。
  • 如果父類沒有無參預設構造方法,則子類的構造方法必須在初始化列表中通過super顯式呼叫父類的某個構造方法。
class Animal {
  int age;

  Animal(this.age);

  run() {
    print('在奔跑ing');
  }
}

class Person extends Animal {
  String name;

  Person(String name, int age) : name=name, super(age);

  @override
  run() {
    print('$name在奔跑ing');
  }

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

複製程式碼

3.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;
  }
}

複製程式碼

注意事項:

  • **注意一:**抽象類不能例項化.
  • **注意二:**抽象類中的抽象方法必須被子類實現, 抽象類中的已經被實現方法, 可以不被子類重寫.

3.6. 隱式介面

Dart中的介面比較特殊, 沒有一個專門的關鍵字來宣告介面.

預設情況下,定義的每個類都相當於預設也宣告瞭一個介面,可以由其他的類來實現(因為Dart不支援多繼承)

在開發中,我們通常將用於給別人實現的類宣告為抽象類:

abstract class Runner {
  run();
}

abstract class Flyer {
  fly();
}

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

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

複製程式碼

3.7. Mixin混入

在通過implements實現某個類時,類中所有的方法都必須被重新實現(無論這個類原來是否已經實現過該方法)。

但是某些情況下,一個類可能希望直接複用之前類的原有實現方案,怎麼做呢?

  • 使用繼承嗎?但是Dart只支援單繼承,那麼意味著你只能複用一個類的實現。

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

  • 除了可以通過class定義類之外,也可以通過mixin關鍵字來定義一個類。
  • 只是通過mixin定義的類用於被其他類混入使用,通過with關鍵字來進行混入。
main(List<String> args) {
  var superMan = SuperMain();
  superMan.run();
  superMan.fly();
}

mixin Runner {
  run() {
    print('在奔跑');
  }
}

mixin Flyer {
  fly() {
    print('在飛翔');
  }
}

// implements的方式要求必須對其中的方法進行重新實現
// class SuperMan implements Runner, Flyer {}

class SuperMain with Runner, Flyer {

}

複製程式碼

3.8. 類成員和方法

前面我們在類中定義的成員和方法都屬於物件級別的, 在開發中, 我們有時候也需要定義類級別的成員和方法

在Dart中我們使用static關鍵字來定義:

main(List<String> args) {
  var stu = Student();
  stu.name = 'why';
  stu.sno = 110;
  stu.study();

  Student.time = '早上8點';
  // stu.time = '早上9點'; 錯誤做法, 例項物件不能訪問類成員
  Student.attendClass();
  // stu.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(Colors.red);
}

enum Colors {
  red,
  green,
  blue
}

複製程式碼

3.9.2. 列舉的屬性

列舉型別中有兩個比較常見的屬性:

  • index: 用於表示每個列舉常量的索引, 從0開始.
  • values: 包含每個列舉值的List.
main(List<String> args) {
  print(Colors.red.index);
  print(Colors.green.index);
  print(Colors.blue.index);

  print(Colors.values);
}

enum Colors {
  red,
  green,
  blue
}

複製程式碼

列舉型別的注意事項:

  • 注意一: 您不能子類化、混合或實現列舉。
  • 注意二: 不能顯式例項化一個列舉

四. 泛型

4.1. 為什麼使用泛型?

對於有基礎的同學, 這部分不再解釋

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 infos1 = {1: 'one', 'name': 'why', 'age': 18}; 
  print(infos1.runtimeType); // _InternalLinkedHashMap<Object, Object>

  // 對型別進行顯示
  Map<String, String> infos2 = {'name': 'why', 'age': 18}; // 18不能放在value中
  var infos3 = <String, String>{'name': 'why', 'age': 18}; // 18不能放在value中

複製程式碼

4.3. 類定義的泛型

如果我們需要定義一個類, 用於儲存位置資訊Location, 但是並不確定使用者希望使用的是int型別,還是double型別, 甚至是一個字串, 這個時候如何定義呢?

  • 一種方案是使用Object型別, 但是在之後使用時, 非常不方便
  • 另一種方案就是使用泛型.

Location類的定義: Object方式

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

class Location {
  Object x;
  Object y;

  Location(this.x, this.y);
}

複製程式碼

Location類的定義: 泛型方式

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

  Location l3 = Location<String>('aaa', 'bbb');
  print(l3.x.runtimeType); // String
}
}

class Location<T> {
  T x;
  T y;

  Location(this.x, this.y);
}

複製程式碼

如果我們希望型別只能是num型別, 怎麼做呢?

main(List<String> args) {
  Location l2 = Location<int>(10, 20);
  print(l2.x.runtimeType);
	
  // 錯誤的寫法, 型別必須繼承自num
  Location l3 = Location<String>('aaa', 'bbb');
  print(l3.x.runtimeType);
}

class Location<T extends num> {
  T x;
  T y;

  Location(this.x, this.y);
}

複製程式碼

4.4. 泛型方法的定義

最初,Dart僅僅在類中支援泛型。後來一種稱為泛型方法的新語法允許在方法和函式中使用型別引數。

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

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

複製程式碼

五. 庫的使用

在Dart中,你可以匯入一個庫來使用它所提供的功能。

庫的使用可以使程式碼的重用性得到提高,並且可以更好的組合程式碼。

Dart中任何一個dart檔案都是一個庫,即使你沒有用關鍵字library宣告

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';

複製程式碼

庫檔案中內容的顯示和隱藏

如果希望只匯入庫中某些內容,或者刻意隱藏庫裡面某些內容,可以使用showhide關鍵字

  • **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關鍵字了

不過官方已經不建議使用這種方式了:

image-20190911173722226

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()));
}


複製程式碼

image-20190911173346382

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管理自己的庫自己的庫,在專案開發中個人覺得不是非常有必要,所以暫時不講解這種方式。

備註:所有內容首發於公眾號,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與演算法等等,也會更新一些自己的學習心得等,歡迎大家關注

公眾號

相關文章