Dart 语言入门

Dart 介绍

  • Dart 是由谷歌开发的计算机语言,可以用于web、服务器、移动应用和物联网等领域的开发。它诞生于2011年,号称要取代JavaScript,但过去几年一直不温不火,直到flutter的出现才被人们重新重视。
  • Dart 官网:https://dart.dev/

Dart 语言特性

借助于先进的工具链和编译器,Dart 是少数同时支持 JIT和 AOT的语言之一。

  • JIT :(Just In Time,即时编译)在运行时即时编译,在开发周期中使用,可以动态下发和执行代码,开发测试效率高,但运行速度和执行性能则会因为运行时即时编译受到影响。在开发期使用 JIT 编译,可以缩短产品的开发周期。
  • AOT:(Ahead of Time,运行前编译)即提前编译,可以生成被直接执行的二进制代码,运行速度快、执行性能表现好,但每次执行前都需要提前编译,开发测试效率低。在发布期使用 AOT,就不需要像 React Native 那样在跨平台 JavaScript 代码和原生 Android、iOS 代码之间建立低效的方法调用映射关系,所以说Dart具有运行速度快,执行性能好的特点。
内存分配与垃圾回收
  • Dart VM 的内存分配策略比较简单,创建对象时只需要在堆上移动指针,内存增长始终是线性的,省去了查找可用内存的过程。
  • 在 Dart 中,并发是通过 Isolate 实现的。Isolate 是类似于线程但不共享内存,独立运行的 worker。这样的机制,就可以让 Dart 实现无锁的快速分配。
  • Dart 的垃圾回收,则是采用了多生代算法。新生代在回收内存时采用“半空间”机制,触发垃圾回收时,Dart 会将当前半空间中的“活跃”对象拷贝到备用空间,然后整体释放当前空间的所有内存。回收过程中,Dart 只需要操作少量的“活跃”对象,没有引用的大量“死亡”对象则被忽略,这样的回收机制很适合 Flutter 框架中大量 Widget 销毁重建的场景。
单线程模型
  • 支持并发执行线程的高级语言(比如,C++、Java、Objective-C),大都以抢占式的方式切换线程,即:每个线程都会被分配一个固定的时间片来执行,超过了时间片后线程上下文将被抢占后切换。如果这时正在更新线程间的共享资源,抢占后就可能导致数据不同步的问题。
  • 解决这一问题的典型方法是,使用锁来保护共享资源,但锁本身又可能会带来性能损耗,甚至出现死锁等更严重的问题。
  • 这时,Dart 是单线程模型的优势就体现出来了,因为它天然不存在资源竞争和状态同步的问题。这就意味着,一旦某个函数开始执行,就将执行到这个函数结束,而不会被其他 Dart 代码打断。
  • 所以,Dart 中并没有线程,只有 Isolate(隔离区)。Isolates 之间不会共享内存,就像几个运行在不同进程中的 worker,通过事件循环(Event Looper)在事件队列(Event Queue)上传递消息通信。
环境搭建
  • 本地开发Dart程序的话需要先安装Dart sdk。MAC 下执行下面两条指令,brew tap dart-lang/dart 成功后执行brew install dart。执行命令中如有报错,根据报错内容自行解决。
  • 这里我们选用VSCode作为开发工具。需要给VSCode安装两个插件,首先在VC中的Extensions中搜索dart插件安装,然后搜索code runner 安装,Code Runner 可以运行我们的文件。
注释
  • Dart 支持行注释和块注释
// var str='this is var';  双斜杠注释
/// String str='this is var'; 三斜杠注释

//块注释
/*
  var str='this is var';
  String str='this is var';
*/
入口函数
  • dart 使用main函数作为入口函数
main() {
}
// 或者
void main() {
}

变量和常量

变量
  • dart是一门强大的脚本类语言,可以不预先定义变量类型,会根据赋值的具体内容推导出变量的类型。
  • dart 使用var 和 具体类型修饰变量。例如:var str = ' this is var; '等同于String str = ' this is var; '。这里需要注意用var了就不要再指定类型,写了类型就不要再写var,如果两个同时写的话会报错。例如: var int a = 0; 会报错Error: Variables can't be declared using both 'var' and a type name.
