第二章:Dart语法(基础语法)

一、变量和常量

虽然 Dart 是代码类型安全的语言,但是由于其支持类型推断,因此大多数变量不需要显式地指定类型:

var name = '旅行者一号';
var year = 1977;
var antennaDiameter = 3.7;
var flybyObjects = ['木星', '土星', '天王星', '海王星'];
var image = {
  'tags': ['土星'],
  'url': '//path/to/saturn.jpg'
};
默认值

在 Dart 中,未初始化的变量拥有一个默认的初始化值:null。即便数字也是如此,因为在 Dart 中一切皆为对象,数字也不例外。

int lineCount;
assert(lineCount == null);
Final 和 Const

如果你不想更改一个变量,可以使用关键字 final 或者 const 修饰变量,这两个关键字可以替代 var 关键字或者加在一个具体的类型前。一个 final 变量只可以被赋值一次;一个 const 变量是一个编译时常量(const 变量同时也是 final 的)。顶层的 final 变量或者类的 final 变量在其第一次使用的时候被初始化。

final name = 'Bob'; // Without a type annotation
final String nickname = 'Bobby';
内置类型

Dart 语言支持下列的类型:

  • numbers> int、double
  • strings
  • booleans
  • lists (也被称为 arrays)
  • sets
  • maps
  • runes (用于在字符串中表示 Unicode 字符)
  • symbols
字符串和数字之间转换的方式
// String -> int
var one = int.parse('1');
assert(one == 1);

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

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

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');
位移操作

整型支持传统的位移操作,比如移位(<<、>>)、按位与(&)、按位或( ),例如:

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

Dart 字符串是 UTF-16 编码的字符序列。可以使用单引号或者双引号来创建字符串:

var s1 = '使用单引号创建字符串字面量。';
var s2 = "双引号也可以用于创建字符串字面量。";
var s3 = '使用单引号创建字符串时可以使用斜杠来转义那些与单引号冲突的字符串:\'。';
var s4 = "而在双引号中则不需要使用转义与单引号冲突的字符串:'";

可以在字符串中以${表达式}的形式使用表达式,如果表达式是一个标识符,可以省略掉 {}。如果表达式的结果为一个对象,则 Dart 会调用该对象的 toString 方法来获取一个字符串。

var s = '字符串插值';

assert('Dart 有$s,使用起来非常方便。' == 'Dart 有字符串插值,使用起来非常方便。');
assert('使用${s.substring(3,5)}表达式也非常方便' == '使用插值表达式也非常方便。');

== 运算符判断两个对象的内容是否一样,如果两个字符串包含一样的字符编码序列,则表示相等。

可以使用+ 运算符将两个字符串连接为一个,也可以将多个字符串挨着放一起变为一个:

var s1 = '可以拼接'
    '字符串'
    "即便它们不在同一行。";
assert(s1 == '可以拼接字符串即便它们不在同一行。');

var s2 = '使用加号 + 运算符' + '也可以达到相同的效果。';
assert(s2 == '使用加号 + 运算符也可以达到相同的效果。');

可以使用三个单引号或者三个双引号创建多行字符串:

var s1 = '''
你可以像这样创建多行字符串。
''';

var s2 = """这也是一个多行字符串。""";

在字符串前加上 r作为前缀创建 “raw” 字符串(即不会被做任何处理(比如转义)的字符串):

// 代码中文解释
var s = r'在 raw 字符串中,转义字符串 \n 会直接输出 “\n” 而不是转义为换行。';
布尔类型Booleans

Dart 使用 bool 关键字表示布尔类型,布尔类型只有两个对象 true 和 false,两者都是编译时常量。

// 检查是否为空字符串 (Check for an empty string).
var fullName = '';
assert(fullName.isEmpty);

// 检查是否小于等于零。
var hitPoints = 0;
assert(hitPoints <= 0);

// 检查是否为 null。
var unicorn;
assert(unicorn == null);

// 检查是否为 NaN。
var iMeantToDoThis = 0 / 0;
assert(iMeantToDoThis.isNaN);

集合Lists

var list = [1, 2, 3];

List<int> fixedLengthList = new List(5);
fixedLengthList.length = 0;  // 错误
fixedLengthList.add(499);    // 错误
fixedLengthList[0] = 87;
List<int> growableList = [1, 2];
growableList.length = 0;
growableList.add(499);
growableList[0] = 87;

