Flutter之Dart 语言基础知识 1

Flutter 概述与 iOS 工作方式

Flutter 简介

Flutter 是一个由 Google 开发的跨平台 UI 工具包,旨在通过单一代码库支持移动(iOS、Android)、Web、桌面(Windows、macOS、Linux)等多个平台开发。自 2017 年推出以来,Flutter 因其快速开发周期和热重载功能(Hot Reload)受到广泛欢迎,开发者可以修改代码后几乎即时看到效果,而无需重新编译。

Flutter 使用 Dart 语言,编译为目标平台的原生机器码(如 iOS 的 ARM 或 Intel 代码),确保应用的高性能和流畅的用户体验。对于 iOS 开发者来说,Flutter 提供了一个高效的跨平台解决方案,减少了为 iOS 和 Android 分别开发和维护代码的成本。

Flutter 在 iOS 上的工作方式

Flutter 在 iOS 上的工作方式包括以下几个关键方面:

  • 编译与性能:Flutter 将 Dart 代码编译为 iOS 原生机器码,确保应用运行速度快,动画流畅。编译过程通过 Skia 图形引擎渲染 UI,Skia 是一个高性能的 2D 图形库。
  • Widget 系统:Flutter 的核心是 Widget 系统,提供了丰富的预定义组件,包括 Material Design(适合 Android)和 Cupertino Widget(适合 iOS)。Cupertino Widget 专门设计为遵循 Apple 的 Human Interface Guidelines,使 Flutter 应用在 iOS 上具有原生应用的视觉和交互体验。
  • 集成与开发:您可以从头开始使用 Flutter 构建 iOS 应用,也可以通过平台通道(Platform Channels)将 Flutter 集成到现有 iOS 应用中。开发环境需要在 macOS 上安装 Xcode 和 Flutter SDK,Xcode 用于编译和调试 iOS 应用。
  • 与 SwiftUI 的对比:Flutter 和 SwiftUI 都是声明式框架,开发者只需描述 UI 的最终状态,框架会自动处理渲染。但 Flutter 使用 Widget 而不是 SwiftUI 的 View,布局机制不同:SwiftUI 中父 View 提出大小建议,子 View 返回实际大小;Flutter 中父 Widget 传递约束(constraints),子 Widget 在这些约束内决定大小。
iOS 开发者的优势

作为 iOS 开发者,您可以利用 Swift 和 SwiftUI 的经验快速适应 Flutter。例如,SwiftUI 的声明式 UI 设计与 Flutter 的 Widget 系统有相似之处,Cupertino Widget 让您轻松创建符合 iOS 设计规范的界面。此外,Flutter 的热重载功能比 Xcode 的实时预览更高效,适合快速迭代开发。

Dart 语言基础知识(与 Swift 对比)

Dart 是 Flutter 的专用语言,设计目标是简单、高效且易于学习。作为 iOS 开发者,您会发现 Dart 与 Swift 有许多相似之处,但也有一些关键差异。以下是 Dart 基本语法的详细对比,包括代码示例和注解。

变量
  • Dart
    • 使用 var 进行类型推断,如 var name = 'Bob';
    • 使用 final 表示不可变变量,如 final name = 'Bob';,初始化后不可更改。
    • 使用 const 表示编译时常量,如 const bar = 1000000;,只能用于编译时已知的值。
    • 语句以分号 ; 结尾。
  • Swift
    • 使用 var 表示可变变量,如 var name: String = "Bob"
    • 使用 let 表示不可变变量,如 let name = "Bob"
    • Swift 无直接 const,但 let 类似。
  • 对比与注解
    • Dart 的 final 和 Swift 的 let 功能相似,都是用于不可变变量,但 Dart 的 const 更严格,仅用于编译时常量。
    • 示例:
      // Dart 示例
      String name = 'Bob';  // 显式类型声明
      var inferredName = 'Bob';  // 类型推断
      final immutableName = 'Bob';  // 不可变变量
      const constantValue = 1000000;  // 编译时常量
      
      // Swift 示例
      var name: String = "Bob"  // 可变变量
      let immutableName = "Bob"  // 不可变变量
      