常量
  • dart 使用final和const 修饰常量,并且都只能赋值一次。
  • const 修饰的常量一开始就得赋值。 const age = 20;
  • final 修饰的常量可以开始不赋值,它不仅有const的编译时常量的特性,最重要的是它是运行时常量,并且final是惰性初始化,即在运行时第一次使用前才初始化。比如我们程序中需要获取当前时间final a = new DateTime.now();,这个肯定是运行时才能确定的量,而不能在编译时赋值,这个时候就需要用final修饰。
命名规则
  • 变量名必须由数字、字母、下划线、美元符($)组成。
  • 标识符开头不能是数字
  • 标识符不能是保留字和关键字
  • 变量的名字是区分大小写的如: age和Age是不同的变量。在实际的运用中,也建议,不要用一个单词大小写区分两个变量
  • 标识符(变量名称)一定要见名思意 :变量名称建议用名词,方法名称建议用动词

数据类型

字符串
  • 定义字符串可以用双引号也可以用单引号,也可以用三个引号(单引号 双引号都可以)定义多行字符串
String str1 = "string1";
String str2 = 'string2';
String str3 = '''
  duo
  hang
  string
  ''';
  • 字符串拼接: 多个字符串可以直接使用 + 拼接
  • 判断字符串是否为空: var str = ''; str.isEmpty
数值类型
  • 整型(int): 必须赋值整型
  • 浮点型(double):既可以赋值浮点型也可以赋值整型
bool类型
  • 只能是false 或者 true
List(数组)
  • var list = ['0','11','222'];
  • var list = new List();
  • var list = new List<String>();
  • 常用属性:
    length 长度
    reversed 翻转,但是反转后的结果是一个集合而不是列表,如果要转换成列表,还需要调用toList()方法
    isEmpty 是否为空
    isNotEmpty 是否不为空
  • 常用方法:
    add 增加
    addAll 拼接数组
    indexOf 查找 传入具体值
    remove 删除 传入具体值
    removeAt 删除 传入索引值
    fillRange 修改,例如myList.fillRange(1, 2,'aaa');
    insert(index,value); 指定位置插入
    insertAll(index,list) 指定位置插入List
    toList() 其他类型转换成List
    join() List转换成字符串
    split() 字符串转化成List,以某个字符切割字符串
    forEach 便利数组 myList.forEach((value){ print("$value"); });
    map 主要用于修改集合数据var newList=myList.map((value){ return value*2; });
    where 过滤数据var newList=myList.where((value){return value>5;});
    any 数组里面只要有一个满足条件就返回true var f=myList.any((value){ return value>5;});
    every 数组里面每一个都满足条件才返回true var f=myList.every((value){ return value>5;});
Maps(字典)
  • var person={"name":"zhangsan","age":20};
  • 常用属性:
    keys 获取所有的key值
    values 获取所有的value值
    isEmpty 是否为空
    isNotEmpty 是否不为空
  • 常用方法:
    remove(key) 删除指定key的数据
    addAll({...}) 合并映射 给映射内增加属性
    containsValue 查看映射内的值 返回true/false
    forEach
    map
    where
    any
    every

Set

  • Set是没有顺序且不能重复的集合,所以不能通过索引去获取值
var s=new Set();
  s.add('香蕉');
  s.add('苹果');
  s.add('苹果');
  print(s);   //{香蕉, 苹果}
判断数据类型
  • Dart中使用is关键字来判断数据类型
  var list = ['0','11','222'];
  if (list is String) {
    print("list is string");
  } else if (list is int) {
    print("list is int");
  } else if (list is List) {
    print("list is list");
  }

运算符

  • 算术运算符: 加(+)、减(-)、乘(*)、除(/)、取整(~/)、取余(%)
  • 关系运算符:恒等(==)、不等(!=)、大于(>)、小于(<)、大于等于(>=)、小于等于(<=)
  • 逻辑运算符: 非(!)、且(&&)、或(||)
赋值运算符
  • 基础赋值运算符:=、??=
    int b = 10; b ??= 20; //该语句表示如果b的值为空则把20赋值给b,如果b的值不为空,则不作处理。如果是 int b; b??= 20; // 此时b的值为20
  • 复合赋值运算符:+=、-=、*=、/=、%=、~/=、++、--
  • 三目运算: 与C语言三目运算一样
