給 Android 開發者的 Dart 語言基礎

本文基於官方的文檔,整理出 Dart 語言中與 Java 和 Kotlin 語言類似和特有的部分,因此本文的讀者需要具備一定的 Java 及 Kotlin 語言基礎,相信大家讀完本文就可以看懂大部分的 Flutter 代碼了。

在學習 Dart 語言時,我們可以使用 DartPad 來編寫和調試 Dart 的代碼,體驗 Dart 的大部分語言功能。

1. 概述

Dart 語言同時借鑑了 JavaJavaScript,它在靜態語法方面和 Java 非常相似,如類型定義、函數聲明、泛型等等,而在動態特性方面又和 JavaScript 很像,如函數式特性、異步支持等等。除了融合 Java 和 JavaScript 語言的優勢之外,Dart 也具有一些其它具有表現力的語法,如可選命名參數、(級聯運算符)和 ?.(條件成員訪問運算符)以及 ??(判空賦值運算符)等等。

2. 變量

2.1 變量定義

在創建變量時,我們可以使用常規的類型定義:

String name = 'Bob'

也可以使用 var 關鍵字進行創建:

var name = 'Bob'

上面示例中 name 變量的類型會被推斷爲 String,如果想不侷限於單一的類型,可以指定爲 dynamic 類型,如下:

dynamic name = 'Bob';
name = 1

未初始化的變量都會默認初始化爲 null,即使是數字變量。

2.2 final 和 const

Dart 中沒有 Kotlin 的 val 關鍵字,但是有 finalconst,兩者的區別是 final 變量在其第一次使用的時候才被初始化,而 const 變量是一個編譯時的常量,且必須在聲明變量時賦值。示例代碼:

final name = 'Bob'; // final 可以直接替代 var 關鍵字
final String nickname = 'Bobby'; // 也可以加在一個具體的類型前
const bar = 1000000; // 直接賦值
const double atm = 1.01325 * bar; // 可以利用其它 const 變量賦值

另外還有一個區別是 final 修飾的變量不能修改引用的對象,而 const 修飾的變量不僅不能修改引用的對象,甚至不能修改對象的屬性,即它是一個不可變的對象。示例代碼:

class Person {
  var name;

  Person(this.name);
}

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final num x, y;

  const ImmutablePoint(this.x, this.y);
}

void main() {
  final person = Person('Bob');
  person.name = 'Bobby'; // 沒有問題
  const point = ImmutablePoint(1, 1);
  point.x = 2; // x 不能被重新賦值
}

通過上面的例子可以看到,你可以將構造函數聲明爲 const 的,這種類型的構造函數創建的對象代表的是一個不可改變的對象。如果你在構造函數前加上了 const,那麼要確保所有屬性均爲 finalconst 的,例如上面代碼中的 ImmutablePoint 類。

當一個變量被 const 修飾時,這個變量引用的類的構造函數一定是 const 的,即這個類是不可變的。

另外,const 關鍵字不僅可以放在等號左邊用來定義常量,還可以放在等號右邊用來定義常量值,該常量值可以賦給任何變量。示例代碼:

var point = const ImmutablePoint(1, 1);
point = ImmutablePoint(2, 2);

上面的代碼是沒有問題的,不過這種定義方式通常用於修飾集合:

var array = [1, 2, 3];
array[1] = 10; // 沒有問題
var constArray = const [1, 2, 3];
constArray[1] = 10; // 不能修改數組內的元素
const baz = []; // 等同於 `const baz = const []`

如上面代碼所示,如果集合前加上 const 關鍵字,代表它是一個不可變的集合。

在等號的兩邊也可以同時使用 const 關鍵字,但是通常可以省略等號右邊的,從 Dart 2 開始可以根據上下文推斷出來:

const point = const ImmutablePoint(1, 1); // 等同於 `const point = ImmutablePoint(1, 1);`

最後,如果使用 const 修飾類中的成員變量,則必須加上 static 關鍵字,即 static const,例如 ImmutablePoint 類中的 origin 變量。

