【C#复习知识点4】 委托、事件、接口、转换、泛型、枚举器和迭代器

——委托

可以理解为:委托是一个包含有序方法列表的对象,这些方法拥有相同类型的参数列表和返回值。
委托既可以卸载类外,也可以写在类内。
委托可以被继承
委托既可以添加普通方法,也可以添加静态方法。

    delegate string Delegate1(string str);        //声明在类的外部
    class Base
    {
        public delegate int Delegate2(int val);   //声明在类的内部

        public void Fun()
        {
            Delegate1 de1 = new Delegate1(Fun3);  //创建类外部的委托的变量
            Console.WriteLine(de1("Delagate1"));

            Delegate2 de2 = new Delegate2(Fun1);  //创建类内部的委托的变量
            de2 += Fun2;                          //添加静态方法
            Console.WriteLine(de2(10));           //打印15,因为委托返回值会返回最后一个方法的返回值
        }

        public int Fun1(int val)
        {
            return val * 2;
        }
        static int Fun2(int val)
        {
            return val + 5;
        }
        string Fun3(string str)
        {
            return str;
        }
    }
    class Child : Base
    {
        public void Method()
        {
            Delegate2 de = new Delegate2(Method1);   //使用基类内部创建的委托
        }
        public int Method1(int val)
        {
            return val + 10;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Base b = new Base();
            b.Fun();
            Console.ReadLine();
        }
    }

如果委托使用引用参数,那么这个参数在委托中第一个方法使用完后,得到的数值再传入下一个方法。

匿名方法

所谓匿名方法,就是没有名字的方法。
当某些方法,只需要在委托中使用一次,其它地方不会调用这个方法,那么可以不声明这个方法,在委托使用时以匿名方法添加上就ok。匿名函数不需要写返回值。
比如:

    class Base
    {
        public delegate int MyDel(int val);        //委托

        public void Fun()
        {
            MyDel myDel1 = delegate (int val) { return val * 2; };//委托初始化时使用匿名方法
            myDel1 += delegate (int val) { return val + 5; };//添加方法时使用匿名放啊
        }
    }
Lambda表达式

Lambda表达式是就是用于进一步简化匿名方法的(Lambda是在匿名方法之后才有的,如果先有Lambda,那么可能就不会有匿名方法了)。因此,所有可以用匿名函数的地方都可以使用Lambda表达式。下面的例子展示Lambda的用法。

    class Base
    {
        public delegate int MyDel1(int x, int y);//两个参数的委托
        public delegate int MyDel2(int val);//一个参数的委托
        public delegate void MyDel3();//没有参数,没有返回值的委托

        public void Fun()
        {
            MyDel1 myDel0 = delegate (int x,int y) { return x + y; };//匿名方法

            MyDel1 mydel1 = (int x, int y) => { return x + y; };//有两个参数的Lambda表达式
            MyDel2 myDel2 = (val) => { return val * 2; };//如果只有一个参数,那么可以不写参数的类型
            MyDel2 myDel3 = val => { return val * 2; };//甚至括号都可以省略
            MyDel2 myDel4 = val => val * 2;//如果出了返回一个值,没有其他语句,可以去掉大括号和return。
            MyDel3 myDel5 = () => { };//委托不需要返回值,也不需要参数,如果没有要执行的方法体,可以简化到这样
        }
    }

——事件

事件其实就是使用委托,只不过事件是专门用于某种特殊用途的委托。也就是发布者/订阅者订阅者模式。
发布者定义一系列的程序,订阅者在发布者中注册(也就是往发布者的委托中加入方法),以便在发布者发生事件时通知订阅者(调用事件委托)。
事件对委托做了一些限制,
1、事件将内部委托私有化,你无法在其他类中访问委托
2、事件的操作非常少,只有添加,移除,调用。
3、事件触发时,让内部委托依次调用方法列表中的方法。
4、事件是直接调用使用的,不可以实例化对象。
事件和委托其实就是下面这种结构



一个普通的事件的使用:

    class Base
    {
        public delegate void Del();//定义事件用到的委托
        public event Del Eve;      //定义事件

        public void Fun()
        {
            Eve();                //调用事件
        }
    }
    class Other
    {
        Base b = new Base();
        public void Method()
        {
            b.Eve += Fun1;        //在事件中注册
        }
        public void Fun1()
        {
            Console.WriteLine("this is Fun1");
        }
    }

