C# Notizen 3 理解C#类和对象

类是一个重要的C#编程概念,它在一个单元内定义了表示和行为。类提供了面向对象编程和面向组件编程所需的语言支持,是创建用户定义的类型时使用的主要机制。传统上,在面向对象编程语言中,术语“类型”指的是行为;而在面向值的编程语言中,该术语指的是数据表示。在C#中,该术语指的是数据表示和行为。这是通用类型系统的基础,意味着当且仅当两种类型在表示和行为方面兼容时,它们在赋值方面才是兼容的。

一、面向对象编程
ps:易于维护的代码
当然,要创建出易于维护、理解和扩展,除确保模型正确外,还要做很多事。例如,还必须确保实现正确、易于理解、条理清晰。

类是重要的C#编程概念,它在一个单元内定义了表示和行为。换句话说,类是一种数据结构,融数据和操作数据的方法于一体。类不过是另一种数据类型,可以像使用预定义类型那样使用它们。类为创建用户定义的类型提供了主要机制。

面向对象编程的4个主要概念是封装、抽象、继承和多态。

封装让类能够隐藏内部实现细节,以免遭受不希望的修改,进而导致内部状态无效或不一致。因此,封装有时也被称为数据隐藏。

通过隐藏内部细节或数据,可以创建一个公有接口(抽象),它表示类的外部细节。这个接口描述了类可执行哪些操作以及类的哪些信息是公有的。只要公有接口不变,以任何方式改变内部细节都不会影响其他依赖于它的类和代码。

通过让类的公有接口较小,并让类与它要表示的实际对象极其相似,可确保它对其他需要使用它的程序员来说是熟悉的。

二、面向组件编程
面向组件编程是一种软件开发方法,它将现有的组件和新组件组合起来,就像将零件组装成汽车一样。软件组件是独立的自我描述功能包,其中包含暴露行为和数据的类型的定义。
C#通过属性、方法、事件和特性(元数据)等概念支持面向组件编程,让您能够创建自我描述的独立功能组件,这些功能组件称为程序集。

三、C#类
在 C#中,类是隐式地从 object 派生而来的引用类型。要定义类,可使用关键字 class。
类体(body)是在左大括号和右大括号内定义的,您在其中定义类的数据和行为。

四、作用域和声明空间
作用域是可使用某个名称的范围,而声明空间是名称是唯一的范围。作用域和声明空间紧密相连,但它们之间有一些细微的差别。

正规的定义是,作用域是一个封闭的区域,在其中无需通过限定就能使用某个名称。这意味着命名空间、类、方法和属性都是作用域和声明空间,因此作用域可彼此嵌套和重叠。
既然作用域定义了名称的可见性,且可以相互重叠,那么在外部作用域中定义的名称在内部作用域中是可见的,反之则不成立。

如下代码中,字段age的作用域为整个Contact类,包括F和G的函数体。在F的函数体中,age指的是字段age。

    class Contact
    {
        public int age;

        public void F()
        {
            age = 19;
        }

        public void G()
        {
            int age;
            age = 24;
        }
    }

在函数G内,作用域发生了重叠,因为有一个名为age的局部变量,其作用域为函数G的整个函数体。在函数G内,当您引用age时,引用的实际上是局部变量age,而不是在外部作用域中定义的字段age。在内部作用域内,在外部作用域内声明的同名实体被隐藏。
使用虚线框指出了作用域边界:

Paste_Image.png

另一方面,声明空间指的是这样一个封闭区域,即其中不能有两个同名的实体。例如,在Contact类中,不能再有两个名为age的实体,除非将其中一个放在函数F或G的函数体内。同样,在函数G内,不能再声明一个名为age的实体。
如果将所有同名的重载方法视为一个实体,则“在声明空间内名称必须唯一”这一规则仍适用。

4.1 访问性
访问性让您能够控制实体在其作用域外的可见性(访问级别)。在C#中,这是通过访问修饰符实现的,访问修饰符指定了在类的外部可如何访问其成员,有时甚至对继承进行了限制。允许访问的类成员是可访问的,而不允许访问的类成员是不可访问的。