数值类型与String之间的转换
  • 数值类型转换成String:
    var num = 12; var string = num.toString();
    var num = 12.0; var string = num.toString();
  • String 转化成数值类型:
    var string = '12'; var num = int.parse(string);
    var string = '12.0'; var num = double.parse(string);
    注意1:如果字符串内容是浮点型,则只能用double.parse()转换,如果使用int.parse()转换则会报错。如果字符串内容是整型,既可以用double.parse() 又可以用int.parse()转换
    注意2:如果字符串是空或者字符串内容不是数值,转换成数值类型的时候会报错,可以用try ... catch 处理
      String price='';
      try{
        var myNum=double.parse(price);
        print(myNum);
      }catch(err){
           print(0);
      } 

函数

  • 函数格式
返回类型  方法名称(参数1,参数2,...){
     方法体
     return 返回值;
}
  • 嵌套函数
  void xxx(){
      aaa(){
          print(getList());
          print('aaa');
      }
      aaa();
  }
  • 可选参数: 用[]括起来的参数都是可选参数,可选参数也可以设置默认参数
    String printUserInfo(String username,[String home = "中国",int age]){  //行参
      if(age!=null){
        return "姓名:$username---家乡:$home---年龄:$age";
      }
      return "姓名:$username---家乡:$home---年龄保密";
    }
    print(printUserInfo('张三',21,'上海')); 
    print(printUserInfo('张三'));
  • 命名参数:命名参数用{}括起来,调用命名参数的函数时,需要写明命名参数的参数名
  String printUserInfo(String username,{int age,String sex='男'}){  //行参
      if(age!=null){
        return "姓名:$username---性别:$sex--年龄:$age";
      }
      return "姓名:$username---性别:$sex--年龄保密";
  }
  print(printUserInfo('张三',age:20,sex:'未知'));
  • 匿名函数
  var fn=(){
    print('我是一个匿名方法');
  };      
  fn();
  • 函数也可以作为另一个函数的参数
  //方法
  fn1(){
    print('fn1');
  }
  //方法
  fn2(fn){
    fn();
  }
  //调用fn2这个方法 把fn1这个方法当做参数传入
  fn2(fn1);
  • 箭头函数:箭头函数的函数体只能有一句代码
    var newList=list.map((value)=>value>2?value*2:value); // 将list中大于2的元素乘以2返回
  • 自执行函数:程序运行起来就会执行的函数
    ((int n){
      print(n);
      print('我是自执行方法');
    })(12);
  • 闭包:函数嵌套函数, 内部函数会调用外部函数的变量或参数, 变量或参数不会被系统回收(不会释放内存)。可以实现常驻内存,不污染全局。 闭包的写法: 函数嵌套函数,并return 里面的函数,这样就形成了闭包。
    fn(){
        var a=123;  /*不会污染全局   常驻内存*/
        return(){           
          a++;          
          print(a);
        };       
      }     
      var b=fn();   
      b(); //124
      b(); //125

  • Dart所有的东西都是对象,所有的对象都继承自Object类。
  • Dart是一门使用类和单继承的面向对象语言,所有的对象都是类的实例,并且所有的类都是Object的子类。
构造函数
默认构造函数
  • dart的默认构造函数只能有一个,由类名+(参数s){}构成Person(String name,int age){this.name = name; this.age = age;}
  • 默认构造函数简写格式为类名+(接收参数的属性s)Person(this.name, this.age);// 将接收到的第一个参数赋值给this.name,接收到的第二个参数,赋值给this.age,等同于上面的Person(String name,int age){this.name = name; this.age = age;}
命名构造函数
  • dart 中命名构造函数可以有多个
  • 命名构造函数格式为类名.函数名(参数s) { }Person.student(){函数体}。使用命名构造函数生成一个实例:Person p = new Person.student();
Dart中的私有属性和方法
  • dart中没有public、private、protected访问修饰符
  • dart中使用下划线(_)将一个属性或者方法变为私有。需要注意的是加了下划线的私有属性或者方法依然可以被当前文件中的其他类访问,只是不能被别的文件中的模块访问。如果需要只能当前类访问,需要将该类放到一个单独的文件中
getter 和 setter
class Rect{
  num height;
  num width; 
  //Rect(this.height,this.width);
  Rect():height=2,width=10{//在构造函数体运行之前初始化实例变量
    
  }
  get area{
    return this.height*this.width;
  }
  set areaHeight(value){
    this.height=value;
  }
}

静态成员和静态方法

  • dart中使用static关键字来实现类级别的变量和函数
  • 静态方法不能访问非静态成员,非静态成员方法可以访问静态成员