这里 Dart 推断出 list 的类型为 List<int>,如果往该数组中添加一个非 int 类型的对象则会报错。你可以阅读 类型推断 获取更多相关信息。

List 的下标索引从 0 开始,第一个元素的下标为 0,最后一个元素的下标为 list.length - 1;

var list = [1, 2, 3];
assert(list.length == 3);
assert(list[1] == 2);

list[1] = 1;
assert(list[1] == 1);

如果想要创建一个编译时常量的 List,在 List 字面量前添加 const 关键字即可:

var constantList = const [1, 2, 3];
// constantList[1] = 1; // 取消注释将导致出错 (Uncommenting this causes an error.)

扩展操作符(...)和 null-aware 扩展操作符(...?),它们提供了一种将多个元素插入集合的简洁方法。

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
//如果扩展操作符右边可能为 null ,你可以使用 null-aware 扩展操作符(...?)来避免产生异常:
var list;
var list2 = [0, ...?list];
assert(list2.length == 1);
无需集合Sets

使用 Set 字面量来创建一个 Set 集合的方法:

//创建一个普通集合
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

//指定类型创建
var names = <String>{}; // 类型+{}的形式创建Set。

//添加元素
var elements = <String>{};
elements.add('fluorine');
elements.addAll(halogens);

//.length 可以获取 Set 中元素的数量:
assert(elements.length == 5);

//Set 字面量前添加 const 关键字创建一个 Set 编译时常量:
final constantSet = const {
  'fluorine',
  'chlorine',
  'bromine',
  'iodine',
  'astatine',
};
// constantSet.add('helium'); // 取消注释将导致出错 (Uncommenting this causes an error).

从 Dart 2.3 开始,Set 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及 Collection If 和 Collection For 操作

Maps集合

通常来说, Map 是用来关联 keys 和 values 的对象。keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。但是 value 可以出现多次。


var gifts = {
  // 键:    值
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

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

//也可以使用 Map 的构造器创建 Map
//这里为什么使用 Map() 而不是使用 new Map() 构造 Map 对象。因为从 Dart2 开始,构造对象的 new 关键字可以被省略掉。
var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

//向现有的 Map 中添加键值对
var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // 添加键值对 (Add a key-value pair)
//从一个 Map 中获取一个值的操作
assert(gifts['first'] == 'partridge');
//如果检索的 Key 不存在于 Map 中则会返回一个 null:
assert(gifts['fifth'] == null);
//.length 可以获取 Map 中键值对的数量
assert(gifts.length == 2);

//在一个 Map 字面量前添加 const 关键字可以创建一个 Map 编译时常量
final constantMap = const {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};
// constantMap[2] = 'Helium'; // 取消注释将导致出错 (Uncommenting this causes an error).

从 Dart 2.3 Map 可以像 List 一样支持使用扩展操作符(... 和 ...?)以及 Collection If 和 Collection For 操作

二、函数方法Functions

Dart 是一种真正面向对象的语言,所以即便函数也是对象并且类型为 Function,这意味着函数可以被赋值给变量或者作为其它函数的参数。

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}
//虽然高效 Dart 指南建议在[公开的 API 上定义返回类型](https://dart.cn/guides/language/effective-dart/design#prefer-type-annotating-public-fields-and-top-level-variables-if-the-type-isnt-obvious),不过即便不定义,该函数也依然有效:
isNoble(atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

//如果函数体内只包含一个表达式,你可以使用简写语法:
语法 => 表达式 是 { return 表达式; } 的简写, => 有时也称之为胖箭头语法。
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

可选参数

可选参数分为命名参数和位置参数,可在参数列表中任选其一使用,但两者不能同时出现在参数列表中。
1.命名参数
当你调用函数时,可以使用 参数名: 参数值 的形式来指定命名参数。例如:

enableFlags(bold: true, hidden: false);

定义函数时,使用 {param1, param2, …} 来指定命名参数:

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

虽然命名参数是可选参数的一种类型,但是你仍然可以使用 @required 注解来标识一个命名参数是必须的参数,此时调用者则必须为该参数提供一个值。例如:

const Scrollbar({Key key, @required Widget child})
位置参数

使用 [] 将一系列参数包裹起来作为位置参数:

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}
//不适用可选参数调用
assert(say('Bob', 'Howdy') == 'Bob says Howdy');
//使用可选参数调用
assert(say('Bob', 'Howdy', 'smoke signal') ==
    'Bob says Howdy with a smoke signal');