这些访问修饰符遵循一组简单的规则,这些规则决定了访问级别。

  • 对于命名空间,不能指定访问修饰符,它们总是public(公有)。
  • 类的访问级别默认为internal(内部),但可将其声明为public或internal。嵌套类(在另一个类中定义的类)的访问级别默认为 private(私有),但可将其声明为 5 种访问级别中的任何一种。
  • 类成员的访问级别默认为private,但可将其声明为5种访问级别中的任何一种。

ps:显式地声明访问级别
虽然 C#提供了的默认访问修饰符是合理的,但您应始终显式地声明类成员的访问级别。这样可避免二义性,指出选择是有意做出的,还可起到自我描述的作用。

C#支持的访问修饰符:

Paste_Image.png

ps:protected internal
使用 protected internal时要小心,因为其实际效果要么是 protected,要么是internal,C#没有提供protected且internal的概念。

4.2 字段和常量
字段是这样的变量,即它表示与类相关联的数据。换句话说,字段是在类的最外层作用域内定义的变量。
对于这两种字段,都可使用5个访问修饰符中的任何一个。通常,字段是private的,这是默认设置。
如果声明字段(不管是实例字段还是静态字段)时没有指定初始值,就将根据其类型赋给相应的默认值。
与字段类似,声明常量时也可使用5个访问修饰符中的任何一个。常量必须有在编译阶段能够计算出来的值,因此必须在声明常量的同时赋值。常量必须有在编译阶段能够计算出来的值,这种要求的好处之一是常量可依赖于其他常量。
常量通常是值类型或字面字符串,因为除string外,要创建其他引用类型的非null值,唯一的方法是使用new运算符,但这是不允许的。

ps:常量应该是恒定不变的
创建常量时,应确保它从逻辑上说是恒定不变的。好的常量应永远不变,如Pi的值、Elvis的出生年份、1摩尔包含的分子数。

如果要创建行为类似于常量的字段,但其类型是常量声明中不允许的,可使用修饰符static和readonly将其声明为只读的静态字段。要初始化只读字段,要么在声明中进行,要么在构造函数中进行。

4.3 属性
鉴于字段表示状态和数据,但通常是私有的,必须有一种机制让类能够向外提供这些信息。知道各种访问级别后,可能想将字段的访问级别声明为public。
这样做可满足抽象规则,但违反了封装规则,因为这导致可从类外部直接操作字段。那么,如何才能同时满足封装规则和抽象规则呢?我们需要这样的东西:其访问语法与字段相同,但访问级别不同于字段。属性正好能够满足这种需求。属性提供了一种访问字段(支撑字段,backing field)的简单方式,它是公有的,同时让我们能够隐藏字段的内部细节。就像字段可以是静态的一样,属性也可以是静态的,这种属性不与特定的类实例相关联。
字段被声明为变量,因此需要占用内存空间,但属性不需要。属性是使用访问器声明的,访问器让您能够控制值是否可读写以及读写时将发生的情况。get访问器用于读取属性值,而set访问器用于写入值。

如下代码说明了声明属性的最简单方法,这种语法称为自动实现的属性(automatic property)。使用这种语法时,无需声明支撑字段,必须同时包括get和set访问器,但无需提供它们的实现,而由编译器提供。

    class Contact
    {
        public string FirstName
        {
            get;
            set;
        }
    }

事实上,对于上图所示的代码,编译器将把它们转换为类似于下图代码的形式

    class Contact
    {
        private string firstName;

        public string FirstName
        {
            get
            {
                return this.firstName;
            }
            set
            {
                this.firstName = value;
            }
        }
    }

ps:自动实现的属性
自动实现的属性很方便,尤其是在需要实现大量属性时。然而,这种方便也需要付出轻微的代价。
由于没有提供访问器,因此无法指定访问器的任何逻辑。另外,使用自动实现的属性语法时,必须声明两个访问器。如果以后发现需要给其中一个访问器指定逻辑,就必须添加支撑字段,并给两个访问器都提供合适的逻辑。
所幸的是,这种修改不会影响类的公有接口,因此可安全地进行,虽然修改起来可能有些繁琐。
get访问器使用一条return语句,该语句命令访问器返回指定的值。在上图中, set访问器将字段firstName设置为value的值, value是一个上下文关键字。用于属性的set访问器中时,关键字value总是意味着“调用者提供的值”,且其类型与属性的类型相同。