3. 內置類型

3.1 數字類型

Dart 支持 intdoublenumintdouble 都是 num 的子類。下面是字符串和數字之間轉換的方式:

// String -> int
var one = int.parse('1');
// String -> double
var onePointOne = double.parse('1.1');
// int -> String
String oneAsString = 1.toString();
// double -> String
String piAsString = 3.14159.toStringAsFixed(2); // 保留 2 位小數,輸出 3.14

3.2 字符串

可以使用單引號或者雙引號來創建字符串:

var s1 = '使用單引號創建字符串字面量。';
var s2 = "雙引號也可以用於創建字符串字面量。";
var s3 = '使用單引號創建字符串時可以使用斜槓來轉義那些與單引號衝突的字符串:\'。';
var s4 = "而在雙引號中則不需要使用轉義與單引號衝突的字符串:'";

可以在字符串中以 ${表達式} 的形式使用表達式,使用方式和 Kotlin 是一樣的,這裏不再詳述。

也可以使用 + 運算符將兩個字符串連接爲一個,也可以將多個字符串挨着放一起變爲一個:

var s1 = '可以拼接'
    '字符串'
    "即使它們不在同一行。";

可以使用三個單引號或者三個雙引號創建多行字符串:

var s1 = '''
你可以像這樣創建多行字符串。
''';

var s2 = """這也是一個多行字符串。""";

在字符串前加上 r 作爲前綴可以創建原生字符串,即不會被做任何處理(比如轉義)的字符串:

var s = r'在原生字符串中,轉義字符串 \n 會直接輸出 “\n” 而不是轉義爲換行。'

3.3 布爾類型

Dart 使用 bool 關鍵字表示布爾類型。

3.4 List

在 Dart 中數組由 List 對象表示,List 字面量與 JavaScript 中數組字面量是一樣的,如下:

var list = [1, 2, 3];

你可以像 Java 中數組的用法來操縱 Dart 中 List 的元素。

如果想要創建一個編譯時常量的 List,在 List 字面量前添加 const 關鍵字即可,如 2.2 節所述,示例代碼:

var constantList = const [1, 2, 3];
constantList[1] = 1; // 會報錯,不能修改數組

Dart 在 2.3 引入了 擴展操作符 ...可空的擴展操作符 ...?,它們提供了一種將多個元素插入集合的簡潔方法。

例如,你可以使用擴展操作符 ... 將一個 List 中的所有元素插入到另一個 List 中:

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

如果擴展操作符右邊可能爲 null ,你可以使用可空的擴展操作符 ...? 來避免產生異常:

var list;
var list2 = [0, ...?list]; // list2 中只有一個元素 0

Dart 在 2.3 還引入了 Collection IfCollection For,在構建集合時,可以使用條件判斷和循環,示例代碼:

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

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

3.5 Set

下面是使用 Set 字面量來創建一個 Set 集合的方法:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

Set 字面量與 List 字面量的區別是:Set 字面量用花括號 {} 表示,而 List 字面量用方括號 [] 表示。

可以在 {} 前加上類型參數創建一個空的 Set,或者將 {} 賦值給一個 Set 類型的變量:

var names = <String>{}; // 類型 + {} 的形式創建 Set
Set<String> names = {}; // 聲明類型變量的形式創建 Set

可以在 Set 字面量前添加 const 關鍵字創建一個 Set 編譯時常量:

final constantSet = const {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
constantSet.add('helium'); // 會報錯,不能修改集合

從 Dart 2.3 開始,Set 也可以像 List 一樣支持使用擴展操作符(......?)以及 Collection IfCollection For 操作。

3.6 Map

Dart 中使用 Map 字面量來創建 Map:

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

你也可以使用 Map 的構造器來創建 Map:

var nobleGases = Map();
// 添加鍵值對
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

可以在 {} 前加上類型參數創建一個空的 Map,或者將 {} 賦值給一個 Map 類型的變量:

var nobleGases = <int, String>{}; // 類型 + {} 的形式創建 Map
Map<int, String> nobleGases = {}; // 聲明類型變量的形式創建 Map

下面有一個問題,如下面代碼所示,如果我忘記了加類型參數,該變量是 Set 還是 Map 呢?

var nobleGases = {};

答案其實是 Map。因爲先有的 Map 字面量語法,所以 {} 默認是 Map 類型。如果忘記在 {} 上註釋類型或賦值到一個未聲明類型的變量上,Dart 會創建一個類型爲 Map<dynamic, dynamic> 的對象。

同 List,在一個 Map 字面量前添加 const 關鍵字可以創建一個 Map 編譯時常量:

final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};
constantMap[2] = 'Helium'; // 會報錯,不能修改映射

從 Dart 2.3 開始,Map 可以像 List 一樣支持使用擴展操作符(......?)以及 Collection IfCollection For 操作。

4. 函數

Dart 是一種真正面向對象的語言,所以函數也是一個對象並且類型爲 Function。下面是定義一個函數的例子:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

如果函數體內只包含一個表達式,可以使用簡寫語法:

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

語法 => 表達式{ return 表達式; } 的簡寫, => 也稱之爲胖箭頭語法

注意,在 =>; 之間的只能是表達式,比如你不能將一個 if 語句放在裏面,但是可以放置條件表達式。

函數可以有兩種形式的參數:必要參數可選參數。必要參數定義在參數列表的前面,可選參數則定義在必要參數的後面。可選參數可以是 命名的位置的。下面來詳述一下。

4.1 可選參數

可選參數分爲命名參數位置參數,可在參數列表中任選其一使用,但兩者不能同時出現在參數列表中。

4.1.1 命名參數

定義函數時,可以使用 {param1, param2, …} 來指定命名參數:

void enableFlags({bool bold, bool hidden}) {...}

當你調用該函數時,需要使用 參數名: 參數值 的形式來指定命名參數:

enableFlags(bold: true, hidden: false);

雖然命名參數是可選參數的一種類型,但是你仍然可以使用 @required 註解來標識一個命名參數是必需的參數,此時調用者則必須爲該參數提供一個值。例如:

void enableFlags({bool bold, @required bool hidden}) {...}

命名參數在 Flutter 中的應用還是蠻多的。

4.1.2 位置參數

可以使用 [] 將一系列參數包裹起來作爲位置參數:

String say(String from, String msg, [String device]) {...}
say('Bob', 'Howdy') // 不使用可選參數調用上述函數
say('Bob', 'Howdy', 'smoke signal') // 使用可選參數調用上述函數

4.1.3 默認參數值

可以用 = 爲函數的命名參數和位置參數定義默認值,沒有指定默認值的情況下默認值爲 null。示例代碼:

void enableFlags({bool bold = false, bool hidden = false}) {...}

List 或 Map 同樣也可以作爲默認值,示例代碼:

void doStuff(
    {List<int> list = const [1, 2, 3],
    Map<String, String> gifts = const {
      'first': 'paper',
      'second': 'cotton',
      'third': 'leather'
    }}) {
  print('list:  $list');
  print('gifts: $gifts');
}

4.2 main() 函數

每個 Dart 程序都必須有一個 main() 頂級函數作爲程序的入口,main() 函數返回值爲 void 並且有一個 List<String> 類型的可選參數。

4.3 函數作爲對象

和 Kotlin 一樣,可以將函數作爲參數傳遞給另一個函數。例如:

void printElement(int element) {
  print(element);
}
var list = [1, 2, 3];
// 將 printElement 函數作爲參數傳遞。
list.forEach(printElement);

也可以將函數賦值給一個變量,比如:

var loudify = (msg) => msg.toUpperCase();
print(loudify('hello'));

4.4 匿名函數(Lambda 表達式)

在 Dart 中,匿名函數(或 Lambda 表達式)的格式如下:

([[類型] 參數[, …]]) {
  函數體;
};

下面代碼定義了只有一個參數 item 且沒有參數類型的匿名方法:

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

如果函數體內只有一行語句,可以使用胖箭頭縮寫法:

list.forEach((item) => print('${list.indexOf(item)}: $item'));

4.5 返回值

在 Dart 中,所有的函數都有返回值,最後沒有返回語句的函數最後一行默認爲執行了 return null;

5. 運算符

5.1 算術運算符

Dart 支持的常用的算術運算符與 Java 是一樣的,唯一不同是除號 / 的結果是一個浮點數,而整除用 ~/ 表示,示例代碼:

print(5 / 2); // 輸出 2.5
print(5 ~/ 2); // 輸出 2

5.2 關係運算符

Dart 支持的關係運算符也是與 Java 一樣的,有一個與 Kotlin 類似的是要判斷兩個對象是否表示相同的事物使用 == 即可。如果是自定義的類,需要重寫 == 運算符和 hashCode 值。

而如果需要確定兩個對象是否完全相同,可以使用 identical() 函數。示例代碼如下:

class Person {
  final String firstName, lastName;

  Person(this.firstName, this.lastName);

  @override
  bool operator ==(dynamic other) {
    if (other is! Person) return false;
    Person person = other;
    return person.firstName == firstName && person.lastName == lastName;
  }

  @override
  int get hashCode {
    int result = 17;
    result = 37 * result + firstName.hashCode;
    result = 37 * result + lastName.hashCode;
    return result;
  }
}

void main() {
  Person person1, person2;
  print(person1 == person2); // 兩個都爲 null,也相等
  person1 = Person("Jimmy", "Sun");
  person2 = Person('Jimmy', 'Sun');
  print(person1 == person2); // 輸出 true
  print(identical(person1, person2)); // 輸出 false
}

5.3 類型判斷運算符

與 Kotlin 類似,Dart 使用 asisis! 來判斷對象類型,而且在用 is 判斷之後 Dart 可以推斷該類型。

5.4 賦值運算符

Dart 可以使用 = 來賦值,也可以使用 ??= 來爲值爲 null 的變量賦值:

a = value;
b ??= value; // 當且僅當 b 爲 null 時才賦值

5.5 條件表達式

Dart 中有兩個特殊的運算符可以用來替代 if-else 語句,其中一個是 Java 中的三元表達式:條件 ? 表達式 1 : 表達式 2,另一個是類似於 Kotlin 中的 Elvis 操作符:表達式 1 ?? 表達式 2,如果表達式 1 爲非 null 則返回其值,否則返回表達式 2 的值。

5.6 級聯運算符(…)

類似於 Kotlin 的作用域函數,級聯運算符 .. 可以讓你在同一個對象上連續調用多個對象的變量或方法。

querySelector('#confirm') // 獲取對象
  ..text = 'Confirm' // 使用對象的成員
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));

級聯運算符還可以嵌套,例如:

final addressBook = (AddressBookBuilder()
      ..name = 'jenny'
      ..email = '[email protected]'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();

5.7 條件訪問運算符

Dart 也支持 Kotlin 中的安全調用操作符:?.

6. 流程控制語句

6.1 For 循環

在 Dart 中,除了支持標準的 for 循環,還對 List 和 Set 等實現了 Iterable 接口的類支持 for-in 形式的迭代:

var collection = [0, 1, 2];
for (var x in collection) {
  print(x); // 0 1 2
}

6.2 Switch 和 Case

在 Dart 中,每一個非空的 case 子句都必須有一個 break 語句,也可以通過 continuethrow 或者 return 來結束非空的 case 語句,否則就會報錯。

但是,Dart 也支持空的 case 語句,允許它以 fall-through 的形式執行,示例代碼:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED': // case 語句爲空時的 fall-through 形式。
  case 'NOW_CLOSED':
    executeNowClosed(); // case 條件值爲 CLOSED 和 NOW_CLOSED 時均會執行該語句。
    break;
}

