2017.11.01 带你走近.NET Framework

分享人:傅云
特邀嘉宾: 周振涛

原文出处: 链接:https://bbs.kafan.cn/thread-2073476-1-1.html

1. 前言

经常有人这样问,“电脑中的.NET Framework是什么,可以卸载吗?”,“.NET Framework会拖慢系统速度吗?”,“有.NET4.5需要安装.NET3.5吗?”。

如果你是一个.NET编程新手(尤其是没有任何编程基础的),初次看到.NET的诸多术语,如CLR,CIL,JIT,程序集等概念,可能会觉得很混乱,而刚入门去理解MSDN比较困难。

本文将尽力以浅显易懂的方式整理.NET Framework相关内容和技术的关系。

楼主不是程序媛,也是小白级别的,内容仅供参考,若有错误欢迎讨论。希望本文能帮到大家。

2. .NET Framework概述

image.png

.NET(读作“DotNet”) Framework是Microsoft为开发和运行程序而建立的一个平台(应用程序包括桌面应用程序,Windows Store应用程序,Web应用程序等)。我们平常所说的.NET指的就是.NET Frakework。但.NET Framework其实是微软.NET战略目前最重要的实现而已。

.NET 是具有多种实现(.NET Framework、Mono、Unity .NET Core)的 ECMA 标准。

Microsoft力求实现.NET的平台无关性(“无关性”指在.NET上开发的的程序可以跨平台运行。“平台”指的是各个系统环境,如Linux)。本文只介绍.NET Framework,它相对最重要也使用最广泛。

应用程序总是要转为二进制代码才能运行。在有.NET平台之前,Windows程序是一般是直接运行在Windows上的,这种应用程序内包含的是本机代码(二进制代码),而之后,.NET应用中包含的是一种CPU不能直接解析的中间代码,只有.NET Framework中的JIT(及时编译器,下文介绍)才可以把它实时的编译成本机代码运行。所以下图这种弹窗已经可以简单解释了,运行.NET应用需要.NET执行环境。

.NET Framework主要有两个组件,一个是.NET Framework类库,另一个是CLR(公共语言运行时)。(暂时不用理解,下文介绍)

3. .NET类库

在编写程序中,一些经常使用的功能,如最简单的求三角函数值,复杂一点的如创建一个窗体等,如果让程序员编写来实现这些是很麻烦的。比如计算6/π的正弦值,(现在只需注意红色部分):

image.png

红色部分即调用了.NET 类库中一个求正弦值的函数,如果类库中没有这个函数计算三角函数呢?至少实现求任意弧度的三角函数值对我来说是很复杂的事情。

当然如果.NET类库只包含这些简单的功能,也没什么大用。.NET Framework类库叫Framework Class Library(FCL),它封装了很多高级的功能,比如我们常见的Windows窗体的代码,WPF,ASP.NET等。而Base Class Library(BCL)是FCL的一个子集,它包含了一些十分基础的东西,如一些类型,向屏幕上输出一句话,计算某个数的绝对值等。

如果不调用FCL中的一些封装的功能,同样会让开发时间变得很长。如果想在程序中弹出一个消息框,(如右图)

image.png

只需要输入System.Windows.Forms.MessageBox.Show("Hello , C# World!","Howdy","MessageBoxButtons.OK",MessageBoxIcon.Information);即可。

如果FCL中没有这个功能,那就需要调用系统相关Windows API,显然比较麻烦。而下面的这个示例只是最简单的调用。

using System;
using System.Runtime.InteropServices;
class Program
{
       [DllImport("User32.dll")]
       public static extern int MessageBox(int a, string b, string c, int type);
       static void Main()
       {
            MessageBox(0, "Hello , C# World !", "Howdy", 4);
            Console.ReadKey(true);
       }
}

什么是Windows API(这里给出一种简单的理解,不十分准确):Windows提供了很多操控Windows本身的接口,编程人员需要通过这些接口使用系统的一些功能,这些接口就是API

读到这里,有些童鞋可能要问了,库很好理解,“类”是什么意思?按照类库的一般定义:

类库(Class Library)是一个综合性的面向对象的可重用类型集合,这些类型包括:接口、抽象类和具体类。

下面简要介绍仅“面向对象”,不介绍接口和抽象类,这样“类”就很好理解了。(不懂编程也可以看)

*为什么要讲面向对象?因为.NET Framework类库中大都遵循“面向对象”设计,不然你只会简单的认为它是个“库”。以及为后文做铺垫。