默认情况下,属性访问器继承属性定义指定的访问级别,但可为访问器get或set指定更严格的访问级别。
还可创建计算得到的属性(calculated property),这种属性是只读的,且没有支撑字段。计算得到的属性非常适合用于提供从其他信息派生而来的数据。

如下代码演示了一个名为 FullName 的计算得到的属性,它将字段 firstName 和lastName合并在一起。

    class Contact
    {
        private string firstName;
        private string lastName;

        public string FullName
        {
            get
            {
                return this.firstName + " " + this.lastName;
            }
        }
    }

ps:只读属性和只写属性
对于显式地声明的属性,可省略两个访问器之一。通过只提供get访问器,可创建只读属性;使用自动实现的属性时,要使其成为只读的,可将set访问器声明为private。
通过只提供set访问器或将get访问器声明为private,可创建只写属性。实际上,应避免创建只写属性。
由于属性访问起来就像是字段一样,因此在访问器中执行的操作应尽可能简单。如果需要执行复杂、耗时或昂贵(占用大量资源)的操作,最好使用方法而不是属性。

4.4 方法
如果说字段和属性定义并实现了数据,那么方法(也称为函数)就定义并实现了可执行的行为或动作。在本书前面的示例和练习中,您一直在使用Console类的WriteLine动作,它就是一个方法。
如下代码演示了如何在Contact类中添加一个方法,它验证电子邮件地址。在这里,方法VerifyEmailAddress的返回类型为void,这意味着它不返回值。

    class Contact
    {
        public void VerifyEmailAddress(string emailAddress);
    }

这个方法的返回类型被声明为bool。

如下代码声明一个返回值的方法

    class Contact
    {
        public bool VerifyEmailAddress(string emailAddress)
        {
            return true;
        }
    }

在方法声明中,可指定5个访问修饰符中的任何一个。除访问修饰符外,还可给方法指定修饰符 static。就像静态属性和静态字段不与类实例相关联一样,静态方法也是如此。在Console类中,方法WriteLine就是静态的。
方法可接受零或多个参数(输入),参数是使用形参列表(formal parameter list)声明的,该列表由一个或多个用逗号分隔的参数组成。对于每个参数,都必须指定其类型和标识符。如果方法不接受任何参数,就必须指定空参数列表。

参数分为3类,如下所示:

  • 值参数:这种参数最常见。调用方法时,对于每个值参数,都将隐式地创建一个局部变量,并将参数列表中相应参数的值赋给它。

ps:参数数组
参数数组是使用关键字params声明的,可将其视为特殊的值参数,它声明单个参数,但在参数列表中,它包含零或多个参数。
在方法的形参列表中,只能包含一个参数数组,且必须位于参数列表的末尾。参数数组也可以是方法的唯一一个参数。

  • 引用参数:不额外占用内存空间,而指向参数列表中相应参数的存储位置。引用参数是使用关键字ref声明的,在形参列表和实参列表中都必须使用该关键字。
  • 输出参数:类似于引用参数,但在形参列表和实参列表中都必须使用关键字 out。与引用参数不同的是,在方法返回前,必须给输出参数赋值。

要让方法对对象执行所需的动作,必须调用它。如果方法需要输入参数,就必须在实参列表中指定它们。如果方法提供输出值,那么这个值也可存储在变量中。
实参列表与形参列表之间通常存在一对一的关系,这意味着调用方法时,对于每个形参,都必须按正确的顺序提供类型合适的值。

ps:将方法作为输入
返回值的方法以及属性也可用作其他方法的输入,只要返回类型与参数类型兼容。这极大地提升了方法和属性的用途,能够将方法调用或属性串接起来,形成更复杂的行为。
在前面的示例中,有一个返回类型为void的方法VerifyEmailAddress,可这样调用它:
Contact c = new Contact();
c.VerifyEmailAddress("joe@example.com");
然而,对于返回类型为bool的方法VerifyEmailAddress,可这样调用它:
Contact c = new Contact();
bool result = c.VerifyEmailAddress("joe@example.com");
就像形参列表一样,调用不需要参数的方法时,也必须指定一个空列表。