默认参数值

可以用 = 为函数的命名和位置参数定义默认值,默认值必须为编译时常量,没有指定默认值的情况下默认值为 null。

/// 设置 [bold] 和 [hidden] 标识……
/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold 的值将为 true;而 hidden 将为 false。
enableFlags(bold: true);

List 或 Map 同样也可以作为默认值。下面的示例定义了一个名为 doStuff() 的函数,并为其名为 list 和 gifts 的参数指定了一个 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');
}
main() 函数

每个 Dart 程序都必须有一个 main() 顶级函数作为程序的入口,main() 函数返回值为 void 并且有一个 List<String> 类型的可选参数。
下面是使用命令行访问带参数的 main() 函数示例:

void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}
函数作为一级对象

可以将函数作为参数传递给另一个函数。例如:

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// 将 printElement 函数作为参数传递。
list.forEach(printElement);

//可以将函数赋值给一个变量
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');
匿名函数

大多数方法都是有名字的,比如 main() 或 printElement()。你可以创建一个没有名字的方法,称之为 匿名函数,或 Lambda表达式 或 Closure闭包。
后面大括号中的内容则为函数体:
([[类型] 参数[, …]]) { 函数体; };

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

//或者可以只有一行语句
list.forEach(
    (item) => print('${list.indexOf(item)}: $item'));
词法作用域

Dart 是词法有作用域语言,变量的作用域在写代码的时候就确定了,大括号内定义的变量只能在大括号内访问,与 Java 类似。
下面是一个嵌套函数中变量在多个作用域中的示例:

bool topLevel = true;

void main() {
  var insideMain = true;

  void myFunction() {
    var insideFunction = true;

    void nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}
//注意 nestedFunction() 函数可以访问包括顶层变量在内的所有的变量。
词法闭包

闭包 即一个函数对象,即使函数对象的调用在它原始作用域之外,依然能够访问在它词法作用域内的变量。
函数可以封闭定义到它作用域内的变量。

//函数 makeAdder() 捕获了变量 addBy。无论函数在什么时候返回,它都可以使用捕获的 addBy 变量。

/// 返回一个将 [addBy] 添加到该函数参数的函数。
Function makeAdder(int addBy) {
  return (int i) => addBy + i;
}

void main() {
  // 生成加 2 的函数。
  var add2 = makeAdder(2);

  // 生成加 4 的函数。
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}
测试函数是否相等

下面是顶级函数,静态方法和示例方法相等性的测试示例:

void foo() {} // 定义顶层函数 (A top-level function)

class A {
  static void bar() {} // 定义静态方法
  void baz() {} // 定义实例方法
}

void main() {
  var x;

  // 比较顶层函数是否相等。
  x = foo;
  assert(foo == x);

  // 比较静态方法是否相等。
  x = A.bar;
  assert(A.bar == x);

  // 比较实例方法是否相等。
  var v = A(); // A 的实例 #1
  var w = A(); // A 的实例 #2
  var y = w;
  x = w.baz;

  // 这两个闭包引用了相同的实例对象,因此它们相等。
  assert(y.baz == x);

  // 这两个闭包引用了不同的实例对象,因此它们不相等。
  assert(v.baz != w.baz);
}

三、运算符

描述 运算符
一元后缀 表达式++ 表达式-- () [] . ?
一元前缀 -表达式 !表达式 ~表达式 ++表达式 --表达式
乘除 取模 (除并取整) * / % ~/
加减法 + -
位运算法 << >> >>>
二进制与 &
二进制异或 ^
二进制或 |
关系和类型测试 >= > <= < as is is!
相等判断 == !=
二进制与 &
逻辑与 &&
逻辑或 ||
空判断 ??
条件表达式 表达式 1 ? 表达式 2 : 表达式 3
级联 ..
赋值 = *= /= += -= &= ^= 等等……

下面是一些运算符表达式的示例:

a++
a + b
a = b
a == b
c ? a : b
a is T

运算符表 中,运算符的优先级按先后排列,即第一行优先级最高,最后一行优先级最低,而同一行中,最左边的优先级最高,最右边的优先级最低。例如:% 运算符优先级高于 == ,而 == 高于 &&。根据优先级规则,那么意味着以下两行代码执行的效果相同:

// 括号提高了可读性。
// Parentheses improve readability.
if ((n % i == 0) && (d % i == 0)) ...

// 难以理解,但是与上面的代码效果一样。
if (n % i == 0 && d % i == 0) ...

Dart 还支持自增自减操作。

描述 运算符
++var var = var + 1 (表达式的值为 var + 1)
var++ var = var + 1 (表达式的值为 var)
--var var = var – 1 (表达式的值为 var – 1)
var-- var = var – 1 (表达式的值为 var)
类型判断运算符

as、is、is! 运算符是在运行时判断对象类型的运算符。
仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型。例如:

//类型转换
(emp as Person).firstName = 'Bob';

//如果你不确定这个对象类型是不是 T,请在转型前使用 is T 检查类型。
if (emp is Person) {
  // 类型检查
  emp.firstName = 'Bob';
}
可以使用 as 运算符进行缩写:
(emp as Person).firstName = 'Bob';

上述两种方式是有区别的:如果 emp 为 null 或者不为 Person 类型,则第一种方式将会抛出异常,而第二种不会。

四、流程控制语句

  • if 和 else
  • for 循环
  • while 和 do-while 循环
  • break 和 continue
  • switch 和 case
  • assert

使用 try-catchthrow 也能影响控制流,详情参考异常部分

If 和 else
if (isRaining()) {
  you.bringRainCoat();
} else if (isSnowing()) {
  you.wearJacket();
} else {
  car.putTopDown();
}
For 循环
//标准的 for 循环进行迭代
var message = StringBuffer('Dart is fun');
for (var i = 0; i < 5; i++) {
  message.write('!');
}

//闭包会自动捕获循环的 索引值
var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());