在生活中,每一个实物都可以看作一个“对象”,而将某一种对象的特征(包括行为特征和静态特征)提取出来,形成一个“模板”,这个“模板”就是我们所说的“类”,“类”仅仅是描述了“对象”的特征,并不实际存在。如果“人类”是一个模板,那么身高,体重,性别就可以看做是模板中定义的静态特征,而“走路”是动态特征。依照这个模板有一个人,身高为“165”,体重为“55kg”,性别为“女“,此时这个人就是一个对象,就可以使她的动态特征(走路)表现出来。在面向对象的术语中,“动态特征”叫做“方法”。要点:“类”是抽象模板,对象是实体,对象能做的事叫“方法”。

这里的“模板”指的是生活中的“模板”,不是C++中的“模板”。

在.NET类库中,很多功能被封装成“类”的形式,只需要将“类”实例化称对象,就可以让“对象”做一些事情。面向对象的程序设计,就是让程序由多个“对象”组成,并让这些对象互相交互。如下所示,.NET Framework类库中提供了一个FileSystemWatcher类(用以监控文件及文件夹的变化),将其实例化成“对象”并监控指定的目录(这里是C盘),目前并不需要看懂它,这里的目的是为了让读者了解.NET Framework类库中类的作用。


[mw_shl_code=xml,true]using System;
using System.IO;
namespace test
{
class Program
{
        static void Main(string[] args)
        {
           Program.WatcherStart(@"C:\", "*.txt");
            //监控C盘
           Console.ReadKey();
        }
        private static void WatcherStart(string path, string filter)
        {
            FileSystemWatcher watcher = new FileSystemWatcher();//将.NET类库中的FileSystemWatcher类实例化
            watcher.Path = path;
            watcher.Filter = filter;//将类中某些“抽象特征”赋予真实的值。
            watcher.Changed += new FileSystemEventHandler(OnProcess);
            watcher.Created += new FileSystemEventHandler(OnProcess);
            watcher.Deleted += new FileSystemEventHandler(OnProcess);
            watcher.Renamed += new RenamedEventHandler(OnRenamed);
            watcher.EnableRaisingEvents = true;
        }
        private static void OnProcess(object source, FileSystemEventArgs e)
        { 
            if (e.ChangeType == WatcherChangeTypes.Created)
            {
                OnCreated(source, e);
            }
            else if (e.ChangeType == WatcherChangeTypes.Changed)
            {
                OnChanged(source, e);
            }
            else if (e.ChangeType == WatcherChangeTypes.Deleted)
            {
                OnDeleted(source, e);
             }
        }
         private static void OnCreated(object source, FileSystemEventArgs e)
        {
            Console.WriteLine(e.FullPath+"被创建");             
        }
        private static void OnChanged(object source, FileSystemEventArgs e)
        {
             Console.WriteLine(e.FullPath+"发生变化");
        }
        private static void OnDeleted(object source, FileSystemEventArgs e)
        {
            Console.WriteLine(e.FullPath+"被删除");
        }
        private static void OnRenamed(object source, RenamedEventArgs e)
        {
            Console.WriteLine(e.FullPath+"被重命名");
        }
    }
}
[/mw_shl_code]

看到这里,应该感觉到.NET类库的强大了,监控一个文件夹需要调动很多Windows API,而现在.NET Framework类库提供了一个模板,即“类”,通过实例化FileSystemWatcher这个类产生一个对象就可以调用这个对象的“方法”监控文件夹了,至于这个对象内部构造并不清楚,就像你让我走路,你肯定不会去考虑我身体内三磷酸腺苷与二磷酸腺苷之间的转换之类的东西,直接让我这个“对象”去走路即可。(这里多嘴一句,这个监控功能满足一般需要,像杀软那样严密的监控不是这样来的)。在编程中,你可以选取不同的类,将它们实例化成“对象”。编写出强大的程序。

这2点作了解:

  1. 由于程序是由各个对象组成,因此维护和纠错变得比较容易,在编写程序前只需要考虑程序由哪些对象构成,处理对象之间交互的逻辑问题即可。