如果想要在非空的 case 語句中實現 fall-through 的形式,可以使用 continue 配合標籤的方式實現:

var command = 'CLOSED';
switch (command) {
  case 'CLOSED':
    executeClosed();
    continue nowClosed; // 繼續執行標籤爲 nowClosed 的 case 子句。
  nowClosed:
  case 'NOW_CLOSED':
    executeNowClosed(); // case 條件值爲 CLOSED 和 NOW_CLOSED 時均會執行該語句。
    break;
}

其它的諸如 if-elsewhiledo-whilebreakcontinue 等流程控制語句和 Java 的用法一樣,這裏不再詳述。

7. 異常

和 Kotlin 一樣,Dart 的所有異常都是非必檢的異常。

7.1 拋出異常

在 Dart 中可以將任何非 null 對象作爲異常拋出而不侷限於 ExceptionError 類,例如:

throw 'Out of llamas!'

7.2 捕獲異常

在 Dart 中使用 oncatch 來捕獲異常,使用 on 來指定異常類型,使用 catch 來捕獲異常對象,兩者可單獨使用,也可同時使用。示例代碼:

try {
  throw Exception('Out of llamas!');
} on Exception { // 單獨使用 on
  print("error");
}

try {
  throw Exception('Out of llamas!');
} catch (e) { // 單獨使用 catch
  print(e);
}

try {
  throw Exception('Out of llamas!');
} on Exception catch (e) { // 同時使用 on 和 catch
  print(e);
}

可以使用關鍵字 rethrow 將捕獲的異常再次拋出:

try {
  throw Exception('Out of llamas!');
} on Exception {
  rethrow;
}

8. 類

8.1 構造函數

8.1.1 成員變量賦值

Dart 提供了一種特殊的語法糖來簡化爲成員變量賦值的過程:

class Point {
  num x, y;

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

8.1.2 命名式構造函數

可以爲一個類聲明多個命名式構造函數來表達更明確的意圖:

class Point {
  num x, y;

  Point(this.x, this.y);

  // 命名式構造函數
  Point.origin() {
    x = 0;
    y = 0;
  }
}

構造函數是不能被繼承的,子類同樣也不能繼承父類的命名式構造函數。

8.1.3 調用父類非默認構造函數

如果父類沒有無參數構造函數,那麼子類必須調用父類的其中一個構造函數,爲子類的構造函數指定一個父類的構造函數只需在構造函數體前使用 : 即可。示例代碼:

class Employee extends Person {
  final Map data;

  Employee(String firstName, String lastName, this.data) : super(firstName, lastName);
}

class Employee extends Person {
  final Map data;

  Employee(String firstName, String lastName, this.data) : super(firstName, lastName) {
    // ...
  }
}

8.1.4 初始化列表

除了調用父類構造函數之外,還可以在構造函數體執行之前初始化實例變量,每個實例變量之間使用逗號分隔。示例代碼:

class Employee extends Person {
  final Map data;

  Employee(String firstName, String lastName)
    : data = {}, super(firstName, lastName) {
    // ...
  }
}

8.1.5 重定向構造函數

有時候類中的構造函數會調用類中其它的構造函數,該重定向構造函數沒有函數體,只需在函數簽名後使用 : 指定需要重定向到的其它構造函數即可:

class Point {
  num x, y;

  // 該類的主構造函數。
  Point(this.x, this.y);

  // 委託實現給主構造函數。
  Point.alongXAxis(num x) : this(x, 0);
}

8.1.6 使用構造函數

從 Dart 2 開始,創建對象的 new 關鍵字是可選的了。

另外,當兩個在構造函數名之前加 const 關鍵字來創建編譯時常量,並且參數值一樣時,這兩個對象是同一個對象:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
print(identical(a, b)); // 輸出 true

8.2 獲取對象的類型

可以使用 Object 對象的 runtimeType 屬性在運行時獲取一個對象的類型,該對象類型是 Type 類的實例:

print('The type of a is ${a.runtimeType}');

8.3 Getter 和 Setter 方法

實例對象的每一個屬性都有一個隱式的 Getter 方法,如果爲非 final 屬性的話還會有一個 Setter 方法,你也可以使用 getset 關鍵字爲額外的屬性添加 GetterSetter 方法:

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

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