class Person {
  static String name = '张三';
  int age=20;
  static void show() {
    print(name);
  }
  void printInfo(){  /*非静态方法可以访问静态成员以及非静态成员*/
      print(name);  //访问静态属性
      print(this.age);  //访问非静态属性
      show();   //调用静态方法
  }
  static void printUserInfo(){//静态方法
        print(name);   //静态属性
        show();        //静态方法
        // print(this.age);     //静态方法没法访问非静态的属性 报错
        // this.printInfo();   //静态方法没法访问非静态的方法 报错
        // printInfo();        //静态方法没发访问非静态的方法  报错
        Person p = Person();
        p.printInfo();
  }
}

Dart 中的对象操作符

  • ? 条件运算符Perosn p; p?.getInfo();// 如果p有值调用getInfo方法,如果没值则什么也不做
  • as 类型转换
    var p;
    p='';
    p=new Person();
    (p as Person).printInfo(); //如果p不是Person类型则报错
  • is 类型判断
  Person p=new Person('张三', 20);
    if(p is Person){
        p.name="李四";
    }
  • .. 级联操作(连缀)
   Person p1=new Person('张三1', 20);
   p1.printInfo();
   p1..name="李四"
     ..age=30
     ..printInfo(); //等同于下面三句
  //  p1.name='张三222';
  //  p1.age=40;
  //  p1.printInfo();

Dart 中的继承

  • 子类使用extends关键词来继承父类
  • 子类会继承父类里面可见的属性和方法 但是不会继承构造函数(默认构造函数和命名构造函数)
  • 子类能复写父类的方法 getter和setter
  • 子类重写父类方法时用@override关键字,默认不用也可以,一般建议使用
  • 子类中使用super关键字调用父类方法
class Person {
  String name;
  num age; 
  Person(this.name,this.age);
  void printInfo() {
    print("${this.name}---${this.age}");  
  }
}
class Web extends Person{
  Web(String name, num age) : super(name, age){ //子类实现自己的构造方法,同时调用父类的构造方法
  }
}

Dart中的抽象类

  • Dart抽象类主要用于定义标准,子类可以继承抽象类,也可以实现抽象类接口。
  • 抽象类通过abstract 关键字来定义
  • Dart中的抽象方法不能用abstract声明,Dart中没有方法体的方法我们称为抽象方法。
  • 如果子类继承抽象类必须得实现里面的抽象方法
  • 如果把抽象类当做接口实现的话必须得实现抽象类里面定义的所有属性和方法。
  • 抽象类不能被实例化,只有继承它的子类可以
extends抽象类 和 implements的区别:
  • 如果要复用抽象类里面的方法,并且要用抽象方法约束自类的话我们就用extends继承抽象类
  • 如果只是把抽象类当做标准的话我们就用implements实现抽象类
abstract class Animal{
  eat();   //抽象方法
  run();  //抽象方法  
  printInfo(){
    print('我是一个抽象类里面的普通方法');
  }
}
class Dog extends Animal{
  @override
  eat() {
     print('小狗在吃骨头');
  }
  @override
  run() {
    print('小狗在跑');
  }  
}
class Cat extends Animal{
  @override
  eat() {
    print('小猫在吃老鼠');
  }
  @override
  run() {
    print('小猫在跑');
  }
}
Dart 中的多态
  • 允许将子类类型的指针赋值给父类类型的指针, 同一个函数调用会有不同的执行效果 。
  • 子类的实例赋值给父类的引用。
  • 多态就是父类定义一个方法不去实现,让继承他的子类去实现,每个子类有不同的表现。
Dart中的接口
  • dart的接口没有interface关键字定义接口,而是普通类或抽象类都可以作为接口被实现。
  • 使用implements关键字进行实现
  • 如果实现的类是普通类,会将普通类和抽象中的属性的方法全部需要覆写一遍。因为抽象类可以定义抽象方法,普通类不可以.

Dart 中的接口

  • 一个类实现多个接口
abstract class A{
  String name;
  printA();
}
abstract class B{
  printB();
}
class C implements A,B{  
  @override
  String name;  
  @override
  printA() {
    print('printA');
  }
  @override
  printB() {
    return null;
  }
}
Dart 中的混入
  • mixins意思是混入,就是在类中混入其他功能。
  • 在Dart中可以使用mixins实现类似多继承的功能
  • mixins使用的条件,随着Dart版本一直在变,这里说的是Dart2.x中使用mixins的条件:
    1、作为mixins的类只能继承自Object,不能继承其他类
    2、作为mixins的类不能有构造函数
    3、一个类可以mixins多个mixins类
    4、mixins绝不是继承,也不是接口,而是一种全新的特性