  2. “不清楚内部构造”体现了面向对象的“封装性”,.NET类库中的很多类也是如此,并不能直接看出它是怎么实现相关功能的。

4. .NET程序的编译和运行

4.1 CIL

CIL(Common Intermediate Language),即通用中间语言,和MSIL(Microsoft Intermediate Language ),IL(Intermediate Language )都是一回事,不要被它们搅昏了,只是在.NET的测试版中被称为MSIL,后来官方改称CIL,但很多人都习惯叫它MSIL。

一个.NET程序被人用代码写出来后,不是被编译器“翻译”成计算机能识别的二进制指令,而是形成CIL。写一个简单的“Hello World!"小程序如下,这些代码是人写出来的,需要编译器把它翻译成CIL:

[mw_shl_code=xml,true]
using System;
class Program
{
        static void Main(string[] args)
        {
            string hello = "Hello World";
            Console.WriteLine(hello);
            Console.Read();
        }
}[/mw_shl_code]

然后编译器翻译成这样(看上去有点汇编的风格):

image.png

为什么要翻译成中间语言而不是直接二进制代码呢,这样不是多此一举吗?并不是。

二进制代码与平台有关,在这个平台上可以执行,在另一个上面不一定,不是一份代码直接拷来拷去到处都可以执行。而理论上讲,CIL因为与.NET平台相关联,是由.NET Framework中的JIT进行编译成适合.NET Framework所在框架的平台的二进制代码,因此一份代码可以“一次编译,到处使用”,减少了程序员的负担,平台兼不兼容.NET程序,取决于.NET Framework与平台的兼容性,这不该由程序员考虑。但就如上文所说,.NET Framework主要还是运行在Windows平台之上,针对其他平台的.NET Framework还未成熟。

如果没有.NET Framework,程序员为了让程序在不同平台运行,需要这样做:

image.png

有了.NET Framework,可以简化开发(按照理论来讲,实际中并不是):

image.png

是什么能让编译器将高级语言转为CIL呢?当然是程序员需要使用.NET Framework支持的语言了,比如C#,F#等(后文简单介绍),这些语言的编译器可以把人写的语句转为CIL。(当然你愿意直接写CIL代码也行,但这样很容易出错,而且大多数情况下根本不需要)

4.2 及时编译器JIT

再次强调,CIL代码不能直接执行!这时需要.NET Framework中的JIT编译器发挥作用了。运行一个.NET程序后,JIT编译器会立刻翻译一些需要的代码,而不是把所有代码一次性都翻译成本机代码,在运行时也是这样。(这也是为什么叫它及时编译器)

在Windows平台中,CLR(后文介绍CLR)带有三个不同的JIT编译器:

  1. 缺省的编译器---主编译器,由它进行数据流分析并输出经过优化的本地代码,所有的中间代码指令均可被它处理。
  2. PREJIT,它建立在主JIT编译器之上。每一个.NET组件安装时它开始运行。
  3. ECONOJIT,在并不充分优化代码的前提下,它能够快速完成IL代码到本地码的转换,编译速度与运行速度都非常快。

什么是代码优化?比如你有一个翻译帮助你和英国人说话,你说“今天......那个......那个.......我没有......吃饭”,你的翻译如果没有“优化”你的话,就会说成“Today...Err...Err...I did not... eat."虽然意思是表达清楚了,但一点也不好听还浪费时间,如果翻译“优化”你的话,就会说成“I did not eat today!",这样就清楚多了。

在ECONOJIT中,它是逐条地将CIL代码转译为本机代码,速度是很快,但代码并不会怎么优化。

举个简单的优化代码的例子,但JIT的优化规则很复杂,怎么优化实际上我并不清楚,以下只是个示例(选看):

[mw_shl_code=css,true]public void GetResult(out int properNumber, out int scoreSum)
    {
        try
        {
            properNumber = 0;
            char[] chArray = this.readAnswers.ToCharArray();
            char[] chArray2 = standardAnswers.ToCharArray();
            for (int i = 0; (i < chArray.Length) && (i < chArray2.Length); i++)
            {
                properNumber = (chArray == chArray2) ? (properNumber + 1) : properNumber;
            }
            scoreSum = this.subjectiveQuestionScore + (properNumber * scorePerOneQuestion);
        }
        catch (NullReferenceException exp)
        {
            MessageBox.Show("未设置标准答案或分值", "输入错误", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            properNumber = 0;
            scoreSum = 0;
        }
    }
[/mw_shl_code]

实际上catch (NullReferenceException exp)中exp这个引用(在栈内存中)并没有(这里简单理解为,按照代码,应该分配一点内存,但实际上并没有,因为不需要),因为我没有exp使用这个变量。实际效果是catch (NullReferenceException)。改变catch子句为:

       [mw_shl_code=shell,true]catch (NullReferenceException exp)
        {
            System.Diagnostics.Trace.WriteLine(exp.ToString());
            MessageBox.Show("未设置标准答案或分值", "输入错误", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            properNumber = 0;
            scoreSum = 0;
        }
     [/mw_shl_code]

这样使用了exp这个变量。效果如catch (NullReferenceException exp)。

而像传统的C++编译器,是编译程序员写的代码时将其优化并转成本机代码,执行的时候,直接执行这个二进制代码。而使用JIT不同,它是用户要运行的时候才去实时的编译成本机代码,执行过一次的本机代码会缓存。有些童鞋可能会觉得这样可能影响性能,但是(仅是理论上)JIT将CIL编译成本机代码时是针对本机硬件的特性做了优化的,针对性更强,而传统的直接生成的本机代码为了考虑兼容性不得不使用基本的指令集而可能无法发挥硬件的性能,但是JIT将CIL转为本机代码可能会花费一点时间。但实际中来看,两者差别不是很大。

4.3 程序集

按照一般的定义,程序集是:

程序集(assembly)是包含编译好的、面向.NET Framework的代码的逻辑单元

它的意思就是,.NET程序的文件就叫做程序集,包括打包进去的图片,音乐等,都属于程序集的一部分,但它准确地讲是一个逻辑单元,动态程序集存储在内存中,而不是存储在文件中。比如我在程序中调用了一个kernel32.dll,显然不会把它打包到程序文件里去,但我使用了它,它就可算作程序集的一部分。一般Windows平台上的程序集由以下及部分组成:

  • [mw_shl_code=css,true]PE文件头
  • CLR(下文介绍)文件头
  • CIL代码
  • 元数据(metadata)
  • 程序集清单(manifest)
  • 可选的嵌入资源
  • [/mw_shl_code]

在Windows上的程序集后缀名一般是.exe或者.dll,但仅仅是后缀名和其他程序一样,其内部是不同的。看着个.NET程序,如果我不告诉你这是.NET程序,单凭后缀名是exe或dll无法判断。

image.png

当然其他平台的程序集一般不会有PE文件头。任何一个可直接或间接执行的文件,要想让Windows执行,必须告诉一声Winsows“我是可以被执行的”,然后由Windows再调用CLR来执行,而不会由CLR直接检测到你的双击文件而运行.NET程序。

CLR文件头,就表明这个文件需要CLR(去执行)。

程序集清单,其包含的信息是程序集内的各个部分之间的关系,还有上图属性中的语言,地区,版本号之类也在这里面。metadata暂不介绍。

嵌入资源,比如图片之类的就是这一种。

4.4 CLR

CLR,公共语言运行时,指的就是执行.NET程序的这个大环境,包含有JIT编译器,它负责.NET程序的内存管理(.NET程序是由CLR管理内存使用的),程序集(参见上文)加载,异常处理,线程同步(这两点本文不会介绍,超出了范围)等。(在CLR上执行的代码一般是托管代码,即上文一直提到的代码,但CLR可以执行一些非托管代码)。

知识回顾:.NET Framework由两大模块组成,其一是类库,另一个是CLR(运行环境)。

image.png

托管代码的一大特性就是内存由CLR管理,不可能手动显式摧毁一个“对象”(上文已经介绍“对象”),当对象不再使用时,.NET CLR的垃圾回收器会择机删除它。如下面的例子为了好说明,用new运算符初始化string对象,在.NET中,为对象分配的内存叫“堆”(选看):

using System;
class Test
{
    static void Main()
    {
    string myStr = new string();
    myStr = null;//去除引用
    }
}

即使myStr不再指向新的string对象,虽然这个对象肯定没有用了,但也不能保证马上被回收。如果加一句GC.Collect();,能够触发垃圾回收器,但垃圾回收器什么时候运行,运行时是否清理对象,均无法预判。更多的垃圾回收器的细节,这里不再多讲。

不能手动摧毁对象不代表不能释放对象占的非托管资源。

*

5.CLS和CTS

也许这两个概念刚开始很难搞清楚。这里简单的理清思路。

可以生成CIL代码的语言很多(即.NET平台上的语言),比如C#,F#,VB等,而.NET平台的目的之一就是语言的互操作性,不管是.NET上的什么语言,都被编译器生成CIL。如果各个语言之间的类型能够对应.NET中的类型,那么语言交互便很容易了。类型是编写程序中数据的类型。

比如在内存中申请一个地址,取名为i,给这个地址所代表的空间里“装”一个整数3,在VB的语法中是Dim i as Integer = 3 在C#中是int i = 3; VB语法中中的Integer和C#语法中的int同时映射到.NET类库中的整数类型System.Int32。

.NET库中定义了一套CTS(公共类型系统),各个语言互相交换数据时才能畅通无阻。 但是各个编程语言中不相同的可不只是类型,还有语法,有些语言中的特性在.NET Framework这个大环境中是没有定义的,该怎么办呢?

一般是3种方法:

  1. 改变语言本身(类似于.NET版本的语言,比如VB.NET):.NET版本的VB比到原来VB6上修改扩充了很多东西,才符合了规范。
  2. 增加语言中的特性,但原来不相容的部分不去处理它,比如C++,托管部分编译成托管代码,其他部分编译成非托管代码。
  3. 第三种主要靠比较厉害的编译器,如.NET CLR中没有定义多继承,(多继承:比如有三个“类”,一个是“床”,另一个是“沙发”,还有一个是“沙发床”,则“沙发床”可以看作是继承了“床”和“沙发”的特征,但.NET中不允许这样,只能是单继承,如“程序员”是“人”,“程序员”就继承了“人”),但是有些语言中有多继承这种语法,为了满足CTS,又不想放弃这个特征,只好利用.NET支持的特性“绕圈子”实现,Eiffel的编译器就是利用Interface以及Attribute来达到多重继承的目的。

看下面这个Venn图(Fortran也是一种语言)),可以清楚地看出CLS是什么:


CLS制定了一种以.NET平台为目标的语言所必须支持的最小特征,还有该语言与其他.NET平台的语言之间实现互操作所需要的特征。有了.NET,仍可以创建不能在不同语言中使用的组件,代码可以不满足CTS,但.NET平台语言的编译器必须支持CLS(因为它是最小子集了)比如某种.NET平台语言中没有定义无符号整数。

public uint Fuc()
{
return 1;
}

这种语言调用不了上面的函数,因为没有定义无符号整数。为了满足互操作性,最好让代码遵循CLS。

总的来说,CLS是CTS的子集,.NET平台的语言不完全支持CTS可以,但必须支持CLS。

6. .NET平台上的语言

支持.NET平台的语言(部分或完全支持)很多,如C#,VB.NET,F#,C++.net,Eiffel,COBOL等等,什么语言不重要,重要的是能不能由特定的编译器把他们转成CIL,高级语言毕竟只是人看的,但由于除C#,F#外这些语言大都出现在.NET平台之前,所以部分.NET平台的特性并不支持。C#,F#是微软开发的,自然,C#对于.NET的各种特性支持的最好(但不是全部都支持,有些特性只能由编写CIL实现。

7. .NET相关问题

  • 1. .NET Framework框架版本向下兼容吗?**

(利用不同.NET Framework生成的程序)

.NET框架不是一成不变的。.NET版本从1.0到现在的4.,其CLR版本号并不是1-4的,.NET4以上的CLR版本号是4.0,一般来说,在4.0以上,版本是向后兼容的(到4.0),.NET2.0~.NET3.5对应的CLR版本号是2.0,CLR没有3.这个版本号。这之间的.NET Framework生成的程序一般可以向后兼容到.NET2.0,.NET1.0~.NET1.1的程序已经十分老旧,这里不做讨论。CLR版本不变,主要更新的是.NET Framework的类库。

为了运行不同版本的.NET程序,可以安装多个版本的.NET Framework,一般并不需要考虑装哪个版本,如果一个程序需要其他版本的.NET Framework,会自动提示。.NET与某些版本的Windows集成,下表仅供参考:


  • 2. .NET会拖慢运行速度吗?

多半是百度知道上某些人的回答。.NET Framework的安装与开机时间和运行速度没有必要的联系。JIT编译器实时编译在某些老机器可能会慢一点,但现在很难感觉到。

而且软件的运行速度不仅取决于运行环境,还在于代码的质量。用非托管C++照样可以写出性能很差的程序。

  • 3. .NET Framework可以卸载吗?

系统集成的版本,多半卸载不了,只能把它关了,自己安装的其他版本确认没有程序依赖.NET Framework也可以删除,但不推荐。其他更多的问题,在看了本文的介绍后应该可以理解MSDN针对用户使用.NET Framework的问题的回答了。
地址:https://msdn.microsoft.com/zh-cn/library/w0x726c2(v=vs.110).aspx

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

推荐阅读更多精彩内容