  // 定義兩個計算產生的屬性:right 和 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;
}

8.4 抽象類與隱式接口

Dart 中沒有 interface 關鍵字,但是可以使用 abstract 關鍵字聲明抽象類。

其實每一個類都隱式地定義了一個接口並實現了該接口,這個接口包含所有這個類的成員變量和方法。如果想要創建一個 A 類支持調用 B 類的變量和方法並且不想繼承 B 類,那麼我們可以將 B 類作爲接口進行實現。示例代碼:

// Person 類的隱式接口中包含 greet() 方法。
class Person {
  // _name 變量同樣包含在接口中,但它只是庫內可見的。
  final _name;

  // 構造函數不在接口中。
  Person(this._name);

  // greet() 方法在接口中。
  String greet(String who) => '你好,$who。我是$_name。';
}

// Person 接口的一個實現。
class Impostor implements Person {
  get _name => '';

  String greet(String who) => '你好$who。你知道我是誰嗎?';
}

8.5 擴展方法

可以使用 extension on 關鍵字來創建擴展方法,示例代碼:

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }
}
print('123'.parseInt() == 123); // 輸出 true

9. 泛型

Dart 的泛型類型與 Java 一致,唯一不同的是它在運行時也會保持類型信息,即不會被擦除:

var names = List<String>();
names.addAll(['小芸', '小芳', '小民']);
print(names is List<String>); // true

10. 庫和可見性

每個 Dart 程序都是一個代碼庫。

Dart 中沒有類似於 Java 的 public、protected 和 private 成員訪問限定符。如果一個標識符以下劃線 _ 開頭則表示該標識符在代碼庫內是私有的。

11. 異步支持

11.1 處理 Future

在 Dart 中使用 asyncawait 關鍵字實現異步編程,它和 JavaScript 中的用法一模一樣,並且讓你的代碼看起來就像是同步的一樣。例如,下面的代碼使用 await 等待異步函數的執行結果:

await lookUpVersion();

必須在帶有 async 關鍵字的異步函數中才能使用 await

Future checkVersion() async {
  var version = await lookUpVersion();
  // 使用 version 繼續處理邏輯
}

儘管異步函數可以處理耗時操作,但是它並不會等待這些耗時操作完成,異步函數執行時會在其遇到第一個 await 表達式的時候返回一個 Future 對象,然後等待 await 表達式執行完畢後繼續執行。

可以使用 try-catch 處理 await 導致的異常:

try {
  version = await lookUpVersion();
} catch (e) {
  // 無法找到版本時做出的反應
}

也可以在異步函數中多次使用 await 關鍵字,示例代碼:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

await 表達式的返回值通常是一個 Future 對象;如果不是的話也會自動將其包裹在一個 Future 對象裏。Future 對象代表一個“承諾”,即 await 表達式會阻塞直到需要的對象返回。

11.2 聲明異步函數

定義異步函數時,需要將關鍵字 async 添加到函數並讓其返回一個 Future 對象。假設有如下返回 String 對象的方法:

String lookUpVersion() => '1.0.0';

將其改爲異步函數,返回值是 Future

Future<String> lookUpVersion() async => '1.0.0';

注意,使用 async 時函數體不需要使用 Future API,Dart 會自動創建 Future 對象。

如果函數不需要返回有效值,需要設置其返回類型爲 Future<void>

11.3 處理返回結果

當異步函數最終返回結果之後,可以使用 FuturethencatchError 等函數來處理結果:

var future = lookUpVersion();
future.then((value) => print(value))
    .catchError((error) => print(error));

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章