——接口

什么时候是必须用到接口,而单纯靠继承是无法完成的呢?
比如下面的代码,我们无法让PrintInfo的参数可以接收任意类。

    class Ca
    {
        public void Fun()
        {
            Console.WriteLine("this is ca");
        }
    }
    class Cb
    {
        public void Fun()
        {
            Console.WriteLine("this is cb");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Ca ca = new Ca();
            Cb cb = new Cb();
            PrintInfo(ca);//打印 tihs is ca
            PrintInfo(cb);//错误
            Console.ReadLine();
        }
        static void PrintInfo(Ca ca)
        {
            ca.Fun();
        }
    }

这个时候我们就可以使用接口,让两个类都实现这个接口,而PrintInfo函数的参数就是这个接口类型,这时,由于两个类都实现了IFun接口,所以它们都可以作为参数传递进去

    interface IFun
    {
        void Fun();
    }
    class Ca : IFun
    {
        public void Fun()
        {
            Console.WriteLine("this is ca");
        }
    }
    class Cb : IFun
    {
        public void Fun()
        {
            Console.WriteLine("this is cb");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Ca ca = new Ca();
            Cb cb = new Cb();
            PrintInfo(ca);//打印this is ca
            PrintInfo(cb);//打印this is cb
            Console.ReadLine();
        }
        static void PrintInfo(IFun c)
        {
            c.Fun();
        }
    }
关于Array.Sort();方法

它其实不是可以排序int类型,而是可以排序继承自IComparable接口的类型,在这个接口中包含唯一一个方法CompareTo(),这个方法规定,在调用它时,需要返回一下几个值:
负数值:当前对象小于用于比较的参数对象
正数值:当前对象大于用于比较的参数对象
0:当前对象等与用于比较的参数对象
因为int实现了IComparable接口的CompareTo()函数,所以int类型可以排序,而Ca没有实现这个接口,所以不能排序

    class Ca
    {
        public int value = 0;
    }

    class Program
    {
        static void Main(string[] args)
        {
            //排序一个int类型数组
            int[] arrs = new int[] { 5, 1, 3, 2, 4 };
            Array.Sort(arrs);
            foreach (var item in arrs)
            {
                Console.Write(item + " ");
            }

            //排序Ca类型数组
            Ca[] cas = new Ca[5];
            for (int i = 0; i < cas.Length; i++)
            {
                cas[i] = new Ca();
                cas[i].value = i + 1;
            }
            Array.Sort(cas);//这段可以正常写,但是运行会报错
            foreach (var item in cas)
            {
                Console.Write(item.value + " ");
            }
            Console.ReadLine();
        }
    }

所以需要让Ca类实现IComparable接口,就可以排序了。

    class Ca : IComparable
    {
        public int value = 0;

        public int CompareTo(object obj)
        {
            Ca ca = (Ca)obj;
            if (value < ca.value)
            {
                return -1;
            }
            if (value > ca.value)
            {
                return 1;
            }
            return 0;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            //排序Ca类型数组
            Ca[] cas = new Ca[5];
            for (int i = 0; i < cas.Length; i++)
            {
                cas[i] = new Ca();
                cas[i].value = i + 1;
            }
            Array.Sort(cas);//打印1 2 3 4 5
            foreach (var item in cas)
            {
                Console.Write(item.value + " ");
            }
            Console.ReadLine();
        }
    }
接口的成员

接口的成员只能是非静态的函数成员,如方法,属性,索引器,事件。

接口的实现

类和结构体都可以实现接口

接口是引用类型

接口是引用类型,它虽然不可以实例化,但是可以用接口声明引用,实现类去实例,就像父类引用,子类实例一样,代码如下:

    interface IFun
    {
        void Fun();
    }
    class Ca : IFun
    {
        public void Fun()
        {
            Console.WriteLine("this is fun");
        }
    }
    class Cb : Ca
    {

    }

    class Program
    {
        static void Main(string[] args)
        {
            Cb cb = new Cb();//子类对象
            cb.Fun();
            Ca ca = cb;//父类引用,子类实例
            ca.Fun();
            IFun ifun = ca;//接口引用,实现类实例
            ifun.Fun();

            Console.ReadLine();
        }
    }