// `for-in` 形式的 [迭代]
var collection = [0, 1, 2];
for (var x in collection) {
  print(x); // 0 1 2
}
While 和 Do-While
//while 循环会在执行循环体前先判断条件:
while (!isDone()) {
  doSomething();
}
//do-while 循环则会先执行一遍循环体 再 判断条件:
do {
  printLine();
} while (!atEndOfPage());

Break 和 Continue
//使用 break 可以中断循环:
while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}
//使用 continue 可以跳过本次循环直接进入下一次循环:
for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}

Switch 和 Case
var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}
断言

在条件表达式为 false 时使用assert(条件, 可选信息);语句来打断代码的执行

// 确保变量值不为 null (Make sure the variable has a non-null value)
assert(text != null);
//assert 的第二个参数可以为其添加一个字符串消息。
assert(urlString.startsWith('https'),
    'URL ($urlString) should start with "https".');

五、异常

Dart 代码可以抛出和捕获异常。异常表示一些未知的错误情况,如果异常没有捕获则会被抛出从而导致抛出异常的代码终止执行。
与 Java 不同的是,Dart 的所有异常都是非必检异常,方法不一定会声明其所抛出的异常并且你也不会被要求捕获任何异常

抛出异常

下面是关于抛出或者 引发 异常的示例:

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

//也可以抛出任意的对象:
throw 'Out of llamas!';

优秀的代码通常会抛出 ErrorException 类型的异常。

因为抛出异常是一个表达式,所以可以在 => 语句中使用,也可以在其他使用表达式的地方抛出异常:

//方法上抛出异常
void distanceTo(Point other) => throw UnimplementedError();
捕获异常

捕获异常可以避免异常继续传递

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

//多个 catch 语句
try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // 指定异常
  buyMoreLlamas();
} on Exception catch (e) {
  // 其它类型的异常
  print('Unknown exception: $e');
} catch (e) {
  // // 不指定类型,处理其它全部
  print('Something really unknown: $e');
}

关键字 rethrow 可以将捕获的异常再次抛出:

void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // 运行时错误
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // 允许调用者查看异常。
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}
Finally

可以使用 finally 语句来包裹确保不管有没有异常都执行代码,如果没有指定 catch 语句来捕获异常,则在执行完 finally 语句后再抛出异常:

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