方法重载
通常,在同一个声明空间内,不能有两个同名的实体,但重载方法除外。在同一个声明空间内,如果多个方法同名但签名(signature)不同,那么它们就是重载的。
方法签名由方法名以及形参的数量、类型和修饰符组成,必须与同一个类中声明的其他方法签名都不同;另外,方法也不能与类中声明的其他所有非方法实体同名。

ps:方法签名
返回类型并非方法签名的一部分,因此两个方法不能只有返回类型不同。
虽然形参列表是方法签名的一部分,但不能因为某个参数为ref或out就认为两个方法不同。判断方法签名是否相同时,不考虑参数的ref或out特性。
重载方法时,只有改变签名。更准确地说,只能改变参数的数量和类型。对于前面使用过的方法Console.WriteLine,它有19个重载版本供您选择。
在.NET Framework中,方法重载很常见。这让您能够像类用户提供单个方法,但用户与之交互时可提供不同的输入。编译器将根据输入决定使用哪个重载版本。

ps:利用不同的返回类型进行重载
您可能利用不同的返回类型进行重载,虽然这可能是合法的 C#代码,但是由于方法签名不包含返回类型,因此这可能导致混乱。为最大限度地减少混乱,应避免这样做。
在需要提供多种执行动作的方式时,方法重载很有用,但是可供选择的空间太大时,可能难以应付。
如下是一个方法重载示例:

public void Search(float latitude, float longitude)
{
    Search(latitude, longtitude, 10, "en-US");
}

public void Search(float latitude, float longitude, internal distance)
{
    Seach(latitude, longitude, distance, "en-US");
}

public void Search(float latitude, float longitude, internal distance, string culture)
{

}

可选参数和命名参数
可选参数让您能够在调用方法时省略相应的实参。只有值参数可以是可选的,所有可选参数都必须位于必不可少的参数后面,且位于参数数组前面。
要将参数声明为可选的,只需给它提供默认值。下述修改后的Search方法使用了可选参数。
public void Search(float latitude, float longitude, int distance = 10,
string culture = "en-US");
其中,参数latitude和longitude是必不可少的,而参数distance和culture都是可选的。使用的默认值与第一个重载的Search方法提供的值相同。
从前一节的Search方法重载可知,参数越多,需要提供的重载版本越多。在这里,只有几个重载版本,但是使用可选参数时只需要一个版本。在有些情况下,重载是唯一的办法,尤其是在参数没有合理的默认值时,但是很多情况下可使用可选参数达到同样的目的。

ps:可选参数和必不可少的参数
有默认值的参数为可选参数,没有默认值的参数为必不可少的参数。
集成非托管编程接口(如Office自动化API)时,可选参数很有用,这些接口在编写时考虑到了可选参数。在这些情况下,原始API可能接受大量的参数(有时多达30个),但大部分参数都有合理的默认值。
调用方法时,可以不显式地给可选参数提供实参,这将使用其默认值。然而,如果调用方法时给可选参数提供了实参,就将使用该实参而不是默认值。

可选参数的缺点:不能使用两个逗号来表示省略了实参
为解决这种问题,C#允许按名称传递实参,这能够显式地指定实参之间的关系以及实参对应的形参。
如下代码是使用实参的示例

Paste_Image.png

这些调用都是等价的。前3个调用只是显式地制定了每个参数。最后两个调用演示了如何在参数列表中省略实参,它们是等价的,只是其中一个混合使用了命名实参和位置实参

ps:不是按名称传递的实参称为位置实参(positional argument)。位置实参最常用。
通常在有可选参数时使用命名实参,但没有可选参数时也可使用命名实参。不同于可选参数,命名实参可用于值参数、引用参数和输出参数;还可将其用于参数数组,但必须显式地声明一个数组来存储值,如下所示:
Console.WriteLine(String.Concat(values: new string[] { "a", "b", "c" }));
正如您从Search方法看到的,通过显式地指出实参的名称,C#提供了另一种功能强大的方法,让你能够编写含义不言自明的代码。

五、实例化类
使用预定义类型时,只需声明变量并给它赋值,而要在程序中使用类,必须创建示例。
虽然直接使用关键字new创建对象,但在后台、虚拟执行系统将负责分配所需的内存,垃圾收集器将负责释放内存

要实例化类,可使用关键字new,如下所示:
Contact c = new Contact();