在堆中的形式如下:


接口使用 as 运算符

如果类没有实现接口而去强转为接口,那么会报错,这时,可以使用 as 运算符强转,它可以让无法发生的强转返回null,而不是报错,代码如下:

    interface IFun
    {
        void Fun();
    }
    class Ca
    {
        public void Fun()
        {
            Console.WriteLine("this is fun");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Ca ca = new Ca();
            IFun ifun = ca;//报错,因为Ca类没有实现该接口
            IFun ifun = ca as IFun;//返回null而不是异常
            ifun.Fun();

            Console.ReadLine();
        }
    }
多个接口的函数成员签名相同

如果一个类实现多个接口,并且多个接口定义的函数成员签名相同,那么类可以实现单个函数满足所有接口

   interface IFun1//接口1
    {
        string Fun(string s);
    }
    interface IFun2//接口2
    {
        string Fun(string s);
    }
    class Ca : IFun1,IFun2//实现接口1和接口2
    {
        public string Fun(string s)//实现一个函数满足两个接口
        {
            return s;
        }
    }
显示实现接口成员

如果类继承了两个函数成员签名一样的接口,但是这两个接口的函数成员实现不一样的功能,那么需要分别实现,可以使用显示实现接口成员的方式,但是要注意,显示实现接口成员只能私有,不能添加访问修饰符,并且调用也只能通过接口引用才可以调用,如下例子:

    interface IFun1//接口1
    {
        string Fun(string s);
    }
    interface IFun2//接口2
    {
        string Fun(string s);
    }
    class Ca : IFun1, IFun2//实现接口1和接口2
    {
        string IFun1.Fun(string s)
        {
            return "this is IFun1";
        }
        string IFun2.Fun(string s)
        {
            return "this is IFun2";
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            IFun1 fun1 = new Ca();
            fun1.Fun("Fun1");//只能通过接口引用调用
            IFun2 fun2 = new Ca();
            fun2.Fun("Fun2");//只能通过接口引用调用

            Console.ReadLine();
        }
    }
接口的继承
    interface IFun
    {
        void Fun();
    }
    class Ca//没有继承接口,但是有满足接口的函数
    {
        public void Fun()
        {
            Console.WriteLine("this is fun");
        }
    }
    class Cb : Ca, IFun//继承Ca,又继承接口,间接使用Ca的Fun函数实现接口
    {
    }
    class Cc : Cb//继承Cb
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
            IFun ifun = new Cc();//使用Cc实例接口的引用
            ifun.Fun();
            Console.ReadLine();
        }
    }
接口可以继承其他接口

直接看例子就能明白

    interface IFun1//接口1
    {
        string Fun(string s);
    }
    interface IFun2//接口2
    {
        string Fun(string s);
    }
    interface IFun : IFun1, IFun2
    {

    }
    class Ca : IFun//实现接口1和接口2
    {
        string IFun1.Fun(string s)
        {
            return "this is IFun1";
        }
        string IFun2.Fun(string s)
        {
            return "this is IFun2";
        }
    }

——转换

checked和unchecked

checked和unchecked可以用来检测溢出。
如果用于表达式,那么溢出时,checked会抛出异常,unchecked会继续执行。

            int i = 100000;
            byte b = unchecked((byte)i);//打印b
            byte c = checked((byte)i);//抛出异常

也可以用作语句,如果用于语句,效果也是一样的

            int i = 100000;
            checked
            {
                byte b = (byte)i;//抛出异常
            }
            unchecked
            {
                byte b = (byte)i;//继续执行
            }
浮点型转换为整形

浮点型会舍去小数部分

double转float

flaot占32位,double占64位,double 类型的值会舍入到最接近float类型的值。之后,如果值太小导致无法转为float表示,那么值会被设置为正或负0.。如果值太大导致无法转为float表示,那么值会被设置为无穷大或负无穷大。

关于引用类型的转换

引用类型的转换,其实是改变引用指向堆中的内存。



显示的引用类型转换