数字
  • Dart
    • 支持 int(整数)和 double(浮点数),如 int intVariable = 3;
    • 允许 intdouble 直接比较,如 3 == 3.0true
    • 除法运算符 / 返回 double,整数除法用 ~/
  • Swift
    • 使用 IntDouble,如 var intVariable: Int = 3
    • IntDouble 是不同类型,不能直接比较,如 3 == 3.0false
  • 对比与注解
    • Dart 的数字类型更灵活,适合跨平台开发;Swift 更严格,类型安全更高。
    • 示例:
      // Dart 示例
      int intVariable = 3;
      double doubleVariable = intVariable.toDouble();
      print(3 == 3.0);  // 输出 true
      
      // Swift 示例
      var intVariable: Int = 3
      var doubleVariable: Double = Double(intVariable)
      print(3 == 3.0)  // 输出 false
      
字符串
  • Dart
    • 支持单引号和双引号,如 String s1 = 'This is a String';
    • 多行字符串使用三引号 '''""",如 '''Multiline string'''
    • 字符串插值使用 $variable,如 'I like $food'
  • Swift
    • 使用双引号,如 let s1: String = "This is a String"
    • 多行字符串使用 #"""..."""#,如 #"""Multiline string"""#
    • 字符串插值使用 \(variable),如 "I like \(food)"
  • 对比与注解
    • Dart 更倾向于单引号,Swift 强制使用双引号。多行字符串语法类似,但 Swift 需要 # 标记。
    • 示例:
      // Dart 示例
      String s1 = 'This is a String';
      String multiline = '''Multiline
      string''';
      String interpolation = 'I like $food';  // food 是变量
      
      // Swift 示例
      let s1: String = "This is a String"
      let multiline = #"""
      Multiline
      string
      """#
      let interpolation = "I like \(food)"  // food 是变量
      
空安全
  • Dart
    • 使用 ? 表示可空类型,如 String? name;
    • 支持空安全运算符,如 ??(空合并)、?.(安全调用)、!(强制解包)。
    • Dart 2.12 引入了空安全(null safety),默认启用。
  • Swift
    • 使用 ? 表示可选类型,如 var name: String?
    • 支持类似运算符,如 ??(空合并)、?.(可选链)、!(强制解包)。
  • 对比与注解
    • Dart 和 Swift 的空安全机制类似,但 Dart 的 late 关键字类似于 Swift 的隐式解包可选类型。
    • 示例:
      // Dart 示例
      String? name;  // 可空字符串
      String nonNullName = name ?? 'default';  // 使用空合并运算符
      
      // Swift 示例
      var name: String?  // 可选字符串
      let nonNullName = name ?? "default"
      
函数
  • Dart
    • 入口点为 void main() { ... },如 void main() { print('Hello, World!'); }
    • 支持命名参数({String name = 'World'})和可选参数([int c = 1])。
    • 默认参数为位置参数,可用 required 标记命名参数为必填。
  • Swift
    • 入口点为 func main() { ... },如 func main() { print("Hello, World!") }
    • 支持默认参数,如 func greet(name: String = "World")
  • 对比与注解
    • Dart 的命名参数更灵活,适合复杂函数;Swift 的参数标签更直观。
    • 示例:
      // Dart 示例
      void main() {
        print('Hello, World!');
      }
      void greet({String name = 'World'}) {
        print('Hello, $name!');
      }
      
      // Swift 示例
      func main() {
        print("Hello, World!")
      }
      func greet(name: String = "World") {
        print("Hello, \(name)!")
      }
      
控制流
  • Dart
    • if 语句需要括号,如 if (a == 1) { ... }
    • for-in 循环用于迭代 Iterable,如 for (var i in list) { ... }
    • 无范围语法(如 Swift 的 0..<5),需手动构造。
  • Swift
    • if 语句无需括号,如 if a == 1 { ... }
    • for-in 循环用于迭代数组或集合,如 for i in array { ... }
  • 对比与注解
    • Dart 的 for-in 更适合迭代集合,Swift 支持范围循环。
    • 示例:
      // Dart 示例
      if (a == 1) {
        print('a is 1');
      }
      for (var i in list) {
        print(i);
      }
      
      // Swift 示例
      if a == 1 {
        print("a is 1")
      }
      for i in array {
        print(i)
      }
      
集合
  • Dart
    • 支持 List(列表)、Set(集合)、Map(映射)。
    • 示例:List<String> list = ['one', 'two'];
    • 支持展开运算符(如 ......?)。
  • Swift
    • 支持 Array(数组)、Set(集合)、Dictionary(字典)。
    • 示例:var list: [String] = ["one", "two"]
    • 使用 + 进行列表拼接。
  • 对比与注解
    • Dart 的集合类型更灵活,Swift 更注重类型安全。
    • 示例:
      // Dart 示例
      List<String> list = ['one', 'two'];
      Set<String> set = {'a', 'b'};
      Map<String, String> map = {'key': 'value'};
      
      // Swift 示例
      var list: [String] = ["one", "two"]
      var set: Set<String> = ["a", "b"]
      var map: [String: String] = ["key": "value"]
      
  • Dart
    • 支持构造函数,如 Point(this.x, this.y);,可以定义命名构造函数。
    • 支持抽象类和隐式接口。
    • 使用 with 关键字实现混入(mixins)。
  • Swift
    • 使用 init 初始化器,如 init(x: Int, y: Int)
    • 无命名构造函数,接口通过协议(protocols)实现。
    • 使用扩展(extensions)替代混入。
  • 对比与注解
    • Dart 的类系统更灵活,Swift 更注重协议和扩展。
    • 示例:
      // Dart 示例
      class Point {
        final int x, y;
        Point(this.x, this.y);
      }
      
      // Swift 示例
      class Point {
        let x: Int, y: Int
        init(x: Int, y: Int) {
          self.x = x
          self.y = y
        }
      }
      

字符串操作

Dart 的字符串是 UTF-16 编码的不可变对象,提供了 26 个方法用于操作和处理字符串。以下是所有方法的详细列表:

方法名称 描述 代码示例 注解
allMatches 返回字符串中所有匹配的子字符串。 var result = 'hello world'.allMatches('o'); 返回一个 Iterable<Match>,包含所有匹配结果。
codeUnitAt 返回指定索引处的 UTF-16 码元。 var code = 'hello'.codeUnitAt(1); 索引从 0 开始,返回整数。
compareTo 比较当前字符串与另一个字符串。 var cmp = 'a'.compareTo('b'); 返回负数、0 或正数,表示当前字符串相对于另一个字符串的排序顺序。
contains 检查字符串是否包含指定子字符串。 var hasO = 'hello'.contains('o'); 返回布尔值。
endsWith 检查字符串是否以指定子字符串结尾。 var endsWithLo = 'hello'.endsWith('lo'); 返回布尔值。
indexOf 返回指定子字符串的第一个匹配位置。 var index = 'hello'.indexOf('l'); 返回索引(从 0 开始),如果未找到则返回 -1。
lastIndexOf 返回指定子字符串的最后一个匹配位置。 var lastIndex = 'hello'.lastIndexOf('l'); 返回索引(从 0 开始),如果未找到则返回 -1。
matchAsPrefix 检查字符串是否以指定模式开头。 var match = 'hello'.matchAsPrefix('he'); 返回 Match 对象或 null
noSuchMethod 当访问不存在的方法或属性时调用。 (继承自 Object,无直接示例) 通常用于调试,继承自 Object。
padLeft 在字符串左侧填充指定字符直到达到指定长度。 var padded = 'hello'.padLeft(10, '-'); 返回新字符串,长度为 10,左侧填充 '-'。
padRight 在字符串右侧填充指定字符直到达到指定长度。 var padded = 'hello'.padRight(10, '-'); 返回新字符串,长度为 10,右侧填充 '-'。
replaceAll 替换所有匹配的子字符串。 var replaced = 'hello'.replaceAll('l', 'L'); 返回新字符串,所有 'l' 被替换为 'L'。
replaceAllMapped 使用函数替换所有匹配的子字符串。 var replaced = 'hello'.replaceAllMapped('l', (m) => m[0].toUpperCase()); 返回新字符串,所有 'l' 被替换为 'L'。
replaceFirst 替换第一个匹配的子字符串。 var replaced = 'hello'.replaceFirst('l', 'L'); 返回新字符串,第一个 'l' 被替换为 'L'。
replaceFirstMapped 使用函数替换第一个匹配的子字符串。 var replaced = 'hello'.replaceFirstMapped('l', (m) => m[0].toUpperCase()); 返回新字符串,第一个 'l' 被替换为 'L'。
replaceRange 替换指定范围内的子字符串。 var replaced = 'hello'.replaceRange(1, 3, 'LL'); 返回新字符串,索引 1 到 3 的部分被替换为 'LL'。
split 根据指定模式分割字符串。 var parts = 'hello world'.split(' '); 返回字符串列表,按空格分割。
splitMapJoin 根据模式分割字符串,并使用函数处理每部分。 var joined = 'hello world'.splitMapJoin(' ', onMatch: (m) => ' '); 返回新字符串,空格被保留。
startsWith 检查字符串是否以指定子字符串开头。 var startsWithHe = 'hello'.startsWith('he'); 返回布尔值。
substring 返回指定范围内的子字符串。 var sub = 'hello'.substring(1, 3); 返回从索引 1 到 3 的子字符串。
toLowerCase 将字符串转换为小写。 var lower = 'Hello'.toLowerCase(); 返回新字符串,所有字符为小写。
toString 返回字符串表示。 (继承自 Object,无直接示例) 通常返回自身,继承自 Object。
toUpperCase 将字符串转换为大写。 var upper = 'hello'.toUpperCase(); 返回新字符串,所有字符为大写。
trim 去除字符串首尾的空白字符。 var trimmed = ' hello '.trim(); 返回新字符串,首尾空白被移除。
trimLeft 去除字符串左侧的空白字符。 var trimmed = ' hello'.trimLeft(); 返回新字符串,左侧空白被移除。
trimRight 去除字符串右侧的空白字符。 var trimmed = 'hello '.trimRight(); 返回新字符串,右侧空白被移除。

注解:

  • Dart 的字符串方法与 Swift 的字符串方法有许多相似之处,例如 containsstartsWithendsWithtrim
  • Dart 的字符串是不可变的,因此所有方法返回新字符串,而不修改原字符串。

集合操作

Dart 支持列表(List)、集合(Set)和映射(Map),以下是每种集合的所有方法及其用法。

列表(List)

方法名称 描述 代码示例 注解
add 在列表末尾添加元素。 var list = [1, 2, 3]; list.add(4); 修改原列表。
addAll 添加多个元素。 var list = [1, 2]; list.addAll([3, 4]); 修改原列表。
any 检查是否有元素满足条件。 var hasEven = [1, 2, 3].any((e) => e % 2 == 0); 返回布尔值。
asMap 将列表转换为映射(索引到元素)。 var map = [1, 2, 3].asMap(); 返回一个映射,键为索引,值为元素。
cast 将列表视图转换为指定类型。 var casted = [1, 2, 3].cast<int>(); 返回新列表,类型为指定类型。
clear 清空列表。 var list = [1, 2, 3]; list.clear(); 修改原列表。
contains 检查是否包含指定元素。 var has2 = [1, 2, 3].contains(2); 返回布尔值。
elementAt 获取指定索引的元素。 var second = [1, 2, 3].elementAt(1); 返回元素,如果索引无效抛出异常。
every 检查所有元素是否满足条件。 var allEven = [2, 4, 6].every((e) => e % 2 == 0); 返回布尔值。
fillRange 填充指定范围的元素。 var list = [1, 2, 3]; list.fillRange(1, 2, 10); 修改原列表,从索引 1 到 2 填充 10。
firstWhere 返回第一个满足条件的元素。 var firstEven = [1, 2, 3].firstWhere((e) => e % 2 == 0); 返回元素,如果未找到抛出异常。
forEach 对每个元素执行操作。 var list = [1, 2, 3]; list.forEach((e) => print(e)); 不返回值。
indexOf 返回指定元素的第一个索引。 var index = [1, 2, 1].indexOf(1); 返回索引,如果未找到返回 -1。
insert 在指定索引插入元素。 var list = [1, 2, 3]; list.insert(1, 10); 修改原列表。
join 将列表元素连接为字符串。 var joined = [1, 2, 3].join(', '); 返回字符串。
lastWhere 返回最后一个满足条件的元素。 var lastEven = [1, 2, 3].lastWhere((e) => e % 2 == 0); 返回元素,如果未找到抛出异常。
map 对每个元素应用函数,返回新列表。 var doubled = [1, 2, 3].map((e) => e * 2).toList(); 返回新列表。
remove 移除第一个匹配的元素。 var list = [1, 2, 3]; list.remove(2); 修改原列表。
removeAt 移除指定索引的元素。 var list = [1, 2, 3]; list.removeAt(1); 修改原列表。
removeLast 移除最后一个元素。 var list = [1, 2, 3]; list.removeLast(); 修改原列表。
removeRange 移除指定范围的元素。 var list = [1, 2, 3]; list.removeRange(0, 2); 修改原列表。
removeWhere 移除所有满足条件的元素。 var list = [1, 2, 3]; list.removeWhere((e) => e % 2 == 0); 修改原列表。
retainWhere 保留所有满足条件的元素。 var list = [1, 2, 3]; list.retainWhere((e) => e % 2 == 0); 修改原列表。
sort 对列表进行排序。 var list = [3, 1, 2]; list.sort(); 修改原列表。
sublist 返回指定范围的子列表。 var sub = [1, 2, 3].sublist(1, 2); 返回新列表。
where 返回所有满足条件的元素(新迭代器)。 var even = [1, 2, 3].where((e) => e % 2 == 0).toList(); 返回新列表。

集合(Set)

方法名称 描述 代码示例 注解
add 添加元素。 var set = <int>{1, 2}; set.add(3); 修改原集合。
addAll 添加多个元素。 var set = <int>{1, 2}; set.addAll([3, 4]); 修改原集合。
any 检查是否有元素满足条件。 var hasEven = <int>{1, 2, 3}.any((e) => e % 2 == 0); 返回布尔值。
cast 将集合视图转换为指定类型。 var casted = <int>{1, 2, 3}.cast<int>(); 返回新集合。
clear 清空集合。 var set = <int>{1, 2, 3}; set.clear(); 修改原集合。
contains 检查是否包含指定元素。 var has2 = <int>{1, 2, 3}.contains(2); 返回布尔值。
containsAll 检查是否包含所有指定元素。 var hasAll = <int>{1, 2, 3}.containsAll([2, 3]); 返回布尔值。
difference 返回当前集合与另一个集合的差集。 var diff = <int>{1, 2, 3}.difference(<int>{2, 3}); 返回新集合。
intersection 返回当前集合与另一个集合的交集。 var inter = <int>{1, 2, 3}.intersection(<int>{2, 3}); 返回新集合。
remove 移除指定元素。 var set = <int>{1, 2, 3}; set.remove(2); 修改原集合。
removeAll 移除所有指定元素。 var set = <int>{1, 2, 3}; set.removeAll([2, 3]); 修改原集合。
removeWhere 移除所有满足条件的元素。 var set = <int>{1, 2, 3}; set.removeWhere((e) => e % 2 == 0); 修改原集合。
retainAll 保留所有指定元素。 var set = <int>{1, 2, 3}; set.retainAll([2, 3]); 修改原集合。
retainWhere 保留所有满足条件的元素。 var set = <int>{1, 2, 3}; set.retainWhere((e) => e % 2 == 0); 修改原集合。
union 返回当前集合与另一个集合的并集。 var union = <int>{1, 2}.union(<int>{3, 4}); 返回新集合。

映射(Map)

方法名称 描述 代码示例 注解
addAll 添加多个键值对。 var map = {'a': 1}; map.addAll({'b': 2}); 修改原映射。
addEntries 添加多个键值对(使用 Iterable<MapEntry>)。 var map = {'a': 1}; map.addEntries([MapEntry('b', 2)]); 修改原映射。
cast 将映射视图转换为指定类型。 var casted = {'a': 1}.cast<String, int>(); 返回新映射。
clear 清空映射。 var map = {'a': 1}; map.clear(); 修改原映射。
containsKey 检查是否包含指定键。 var hasA = {'a': 1}.containsKey('a'); 返回布尔值。
containsValue 检查是否包含指定值。 var has1 = {'a': 1}.containsValue(1); 返回布尔值。
forEach 对每个键值对执行操作。 var map = {'a': 1}; map.forEach((k, v) => print('$k: $v')); 不返回值。
putIfAbsent 如果键不存在,则添加新值。 var map = {'a': 1}; map.putIfAbsent('b', () => 2); 返回键的值,如果不存在则添加并返回新值。
remove 移除指定键的键值对。 var map = {'a': 1}; map.remove('a'); 修改原映射。
removeWhere 移除所有满足条件的键值对。 var map = {'a': 1, 'b': 2}; map.removeWhere((k, v) => k == 'a'); 修改原映射。
update 更新指定键的值。 var map = {'a': 1}; map.update('a', (v) => v * 2); 如果键不存在,则抛出异常。
updateAll 更新所有键值对的值。 var map = {'a': 1, 'b': 2}; map.updateAll((k, v) => v * 2); 修改原映射。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,048评论 6 542
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,414评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,169评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,722评论 1 317
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,465评论 6 412
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,823评论 1 328
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,813评论 3 446
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,000评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,554评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,295评论 3 358
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,513评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,035评论 5 363
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,722评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,125评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,430评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,237评论 3 398
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,482评论 2 379

推荐阅读更多精彩内容