class Person{
  String name;
  num age;
  Person(this.name,this.age);
  printInfo(){print('${this.name}----${this.age}');}
  void run(){print("Person Run");}
}
class A {
  String info="this is A";
  void printA(){ print("A");}
  void run(){print("run")}
}
class B {  
  void printB(){print("B");}
  void run(){print("B Run");}
}
class C extends Person with B,A{
  C(String name, num age) : super(name, age);
  
}

  • mixins的类型就是其超类的子类型。
class A {
  String info="this is A";
  void printA(){
    print("A");
  }
}
class B {
  void printB(){
    print("B");
  }
}
class C with A,B{
  
}
void main(){  
  var c=new C();   
  print(c is C);    //true
  print(c is A);    //true
  print(c is B);   //true
}

泛型

  • 泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验)
T getData<T>(T value){
      return value;
 }

  • Dart中使用import关键字引入
  • Dart中的库主要有三种:
自定义库
  • 通过import "库路径"导入import 'lib/xxx.dart';
系统内置库
  • import 'dart: math';
三方库

1、从下面网址找到要用的库

https://pub.dev/packages
https://pub.flutter-io.cn/packages
https://pub.dartlang.org/flutter/

2、需要在自己想项目根目录新建一个pubspec.yaml
3、在pubspec.yaml文件 然后配置名称 、描述、依赖等信息,内容如下

name: xxx
description: A new flutter module project.
dependencies:
http: ^0.12.0+2
date_format: ^1.0.6

4、然后运行 pub get 获取包下载到本地
5、看文档引入库使用

async和await
  • 使用async和await 这两个关键字的使用只需要记住两点:
    1、只有async方法才能使用await关键字调用方法
    2、如果调用别的async方法必须使用await关键字
  • async是让方法变成异步。
  • await是等待异步方法执行完成。
void main() async{
  var result = await testAsync();
  print(result);
}
//异步方法
testAsync() async{
  return 'Hello async';
}

Dart库中的冲突解决

  • 当引入两个库中有相同名称的标识符的时候就会出现冲突,出现冲突的时候可以使用as关键字来指定库的前缀
import 'lib/Person1.dart';
import 'lib/Person2.dart' as lib; // 这里使用as关键字指定Person2.dart库的前缀为lib

main(List<String> args) {
  Person p1=new Person('张三', 20); // 这里调用的是Person1.dart库中的Person
  p1.printInfo();
  lib.Person p2=new lib.Person('李四', 20);// 这里通过lib前缀调用的是Person2.dart中的Person
  p2.printInfo();
}
部分导入
  • 导入库的一部分有两种模式
只导入需要的部分
  • 使用show关键字导入需要的部分,其他部分不导入 import 'package:lib1/lib1.dart' show foo;// 这里只导入lib1.dart库中的foo函数,其他不导入
隐藏不需要导入的部分
  • 使用hide关键字隐藏不需要导入的部分,其他部分导入import 'package:lib2/lib2.dart' hide foo; // 导入/lib2.dart库中除foo函数外的其他功能
延迟加载
  • 也称为懒加载,可以在需要的时候再进行加载。懒加载的最大好处是可以减少APP的启动时间。
  • 懒加载使用deferred as关键字来指定import 'package:deferred/hello.dart' deferred as hello;
  • 当需要使用的时候,需要使用loadLibrary()方法来加载:
    greet() async {
      await hello.loadLibrary();
      hello.printGreeting();
    }
Flutter 包管理
1、pub仓库下载的包
dependencies:
     english_words: ^3.1.3
2、 本地包
dependencies:
    pkg1:
        path: ../../code/pkg1      // 本地包路径
3、使用git 方式引入第三方库
dependencies:
  carousel_pro:
    git:
      url: git://github.com/jlouage/flutter-carousel-pro.git      // url 提供仓库的地址
      ref: master     // ref参数将依赖关系固定到特定git commit,branch或tag
      path: path/subPath      // Pub假定包位于Git存储库的根目录中,如果不是这种情况,您可以使用path参数指定位置
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容