之前说过,可以用父类声明,子类构造,但不可以子类声明,父类构造。之前解释是会报错,其实是抛出InvalidCastException异常,不会导致编译错误,需要特别注意。有两种情况是可以子类声明,父类构造的
1、父类对象为空

    class A
    {
    }
    class B : A
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            A a = null;
            B b = (B)a;

            Console.ReadLine();
        }
    }

2、父类已经是子类对象的引用

    class A
    {
    }
    class B : A
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            A a = b;
            B b2 = (B)a;

            Console.ReadLine();
        }
    }
装箱

值类型默认在堆上不包括它们的对象组件,然而,如果需要对象组件,我们可以使用装箱。装箱是一种隐式转换,他接受值类型的值,根据这个值在堆上的创建一个完整的引用类型对象,并返回对象引用。也就是说,如果想把值类型转为引用类型,可以通过装箱。



如上面的例子,装箱一个值类型。
上述例子存在一个知识点,装箱并不是在被装箱的项上发生了操作,而是对值的副本进行操作,装箱之后会有原值和引用类型副本两个,都可以独立操作。

拆箱

拆箱是将引用类型转换为值类型显示转换,在拆箱时,系统会进行以下操作:
检测要拆箱的对象实际的装箱值。
把对象的值复制到变量里。
因此,拆箱后的变量和拆箱前的引用对象也都是独立的。如果拆箱为一个非它的原始类型,会抛出InvalidCastException异常。

自定义转换

之前介绍过自定义转换,这里再说几点约束:
1、只能为类或结构体定义自定义转换,不能重新定义标准的隐式或显示转换
2、只能定义不同类型的转换,不能是继承关系。

is运算符和as运算符

is运算符可以在转换之前判断是否可以转换,返回true或false。

    class A
    {
    }
    class B : A
    {
    }
    class Program
    {
        static void Main(string[] args)
        {
            B b = new B();
            A a = null;
            if (a is B)//判断是否可以转换
            {
                a = b;
            }

            Console.ReadLine();
        }
    }

as运算符进行强制转换,即使无法强制转换也不会抛出异常,只会返回null。

——泛型

泛型的约束

使用where关键字
where T : struct 那么T只能是指类型
where T : class 那么T只能是引用类型(比如类,数组,委托,接口)
where T : 类型 那么T只能是这个类,或者是派生这类类
where T : 接口名 那么T只能是这个接口,或者是实现这个接口的类型
where T : class,new() 那么T必须包含公共的无参的构造函数,不然就会报错

——枚举器与迭代器

对于数组可以使用forea遍历,是因为数组是一个可枚举类型,可枚举类型通过枚举器访问每一个项。
IEnumerator(枚举器接口)

枚举器必须实现该接口,该接口包括3个重要的函数成员:
1、Current:它是一个属性,是只读的,用来返回当前索引的项,返回值是一个object,所以可以返回所有类型。
2、MoveNext:它是一个方法,用来把当前索引移到下一个索引位置。返回一个bool值。
3、Reset:它是一个方法,用来把位置重置为原始状态的方法。


IEnumerable(可枚举类型)

可枚举类型接口有一个函数,就是GetEnumerator(),它返回一个IEnumerator,也即是枚举器,foreach其实可以理解给我就是调用可枚举类型中的GetEnumerator()获取枚举器,然后通过这个枚举器调用MoveNext和Current来遍历所有项。



下面演示一个简单的的枚举器和可枚举类型:

    //枚举器
    class MyColor : IEnumerator
    {
        string[] color;
        int index = -1;

        public MyColor(string[] color)
        {
            this.color = new string[color.Length];
            for (int i = 0; i < color.Length; i++)
                this.color[i] = color[i];  
        }
        public object Current
        {
            get
            {
                if (index == -1)
                    throw new InvalidOperationException();
                else if (index > color.Length - 1)
                    throw new InvalidOperationException();
                return color[index];
            }
        }

        public bool MoveNext()
        {
            if (index < color.Length - 1)
            {
                index++;
                return true;
            }
            return false;
        }

        public void Reset()
        {
            index = -1;
        }
    }
    //Spectrum类定义为可枚举类型
    class Spectrum : IEnumerable
    {
        string[] color = { "White", "Red", "Green", "Pink" };
        //使用MyColor枚举器枚举
        public IEnumerator GetEnumerator()
        {
            return new MyColor(color);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Spectrum spe = new Spectrum();
            foreach (var item in spe)//foreach遍历
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }
    }