对于新创建的对象,必须制定初始状态,这意味着对于声明的每个字段,都必须显式地提供初始值,否则它将使用默认值
有时这种初始化足够了,但通常还不够。为在初始化阶段执行其它操作,C#提供了实例构造函数(有时简称构造函数),这是一个特殊的方法,每当创建实例时都自动执行。
构造函数与类同名,但不能返回值,这不同于返回void的方法。如果构造函数没有参数,就是默认构造函数。

ps:默认构造函数
每个类都必须至少有一个构造函数,但你并非总是需要编写它。如果没有提供任何构造函数,C#将创建一个默认构造函数。这个构造函数实际上什么也不是,但确实存在。
仅当没有提供任何构造函数时,编译器才会生成默认构造函数,这让你一不小心就会破坏类的公有接口:添加了接受参数的构造函数,却忘记显式地添加默认构造函数。因此,最好总是提供默认构造函数,而不是让编译器生成。
默认构造函数(以及其他任何构造函数)可使用任何访问修饰符,因为完全可以创建私有默认构造函数。如果要允许实例化类,同时要确保创建其对象时都提供某些信息,这将很有用。

如下列出了Contact类的默认构造函数

public class Contact
{
    public Contact()
    {

    }
}

就像可以重载常规方法一样,构造函数也可以重载。与常规方法一样,重载的构造函数的签名不能相同。

一些提供特殊构造函数的原因:

  • 使用默认构造函数创建的对象的初始状态不合理
  • 提供初始状态既方便又合理
  • 创建对象的开销可能很大,因此想确保对象的初始状态是正确的
  • 非公有构造函数可限制使用它来创建对象的权限

如下声明一个重载构造函数

public class Contact
{
    public Contact(string firstName, string lastName, DateTime dateOfBirth)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.dateOfBirth = dateOfBirth;
    }
}

如上重载构造函数中,将参数的值赋给了相应的私有字段

当类包含多个构造函数时,通常将它们串接(chain together)起来,但并非总是这样做。要串接构造函数,可使用包含关键字this的特殊语法、

ps:关键字this
关键字 this表示类的当前实例,它类似于Visual Basic关键字Me、F#标识符self、Python特性(attribute)self和Ruby中的self。
this关键字的常见用途如下:

  • 限定被相似名称隐藏的成员
  • 将对象作为参数传递给其他方法
  • 在构造函数中指定要调用哪个构造函数
  • 在扩展方法中指定要扩展的类型

由于静态成员与类相关联,而不与实例相关联,因此不能使用关键字this来引用它。
在声明一个重载构造函数的源码中,关键字 this 用于将类字段和参数区分开来,因为它们的名称相同。

如下代码使用了构造函数串接

public class Contact
{
    public Contact()
    {

    }

    public Contact(string firstName, string lastName, DateTime dateOfBirth)
        :this()
        {
            this.firstName = firstName;
            this.lastName = lastName;
            this.dateOfBirth = dateOfBirth;
        }
}

构造函数串接的优点之一是,可串接类中的任何构造函数,而不仅仅是默认构造函数。使用构造函数串接时,明白构造函数的执行顺序很重要。将沿构造函数链前行,直到到达串接的最后一个构造函数,然后沿链条从后往前执行构造函数。在下面所示的C类中,有3个构造函数,每个构造函数都串接到默认构造函数。
如下代码说明了串接构造函数的执行顺序

public class C
{
    string c1;
    string c2;
    int c3;

    public C()
    {
        Console.WriteLine ("Default constructor");
    }

    public C(int i, string p1) : this(p1)
    {
        Console.WriteLine (i);
    }

    public C(string p1) : this()
    {
        Console.WriteLine (p1);
    }
}

下图说明了使用第二个构造函数(它接受一个int参数和一个string参数)实例化对象时,构造函数的执行顺序。

Paste_Image.png

静态构造函数
在有些情况下,类可能要求特殊的初始化操作,这种操作最多执行一次:访问实例成员之前。
为此,C#提供了静态构造函数,其形式与默认构造函数相同,但是不使用访问修饰符,而是使用修饰符 static。由于静态构造函数初始化类,因此不能直接调用静态构造函数。
静态构造函数最多执行一次:首次创建实例或首次引用静态类成员时。