上述代码Spectrum类为可枚举类型,它使用MyColor枚举器进行枚举,所以它可以被foreach遍历。
其实主函数的foreach就相当于如下代码:

    class Program
    {
        static void Main(string[] args)
        {
            Spectrum spe = new Spectrum();

            IEnumerator enumerator = spe.GetEnumerator();//获取枚举器

            while (enumerator.MoveNext())//索引写一个
            {
                object obj = enumerator.Current;//当前索引的值
                string str = (string)obj;
                Console.WriteLine(str);
            }
            enumerator.Reset();//恢复


            Console.ReadLine();
        }
    }
迭代器

迭代器其实就是对枚举器的简化,使用迭代器,程序在编译时自动为我们创建枚举器,枚举器用到 yield return 或者 yield break
1、迭代器返回枚举器

class MyClass : IEnumerable
    {
        public IEnumerator GetEnumerator()//在GetEnumerator方法中,返回枚举器
        {
            return GetColor();//返回GetColor迭代器返回的枚举器
        }
        public IEnumerator GetColor()//返回枚举器
        {
            yield return "Red";
            yield return "Black";
            yield return "Green";
            yield return "Pink";
            yield return "Yellow";
        }
    }
class Program
    {
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            foreach (var item in mc)
            {
                Console.Write(item + " ");
            }
            Console.ReadLine();
        }
    }

2、迭代器返回可枚举类型

    class MyClass : IEnumerable
    {
        public IEnumerator GetEnumerator()//在GetEnumerator方法中,返回枚举器
        {
            return GetColor().GetEnumerator();//通过调用GetColor返回的IEnumerable(可枚举类型)的GetEnumerator方法,返回枚举器
        }
        public IEnumerable GetColor()//返回可枚举类型
        {
            yield return "Red";
            yield return "Black";
            yield return "Green";
            yield return "Pink";
            yield return "Yellow";
        }
    }
class Program
    {
        static void Main(string[] args)
        {
            MyClass mc = new MyClass();
            foreach (var item in mc)//直接调用MyClass中的GetEnumerator方法迭代
            {
                Console.Write(item + " ");
            }
            Console.WriteLine();
            foreach (var item in mc.GetColor())//调用MyClass中的GetColor方法返回的IEnumerable中的GetEnumerator方法迭代
            {
                Console.Write(item + " ");
            }
            Console.ReadLine();
        }
    }
迭代器作为属性
 class MyClass : IEnumerable
    {
        bool b;
        public MyClass(bool b)
        {
            this.b = b;
        }
        public IEnumerator GetEnumerator()
        {
            return b ? GetColor1.GetEnumerator() : GetColor2.GetEnumerator();//通过b的位true或者false判断调用哪个迭代器属性
        }
        private IEnumerable GetColor1//迭代器属性1
        {
            get
            {
                yield return "Red";
                yield return "Black";
                yield return "Green";
                yield return "Pink";
                yield return "Yellow";
            }
        }
        private IEnumerable GetColor2//迭代器属性2
        {
            get
            {
                yield return "Yellow";
                yield return "Pink";
                yield return "Green";
                yield return "Black";
                yield return "Red";
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            MyClass mc = new MyClass(true);
            MyClass mc2 = new MyClass(false);
            foreach (var item in mc)
            {
                Console.Write(item + " ");
            }
            Console.WriteLine();
            foreach (var item in mc2)
            {
                Console.Write(item + " ");
            }

            Console.ReadLine();
        }
    }
迭代器的实质

1、使用迭代器,需要引入System.Collections.Generic命名空间。
2、迭代器没有实现枚举器中的Resset方法,因此接口调用该方法时会抛出异常。
3、在后台,编译器产生的枚举器是包含4个状态的状态机
Befor:首次调用MoveNext的初始状态
Running:调用MoveNext会进入这个状态,在这个状态中,枚举器检测并设置下一项的位置。在遇到yield return或者yield break或迭代块结束时,退出状态。
Suspende状态机等待下次调用MoveNext状态
After:没有更多项可以枚举。


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