六、嵌套类
嵌套类(nested class)完全封装(嵌套)在另一个类的声明中。嵌套类提供了一种方便的方法,让外部类能够创建并使用其对象,但在外部类的外面不能访问它们。虽然嵌套类很方便,但也容易滥用,这可能导致类难以处理。
嵌套类的访问级别至少与包含它的类相同。例如,嵌套类为 public,而包含它的类为internal,则嵌套类的访问级别默认也为internal,只有所属程序集的成员能够访问它。然而,如果包含它的类为public,嵌套类遵循的访问级别规则将与非嵌套类相同。
在下述情况下应考虑将类实现为嵌套类:它本身没有意义,且从逻辑上说可包含在另一个类内或其成员需要访问另一个类的私有数据。嵌套类通常不应是公有的,因为它们仅供在包含它的类中使用。

七、分部类
分部类(partial class)能够将类声明分成多个部分—通常存储在多个文件中。分部类的实现方法与常规类完全相同,但在关键字class前面有关键字partial。使用分部类时,其所有部分都必须在编译阶段可用,且访问级别相同,这样才能组成完整的类。
代码生成工具(如Visual Studio的可视化设计器)大量地使用了分部类,该设计器为您生成类,用于表示你设计的可视化控件。机器生成的代码单独放在分部类的一个部分中,这样你可以修改分部类的另一部分,而不用担心重新生成机器生成的部分时,所做的修改会丢失。
在不涉及机器生成代码的情形下,也可使用分部类。声明大型类时,可受益于使用分部类,但有时候这意味着类的功能太多了,最好将其分成多个类。
ps:嵌套类和分部类
虽然C#不像Java那样要求每个类一个文件,但是这样做通常是有好处的。使用嵌套类时,除非包含它的类是分部类,否则根本无法实现每个类一个文件的目标。

八、静态类
到目前为止,你知道修饰符static可用于构造函数、字段、方法和属性;修饰符static也可用于类,这将定义静态类。静态类只能有一个构造函数且是静态的,因此不可能创建静态类的实例。有鉴于此,静态类通常包含实用程序或辅助方法(helper method),它们不需要类实例就能工作。

ps:隐式静态成员
静态类只能包含静态成员,但这些成员并不会自动变成静态的,必须显式地使用修饰符 static。然而,可将任何静态成员声明为 public、private或internal的。
扩展方法是常规的静态方法,但第一个参数包含修饰符this,该参数指定要扩展的类型,通常称为类型扩展参数。扩展方法必须在非嵌套、非泛型静态类中声明。
由于扩展方法不过是经过特殊标记的静态方法,因此它对被扩展的类型没有特殊访问权限,而只能通过该类型的公有接口访问其成员。另外,调用扩展方法时,需要使用更传统的方式—使用其全限定名。

ps:访问internal成员
如果扩展方法是在被扩展的类型所属的程序集中定义的,那么它也能够访问该类型的internal成员。
虽然扩展方法的签名可以与被扩展类型的实际方法相同,但是这样的扩展方法将不可见。解析方法时,编译器确保实际类方法优先于扩展方法,这样可禁止扩展方法改变标准类方法的行为,因为这种改变将导致无法预料(至少是意外)的行为。

九、对象初始值设定项
前面介绍了如何创建构造函数,为设置初始状态提供一种方便的方法。然而,与方法重载一样,需要设置的字段越多,可能需要提供的重载构造函数也越多。虽然构造函数支持可选参数,但是有时您想在创建对象实例时设置属性。
类提供了对象初始值设置项语法,能够在调用构造函数的同时设置公有字段或属性。这提供了极大的灵活性,可极大地减少需要提供的重载构造函数。

如下代码使用了对象初始值设定项

    Contact c1 = new Contact();
    c1.FisrtName = "Carl";
    c1.LastName = "Doenitz";
    c1.DateOfBirth = new DateTime(1992,08,15);
    Console.WriteLine(c1.ToString());

    Contact c2 = new Contact
    {
        FistName = "Carl",
        LastName = "Doenitz",
        DateOfBirth = new DateTime(1992,08,15);
    };

    Console.WriteLine(c2.ToString());

只要字段或属性之间不存在依存关系,对象初始值设定项就是一种简介的方法,可用于同时实例化和初始化对象。

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

推荐阅读更多精彩内容