第3章:对象基础

对象和类是本书的核心,并且自从PHP 5十多年前问世以来,它们就也奠定了PHP的核心。在本章中,我为进一步深入的研究奠定了基础通过检查PHP的核心面向对象功能来确定对象和设计。如果您不熟悉面向对象编程时,您应该仔细阅读本章
本章将涵盖以下主题:
• 类和对象:声明类和实例化对象
• 构造方法:自动设置对象
• 原始和类类型:为什么类型很重要
• 继承:为什么我们需要继承以及如何使用它
• 可见性:简化对象接口并保护您的方法和介入的属性

类和对象

理解面向对象编程的第一个障碍是奇妙而美妙的关系在类和对象之间。对于很多人来说,这种关系代表着启示,首先是面向对象的激动。因此,让我们不要无视基础知识

开始一个类class

通常用对象来描述类。这很有趣,因为对象经常被描述就班级而言。这种循环性会使面向对象编程的第一步变得困难。因为塑造对象的是类,所以我们应该先定义一个类

// listing 03.01
class ShopProduct
{
    // class body
}

尽管示例中的ShopProduct类还不是非常有用,但它已经是一个合法类。我有但是做得很重要。我定义了一个类型;也就是说,我创建了一个数据类别我可以在脚本中使用的 在学习本章时,它的功能应该更加清楚

开始一个对象(或两个)

如果一个类是用于生成对象的模板,则说明对象是已经结构化的数据根据类中定义的模板。据说一个对象是其类的实例。它是这种类型的由类定义
我将ShopProduct类用作生成ShopProduct对象的模型。为此,我需要新的操作员。new运算符与类名结合使用,如下所示

// listing 03.02
$product1 = new ShopProduct(); 
$product2 = new ShopProduct();

使用类名称作为唯一操作数调用new运算符,并返回该类的实例。在我们的示例中,它生成一个ShopProduct对象。
我已经使用ShopProduct类作为模板来生成两个ShopProduct对象。虽然他们在功能上相同(即为空),product1和 product2是同一类型的不同对象从单个类生成。
如果您仍然感到困惑,请尝试这种类比。可以将一类课程视为制造塑料鸭子的机器中的演员表。我们的目标是这台机器产生的鸭子。生成的事物的类型由从中压制的模具。鸭子在各个方面看起来都是相同的,但它们是不同的实体。其他换句话说,它们是同一类型的不同实例。鸭子甚至可能有自己的序列号证明自己的身份。在PHP脚本中创建的每个对象也被赋予其自己的唯一标识符。(注意标识符在对象的生命期内是唯一的;也就是说,PHP甚至在一个进程内也重复使用标识符。我可以通过打印product1和 product2对象来证明这一点

// listing 03.03
var_dump($product1);
var_dump($product2);

执行这些功能将产生以下输出:


object(ShopProduct)#1 (0) { } object(ShopProduct)#2 (0) { }


请注意,在php的古代版本(5.1版之前)中,可以直接打印对象。这投了 对象到包含对象的iD的字符串。从php 5.2起,该语言不再支持这种魔术,并且任何尝试将对象视为字符串的尝试现在都会导致错误,除非使用名为__toString()的方法在对象的类中定义。我将在本章后面介绍方法并在第4章介绍__toSting().

通过将对象传递给var_dump(),我提取了有用的信息,其中包括在井号后的每个对象的内部标识符
为了使这些对象更有趣,我可以修改ShopProduct类以支持特殊对象。数据字段称为属性

在类中设置属性

类可以定义称为属性的特殊变量。属性,也称为成员变量,持有数据可能因对象而异。因此,对于ShopProduct对象,您可能希望操纵例如标题和价格字段。
类中的属性看起来与标准变量相似,除了在声明属性时,您必须在属性变量之前添加可见性关键字。它可以是公共的,受保护的或私有的,并且可以确定可以访问属性的范围

注意范围是指变量具有含义的函数或类上下文(在同一上下文中引用) 方法的方法,我将在本章稍后介绍。因此函数中定义的变量存在于本地作用域,并且在函数范围外定义的变量存在于全局作用域中。根据经验,这是不可能的访问在比当前范围更本地的范围内定义的数据。因此,如果您在函数,您以后不能再从该函数外部访问它。物体比这更具渗透性,因为有些对象变量有时可以从其他上下文中访问。可以访问哪些变量以及正如您将看到的,它是由public,protected和private关键字确定的。

在本章的后面,我将返回这些关键字以及可见性问题。现在,我要声明使用public关键字的一些属性

// listing 03.04
class ShopProduct
{
  public $title = "default product";
  public $producerMainName = "main name"; 
  public $producerFirstName = "first name"; public $price = 0;
}

如您所见,我设置了四个属性,为每个属性分配一个默认值。我的任何物体现在将使用默认数据预填充来自ShopProduct类的实例化。中的公共关键字每个属性声明均确保我可以从对象上下文外部访问该属性。
您可以使用字符“->”(对象)逐个对象地访问属性变量运算符)以及对象变量和属性名称,例如

// listing 03.05
$product1 = new ShopProduct(); 
print $product1->title;

结果:default product
由于属性定义为公共属性,因此您可以像读取它们一样为它们分配值,替换类中设置的任何默认值:

// listing 03.06
$product1 = new ShopProduct(); 
$product2 = new ShopProduct(); 
$product1->title="My Antonia"; 
$product2->title="Catch 22";

通过在ShopProduct类中声明并设置$title属性,可以确保所有ShopProduct对象首次创建时具有此属性。这意味着使用此类的代码可以与ShopProduct对象一起使用基于该假设。因为我可以重置它,所以$title的值可能因对象而异。

注意使用类,函数或方法的代码通常被描述为类,函数或方法的代码 客户或作为客户代码。在接下来的章节中,您将经常看到该术语

实际上,PHP并不强迫我们在类中声明所有属性。您可以添加属性动态地对象,像这样

// listing 03.07 
$product1->arbitraryAddition = "treehouse";

但是,这种为对象分配属性的方法在对象-面向程序设计
为什么动态设置属性是一种不好的做法?创建类时,您将定义类型。您告知世界您的类(以及从中实例化的任何对象)由一组特定的字段和职能。如果您的ShopProduct类定义了$title属性,那么任何与ShopProduct一起使用的代码对象可以在$title属性可用的假设下进行。不能保证关于已动态设置的属性
在此阶段,我的对象仍然很麻烦。当我需要使用对象的属性时,我必须当前是从对象外部进行的。我伸手去设置并获取物业信息。设定多个多个对象上的属性很快就会变得很繁琐

// listing 03.08
$product1 = new ShopProduct(); 
$product1->title = "My Antonia"; 
$product1->producerMainName = "Cather"; 
$product1->producerFirstName = "Willa"; 
$product1->price = 5.99;

我再次使用ShopProduct类,一个接一个地覆盖所有默认属性值,直到我已经设置了所有产品详细信息。现在,我已经设置了一些数据,我也可以访问它

// listing 03.09
print "author: {$product1->producerFirstName} " . "{$product1->producerMainName}\n";

结果:author: Willa Cather
种设置属性值的方法存在很多问题。因为PHP让您动态设置属性,如果拼写错误或忘记了属性名称,则不会收到警告。例如,假设我要输入以下行
$product1->producerMainName = "Cather";
Unfortunately, I mistakenly type it like this:
$product1->producerSecondName = "Cather";
就PHP引擎而言,此代码是完全合法的,因此不会警告我。当我来打印作者的名字,我会得到意想不到的结果
另一个问题是我的类太放松了。我无须设定标题,价格或生产者名称。客户端代码可以确保这些属性存在,但很可能会遇到默认值的频率不高。理想情况下,我想鼓励任何实例化ShopProduct的人对象以设置有意义的属性值
最后,我得跳个圈去做一些我可能想经常做的事情。正如我们已经看到,打印完整的作者姓名是一个烦人的过程
最好让对象代表我处理这种繁琐的工作。所有这些问题都可以通过为ShopProduct对象提供自己的一组函数来解决。可用于从对象上下文中操纵属性数据

使用方法

正如属性允许对象存储数据一样,方法也允许对象执行任务。方法是在类中声明的特殊功能。如您所料,方法声明类似于函数宣言。function关键字在方法名称之前,后跟可选的参数列表括号中的变量。方法主体用花括号括起来

public function myMethod($argument, $another)
{
// ... 
}

与函数不同,方法必须在类的主体中声明。他们还可以接受许多限定词,包括可见性关键字。像属性一样,方法可以声明为public,protected或私人的。通过将方法声明为public,可以确保可以从当前对象外部调用该方法。如果在方法声明中省略了可见性关键字,则该方法将隐式声明为public。但是,为所有方法明确声明可见性被认为是一种好习惯(我将返回到方法本章后面的修饰符

class ShopProduct
{
    public $title = "default product";
    public $producerMainName = "main name"; 
    public $producerFirstName = "first name"; 
    public $price = 0;
    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    } 
}

在大多数情况下,您将使用对象变量和对象一起调用方法运算符->和方法名称。您必须在方法调用中使用括号,就像您在调用一个函数(即使您没有向该方法传递任何参数)

// listing 03.11
$product1 = new ShopProduct(); 
$product1->title = "My Antonia"; 
$product1->producerMainName = "Cather"; 
$product1->producerFirstName = "Willa"; 
$product1->price = 5.99;
print "author: {$product1->getProducer()}\n";

结果:author: Willa Cather
我将getProducer()方法添加到ShopProduct类中。请注意,我将getProducer()声明为公共,这意味着可以在课堂外调用它
我在此方法的主体中介绍一个功能。$this伪变量是一种机制类可以引用一个对象实例。如果您难以理解这个概念,请尝试将$this替换为短语“当前实例”。考虑以下语句
$this->producerFirstName
转换为以下内容
当前实例的$producerFirstName属性
因此,getProducer()方法合并并返回$producerFirstName$producerMainName属性,每次需要引用完整的生产者名称时,我就不必执行此任务
这使l类有所改善。但是,我仍然有很多不必要的灵活性。我靠在客户端编码器上,以从其默认值更改ShopProduct对象的属性。这是有问题的有两种方式。首先,需要五行代码才能正确初始化ShopProduct对象,并且没有编码人员会感谢您为了那个原因。其次,我无法确保在ShopProduct对象为初始化。我需要一种从类实例化对象时自动调用的方法

创建构造方法

创建对象时将调用构造函数方法。您可以使用它进行设置,以确保为基本属性分配值,并完成所有必要的准备工作

请注意,在php 5之前的版本中,构造函数方法采用了封闭它的类的名称。 因此ShopProduct类将使用ShopProduct()方法作为其构造函数。这不再适用于所有情况,自php 7起不推荐使用。将您的构造方法命名为__construct()

请注意,方法名称以两个下划线字符开头。您将看到此命名PHP类中许多其他特殊方法的约定。在这里,我为ShopProduct定义了一个构造函数类

// listing 03.12
class ShopProduct
{
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price = 0;

    public function __construct(
        $title,
        $firstName,
        $mainName,
        $price
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}

我再次将功能收集到该类中,从而节省了工作量和使用它的代码中的重复项。使用new运算符创建对象时,将调用__construct()方法

$product1 = new ShopProduct( "My Antonia",
    "Willa",
    "Cather", 5.99
);
print "author: {$product1->getProducer()}\n";

结果:author: Willa Cather


提供的所有参数都传递给构造函数。因此,在我的示例中,我传递了标题,名字,主要名称,然后将产品价格告知构造函数。构造方法使用伪变量$this为每个对象的属性分配值

注意,ShopProduct 对象现在更易于实例化并且更安全使用。实例化和设置是 在一个语句中完成。任何使用ShopProduct对象的代码都可以合理地确定其所有属性已初始化。

这种可预测性是面向对象编程的重要方面。你应该设计你的类,以便对象的用户可以确定其功能。使对象安全的一种方法是渲染可预测的属性中包含的数据类型。可以确保始终创建$ name属性例如,字符数据。但是,如果将属性数据从外部传递进来,该如何实现呢?类?在下一节中,我将研究一种可用于强制方法声明中的对象类型的机制

参数和类型

类型决定了可以在脚本中管理数据的方式。您使用字符串类型显示字符例如,使用字符串函数操作此类数据。整数用于数学表达式,布尔表达式用在测试表达式中,依此类推。这些类别称为原始类型。但是,在更高层次上,类定义了类型。因此,ShopProduct对象属于原语类型对象,但它也属于ShopProduct类类型。在本节中,我将介绍两种类型与类方法有关的种类
方法和函数的定义不一定要求参数必须是特定的类型。这既是诅咒,又是祝福。参数可以是任何类型的事实为您提供了灵活性。您可以构建对不同数据类型进行智能响应的方法,针对不断变化的需求量身定制功能情况。当方法主体期望一个方法执行时,这种灵活性也可能导致歧义化为代码。保留一种类型但获取另一种类型的参数

基本类型

PHP是一种松散类型的语言。这意味着没有必要声明变量来保存特定的数据类型。变量$ number可以将值2和字符串“ two”包含在同一字符串中范围。在强类型语言(例如C或Java)中,必须在分配之前声明变量的类型它的值,当然,该值必须是指定的类型
这并不意味着PHP没有类型的概念。可以分配给变量的每个值都有一种。您可以使用PHP的类型检查功能之一来确定变量值的类型。列出了PHP可以识别的基本类型及其相应的测试功能。每个函数都接受一个变量或值,如果此参数为相关类型,则返回true
PHP中的原始类型和检查函数

Type Checking Function Type Description
is_bool() Boolean One of the two special values, true or false
is_integer() Integer A whole number. Alias of is_int() and is_long()
is_double() Double A floating point number (a number with a decimal point). Alias of is_float()
is_string() String Character data
is_object() Object An object
is_array() Array An array
is_resource() Resource A handle for identifying and working with external resources such as databases or files
is_null() Null An unassigned value

在使用方法和函数时,检查变量的类型尤其重要。论点

基本类型问题:一个例子

您需要密切注意键入代码。这是许多类型相关的例子之一您可能遇到的问题。
假设您正在从XML文件中提取配置设置。<resolvedomains> XML元素告诉您的应用程序是否应尝试将IP地址解析为域名,这很有用但过程相对昂贵。这是一些示例XML。php代码同目录中新建resolve.xml文件

<!-- listing 03.14 --> 
<settings>
    <resolvedomains>false</resolvedomains>
</settings>

字符串“ false”由您的应用程序提取,并作为标志传递给称为outputAddresses(),显示IP地址数据。这是outputAddresses():
在class ShopProduct 的下边添加一个class AddressManager

// listing 03.15
class AddressManager
{
    private $addresses = ["209.131.36.159", "216.58.213.174"];

    public function outputAddresses($resolve)
    {
        foreach ($this->addresses as $address) {
            print $address;
            if ($resolve) {
                print " (" . gethostbyaddr($address) . ")";
            }
            print "\n";
            echo 12;
            $product1 = new ShopProduct("My Antonia",
                "Willa",
                "Cather", 5.99
            );
            print "author: {$product1->getProducer()}\n";
        }
    }
}

当然,AddressManager类可以做一些改进。硬编码不是很有用以IP地址为类。不过,outputAddresses()方法循环遍历$addresses数组属性,打印每个元素。如果$resolve参数变量本身解析为true,该方法将输出域名以及IP地址
这是一种将设置XML配置元素与AddressManager类。看看是否可以发现它的缺陷

// listing 03.16
$settings = simplexml_load_file(__DIR__."/resolve.xml");
$manager = new AddressManager();
$manager->outputAddresses((string)$settings->resolvedomains);

该代码段使用SimpleXML API来获取resolvedomains元素的值。在在此示例中,我知道此值是文本元素“ false”,并将其强制转换为字符串,如SimpleXML文档建议我应该
此代码将无法正常运行。在将字符串“ false”传递给outputAddresses()时方法,我误解了该方法对参数所做的隐式假设。方法是期望布尔值(是或否)。实际上,字符串“ false”在测试中将解析为true。这是因为PHP会在测试上下文中为您有用地将非空字符串值转换为Boolean true。考虑以下代码

if ( "false" ) { 
// ...
}

实际上等效于此:

if ( true ) { 
// ...
}

您可以采用多种方法来解决此问题
您可以使outputAddresses()方法更宽容,以便它识别字符串和应用一些基本规则将其转换为等效的布尔值

// listing 03.17
public function outputAddresses($resolve)
{
    if (is_string($resolve)) {
        $resolve =
            (preg_match("/^(false|no|off)$/i", $resolve)) ? false : true;
    }
    // ...
}

但是,有充分的设计理由要避免这种方法。一般来说,这是为方法或功能提供清晰,严格的界面要比提供模糊的方法更好。模糊和宽容的功能和方法会引起混乱,从而滋生错误
您可以采取另一种方法:保持outputAddresses()方法不变,并包含一个注释,其中包含明确的说明,即$resolve参数应包含布尔值。这个该方法实质上告诉编码人员阅读小字或收获后果

/**
     * Outputs the list of addresses.
     * If $resolve is true then each address will be resolved * @param $resolve boolean Resolve the address?
     */
    public function outputAddresses($resolve)
    {
      //.....
    }

假设您的客户编码人员是文档的勤奋读者,这是一种合理的方法。最后,您可以对outputAddresses()严格规定准备在其中找到的数据类型。$ resolve参数。对于布尔类型之类的原始类型,实际上只有一种方法可以在PHP 7发行版。您将必须编写代码来检查传入的数据并采取某种措施(如果有的话)与所需的类型不匹配

function outputAddresses($resolve)
{
    if (!is_bool($resolve)) {
        // do something drastic
    }
    //...
}

此方法可用于强制客户端代码在$ resolve中提供正确的数据类型。论点或发出警告

在下一节“ 注意:对象类型”中,我将描述一种更好的约束方法 传递给方法和函数的参数类型。

代表客户转换字符串参数将很友好,但可能会提出其他问题。在提供转换机制时,您会猜测客户端的上下文和意图。通过另一方面,强制使用布尔数据类型,则让客户端决定是否将字符串映射到布尔值,并确定哪个单词应映射为true或false。所述outputAddresses()方法,同时,专注于设计要执行的任务。强调执行特定的任务故意忽略更广泛的上下文是面向对象编程中的重要原则,我将在整本书中经常回到它
实际上,您处理参数类型的策略将取决于任何潜在的严重性一方面是错误,另一方面是灵活性的好处。PHP为您强制转换最原始的值,取决于上下文。字符串中的数字在以下情况下会转换为其等效的整数或浮点数例如,用在数学表达式中。因此,您的代码自然可以原谅类型错误。如果您期望方法参数之一是数组,但是,您可能需要格外小心。通过PHP数组函数之一的nonarray值不会产生有用的结果,并且可能导致级联您的方法中的错误
因此,您可能会在类型测试之间取得平衡,从一种类型转换为另一种类型。另一个,并依靠良好,清晰的文档(您应该提供文档,以及其他任何方式您决定这样做)
但是,如果您解决此类问题,就可以确定一件事—类型很重要。事实PHP的类型很松散,因此变得更加重要。您不能依靠编译器来防止类型相关虫子; 当意外类型进入您的帐户时,您必须考虑它们的潜在影响论点。您无法负担委托客户编码人员阅读您的想法,因此应始终考虑您的方法将如何处理传入的垃圾
但是,如果您解决此类问题,就可以确定一件事—类型很重要。事实PHP的类型很松散,因此变得更加重要。您不能依靠编译器来防止类型相关虫子; 当意外类型进入您的帐户时,您必须考虑它们的潜在影响论点。您无法负担委托客户编码人员阅读您的想法,因此应始终考虑您的方法将如何处理传入的垃圾

提示:对象类型

正如参数变量可以包含任何原始类型一样,默认情况下它可以包含任何类型的对象。这种灵活性有其用途,但可能在方法定义的上下文中出现问题。
想象一个设计用于ShopProduct对象的方法

// listing 03.18
class ShopProductWriter
{
    public function write($shopProduct)
    {
        $str = $shopProduct->title . ": "
            . $shopProduct->getProducer()
            . " (" . $shopProduct->price . ")\n";
        print $str;
    }
}
// listing 03.19
$product1 = new ShopProduct("My Antonia", "Willa", "Cather", 5.99);
$writer = new ShopProductWriter();
$writer->write($product1);

结果:My Antonia: Willa Cather (5.99)


ShopProductWriter类包含单个方法write()。write()方法接受一个ShopProduct对象并使用其属性和方法来构造和打印摘要字符串。我用了参数变量$shopProduct的名称,表示该方法需要ShopProduct对象,但是我没有强制执行。这意味着我可能会传递一个意外的对象或原始类型,而不会明智些,直到我开始尝试使用$ shopProduct参数。到那时,我的代码可能已经假定已传递了一个真正的ShopProduct对象

注意您可能想知道为什么我没有将write()方法直接添加到ShopProduct中。原因在于 责任领域。所述ShopProduct类是负责管理的产品数据; 该ShopProductWriter负责编写它。在阅读本章时,您将开始明白为什么这种分工会有用

为了解决这个问题,PHP 5引入了类类型声明(即所谓的类型提示)。要添加一个方法参数的类类型声明,您只需在方法参数前面放置一个类名你需要约束。因此,我可以这样修改write()方法

// listing 03.20
public function write(ShopProduct $shopProduct)
    {
        //...
    }

现在,如果write()方法包含类型为object的对象,则它将仅接受$ shopProduct参数ShopProduct。此代码段尝试使用躲闪的对象调用write()

// listing 03.21
class Wrong
{
}
$writer = new ShopProductWriter(); 
$writer->write(new Wrong());

因为write()方法包含一个类类型声明,所以将错误的对象传递给它会导致致命错误
TypeError:传递给ShopProductWriter :: write()的参数1必须是的实例ShopProduct,给出了错误的实例,在...上的Runner.php中调用
这使我不必在使用参数之前测试参数的类型。这也使得客户签名的方法签名更加清晰。她可以看到write()方法的要求乍看上去。她不必担心由类型错误引起的一些晦涩的错误,因为声明严格执行
尽管这种自动类型检查是防止错误的一种好方法,但对于了解在运行时检查类型声明。这意味着类声明只会在将不需要的对象传递给方法时报告错误。如果对write()的调用被掩埋了在仅在圣诞节早晨运行的条件条款中,如果您尚未仔细检查代码。
有了标量类型声明,我可以向ShopProduct类添加一些约束

// listing 03.22
class ShopProduct
{
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price = 0;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
    }
    // ...
}

通过以这种方式支持构造函数方法,我可以确定$ title,$ firstName,$ mainName参数将始终包含字符串数据,而$price将包含浮点数。我可以证明通过使用错误的信息实例化ShopProduct来实现此目的

// listing 03.23
// will fail
$product = new ShopProduct("title", "first", "main", []);

我尝试实例化ShopProduct对象。我将三个字符串传递给构造函数,但最终失败通过传递一个空数组而不是所需的float来克服障碍。由于类型声明,PHP不会让我逃脱了:
TypeError:传递给ShopProduct :: __ construct()的参数4必须为float类型,给定数组,调用...

// listing 03.24
$product = new ShopProduct("title", "first", "main", "4.22");

到目前为止,非常有用。但是回想一下我们在AddressManager类中遇到的问题。的字符串“ false”已悄悄地解析为布尔值true。默认情况下,如果我使用布尔类型,仍然会发生这种情况如下所示在AddressManager :: outputAddresses()方法中进行声明:

// listing 03.25
public function outputAddresses(bool $resolve)
{
// ... 
}

现在考虑通过这样的字符串传递的调用

// listing 03.26 
$manager->outputAddresses("false");

由于隐式强制转换,它在功能上与传递布尔值true的函数相同。您可以使标量类型声明严格,尽管只能逐个文件地进行。在这里,我打开严格键入声明并再次使用字符串调用outputAddresses():

// listing 03.27
declare(strict_types=1); 
$manager->outputAddresses("false");

因为我声明了严格的类型,所以此调用将引发TypeError
TypeError:传递给AddressManager :: outputAddresses()的参数1必须为类型布尔值,给定的字符串,在...中调用

请注意,strict_types 声明适用于从中进行调用的文件,而不适用于 实现了功能或方法。因此,由客户端代码强制执行严格性

您可能需要使参数成为可选参数,但是如果提供了参数,则必须对其进行约束。您可以通过提供默认值来做到这一点

class ConfReader
{
    public function getValues(array $default = null)
    {
        $values = [];
// do something to get values
// merge the provided defaults (it will always be an array)

        $values = array_merge($default, $values);
        return $values;
    }
}

我列出了PHP支持的类型声明

Type declaration Since Description
array 5.1 An array. Can default to null or an array
int 7.0 An integer Can default to null or an integer
float 7.0 A floating point number (a number with a decimal point). An integer will be accepted—even with strict mode enabled. Can default to null, a float,or an integer
callable 5.4 Callable code (such as an anonymous function). Can default to null
bool 7.0 A Boolean. Can default to null or a Boolean.
string 5.0 Character data. Can default to null or a string.
self 5.0 A reference to the containing class
[a class type] 5.0 The type of a class or interface. Can default to null

当我描述类类型声明时,我暗示类型和类是同义词。有两者之间的关键区别。当定义一个类时,您也定义了一个类型,但是一个类型可以描述整个班级。不同类别可以组合在一起的机制一个类型下的被称为继承。我将在下一节中讨论继承

遗产

继承是可以从基类派生一个或多个类的方法。从另一个继承的类称为它的子类。这种关系通常在父母和子女的条款。子类是从父类派生并继承其特性。这些特征包括属性和方法。子类通常会添加新的其父级提供的功能(也称为超类);因此,据说有一个子类扩展其父级
在深入探讨继承语法之前,我将研究它可以帮助您解决的问题

继承问题

再看一下ShopProduct类。目前,它非常通用。它可以处理各种产品

// listing 03.29
$product1 = new ShopProduct("My Antonia", "Willa", "Cather", 5.99);
$product2 = new ShopProduct(
    "Exile on Coldharbour Lane", "The", "Alabama 3", 10.99
);
print "author: " . $product1->getProducer() . "\n";
print "artist: " . $product2->getProducer() . "\n";

结果:
author: Willa Cather
artist: The Alabama 3

将生产者名称分为两部分,对书籍和CD都适用。我希望能够按“阿拉巴马州3”和“凯瑟”排序,而不按“ The”和“ Willa”排序。懒惰是一种出色的设计策略,因此在此阶段,无需担心将ShopProduct用于一种以上的产品
但是,如果我在示例中增加了一些新的要求,事情将变得更加复杂。例如,假设您需要代表书籍和CD的特定数据。对于CD,您必须存储总上场时间;对于书籍,总页数。可能会有许多其他差异,但是这将有助于说明问题
如何扩展示例以适应这些变化?立即出现两个选项他们自己。首先,我可以将所有数据放入ShopProduct类中。其次,我可以拆分ShopProduct分为两个单独的类
让我们研究第一种方法。在这里,我将与CD和书籍相关的数据合并在一个类中

// listing 03.30
class ShopProduct
{
    public $numPages;
    public $playLength;
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price;

    public function __construct(
        string $title,
        string $firstName, 
        string $mainName, 
        float $price,
        int $numPages = 0, 
        int $playLength = 0
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
        $this->numPages = $numPages;
        $this->playLength = $playLength;
    }

    public function getNumberOfPages()
    {
        return $this->numPages;
    }

    public function getPlayLength()
    {
        return $this->playLength;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}

我提供了对$ numPages$ playLength属性的方法访问权限,以说明差异部队在这里工作。从此类实例化的对象将包含冗余方法,并且对于CD,必须使用不必要的构造函数实例化:CD将存储信息和功能
与书页有关的内容,并且一本书将支持播放长度数据。这可能是你可以忍受的东西马上。但是,如果我添加更多产品类型,每种产品都有自己的方法,然后添加每种类型都有更多方法?我们的班级将变得越来越复杂且难以管理
因此,将不属于一个类的字段强制强制会导致带有冗余对象的肿对象属性和方法
问题也不止于数据。我也遇到功能上的困难。考虑一种汇总产品的方法。销售部门已要求有一个清晰的摘要行以用于发票。他们要我包括CD的播放时间和书籍的页数,因此我将不得不为每种类型提供不同的实现。我可以尝试使用标志来跟踪对象的格式。这是一个例子:

 // listing 03.31
    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        if ($this->type == 'book') {
            $base .= ": page count - {$this->numPages}";
        } elseif ($this->type == 'cd') {
            $base .= ": playing time - {$this->playLength}";
        }
        return $base;
    }

为了设置$ type属性,我可以测试构造函数的$ numPages参数。还是一次同样,ShopProduct类变得比必需的更加复杂。当我增加更多差异时格式或添加新格式,这些功能差异将变得更加难以管理。也许我应该尝试解决该问题的另一种方法。
随着ShopProduct开始感觉像是两个类,我可以接受这一点并创建两种类型而不是一个。这是我可能的方法

// listing 03.32
class CdProduct
{
    public $playLength;
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $playLength
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
        $this->playLength = $playLength;
    }

    public function getPlayLength()
    {
        return $this->playLength;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": playing time - {$this->playLength}";
        return $base;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}
// listing 03.33
class BookProduct
{
    public $numPages;
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $numPages
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
        $this->numPages = $numPages;
    }

    public function getNumberOfPages()
    {
        return $this->numPages;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": page count - {$this->numPages}";
        return $base;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }
}

我已经解决了复杂性问题,但是付出了一定的代价。我现在可以创建一个getSummaryLine()方法用于每种格式都无需测试标志。两个类都不维护与其无关的字段或方法
代价在于重复。每个类中的getProducerName()方法完全相同。每构造函数以相同的方式设置许多相同的属性。这是你的另一种难闻的气味应该训练自己嗅出
如果我需要getProducer()方法对每个类都表现相同,则我对一个需要为另一种实现。如果没有照顾,课程很快就会退出同步
即使我有信心可以保持重复,但我的担忧仍未结束。我现在有两种类型而不是一个
还记得ShopProductWriter类吗?它的write()方法被设计为使用单一类型:ShopProduct。我如何才能像以前一样修改它?我可以从中删除类类型声明方法签名,但是我必须相信要幸运的是,write()传递了正确类型的对象。我可以将我自己的类型检查代码添加到方法的主体中

class ShopProductWriter
{
    public function write($shopProduct)
    {
        if (
            !($shopProduct instanceof CdProduct) &&
            !($shopProduct instanceof BookProduct)
        ) {
            die("wrong type supplied");
        }
        $str = "{$shopProduct->title}: "
            . $shopProduct->getProducer()
            . " ({$shopProduct->price})\n";
        print $str;
    }
}

注意示例中的instanceof运算符;如果左侧的对象-手工操作数是右操作数表示的类型。
再一次,我被迫加入新的复杂性层。我不仅要测试$ shopProduct参数针对write()方法中的两种类型,但是我必须相信每种类型都会继续支持彼此相同的字段和方法。当我只是整洁时要求使用单一类型,因为我可以使用类类型声明,并且可以确信ShopProduct类支持特定的接口
ShopProduct类的CD和书籍方面不能很好地协同工作,但不能分开,它似乎。我想将书籍和CD作为单一类型使用,同时为每种格式。我想在一个地方提供通用功能以避免重复,但允许每个地方格式以不同方式处理某些方法调用。我需要使用继承

处理继承

构建继承树的第一步是找到基类的元素,这些元素不适合在一起,或者需要以不同的方式处理
我知道getPlayLength()和getNumberOfPages()方法并不在一起。我也知道我需要为getSummaryLine()方法创建不同的实现。让我们用这些差异作为两个派生类的基础


// listing 03.35
class ShopProduct
{
    public $numPages;
    public $playLength;
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $numPages = 0,
        int $playLength = 0
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
        $this->numPages = $numPages;
        $this->playLength = $playLength;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        return $base;
    }
}

// listing 03.36
class CdProduct extends ShopProduct
{
    public function getPlayLength()
    {
        return $this->playLength;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": playing time - {$this->playLength}";
        return $base;
    }
}

// listing 03.37
class BookProduct extends ShopProduct
{
    public function getNumberOfPages()
    {
        return $this->numPages;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": page count - {$this->numPages}";
        return $base;
    }
}

要创建子类,必须在类声明中使用extends关键字。在这个例子中创建了两个新的类BookProduct和CdProduct。两者都扩展了ShopProduct类
由于派生类未定义构造函数,因此父类的构造函数会自动实例化它们时调用。子类继承对所有父级公共和受保护对象的访问方法(尽管不是私有方法或属性)。这意味着您可以调用getProducer()从CdProduct类实例化的对象上的方法,即使getProducer()是在Shop产品类别

// listing 03.38
$product2 = new CdProduct("Exile on Coldharbour Lane", "The",
    "Alabama 3",
    10.99,
    0,
    60.33
);
print "artist: {$product2->getProducer()}\n";

结果:artist: The Alabama 3
因此,这两个子类都继承了公共父类的行为。您可以对待BookProduct对象,就好像它是一个ShopProduct对象。您可以将BookProduct或CdProduct对象传递给ShopProductWriter类的write()方法将全部按预期工作。
请注意,CdProduct和BookProduct类都覆盖了getSummaryLine()方法,提供自己的实现。派生的类可以扩展,但也可以更改其父类的功能
该方法的超类的实现似乎是多余的,因为它已被覆盖由其两个孩子。但是,它提供了新的子类可能使用的基本功能。的方法的存在还向客户端代码保证所有ShopProduct对象都将提供一个getSummaryLine()方法。稍后,您将看到如何在基类中实现此承诺根本不提供任何实现。每个子ShopProduct类都继承其父级的属性。BookProduct和CdProduct都在其getSummaryLine()版本中访问$ title属性
首先,继承可能是一个很难理解的概念。通过定义扩展另一个的类,您可以确保从其实例化的对象首先由子对象的特征定义,然后由家长班。另一种思考方式是搜索。当我调用$ product2-> getProducer(),在CdProduct类中找不到这样的方法,并且调用属于到ShopProduct中的默认实现。当我调用$ product2-> getSummaryLine()时,另一方面,可以在CdProduct中找到并调用getSummaryLine()方法
属性访问也是如此。当我在BookProduct类的$ title中访问getSummaryLine()方法,在BookProduct类中找不到该属性。取而代之的是父类,来自ShopProduct。$ title属性同样适用于两个子类,因此,它属于超类
快速浏览ShopProduct构造函数,可以看到我仍在管理基本数据库中的数据应该由其子代处理的类。BookProduct类应处理$ numPages参数和属性,而CdProduct类应处理$ playLength参数和属性。为了做到这一点工作时,我将在每个子类中定义构造函数方法

构造函数与继承

在子类中定义构造函数时,您有责任将所有参数传递给父母 如果这样做失败,则最终可能会得到部分构造的对象。要在父类中调用方法,必须首先找到一种引用类本身的方法:一个句柄。PHP为此提供了parent关键字
要在类而不是对象的上下文中引用方法,请使用::而不是->

parent::__construct()

前面的意思是“调用父类的__construct()方法。”在这里我修改了示例,以便每个类仅处理适合它的数据:

// listing 03.39
class ShopProduct
{
    public $title;
    public $producerMainName;
    public $producerFirstName;
    public $price;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        return $base;
    }
}

// listing 03.40
class BookProduct extends ShopProduct
{
    public $numPages;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $numPages
    )
    {
        parent::__construct(
            $title,
            $firstName,
            $mainName,
            $price
        );
        $this->numPages = $numPages;
    }

    public function getNumberOfPages()
    {
        return $this->numPages;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( $this->producerMainName, ";
        $base .= "$this->producerFirstName )";
        $base .= ": page count – {$this->numPages}";
        return $base;
    }
}

// listing 03.41
class CdProduct extends ShopProduct
{
    public $playLength;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $playLength
    )
    {
        parent::__construct(
            $title,
            $firstName,
            $mainName,
            $price
        );
        $this->playLength = $playLength;
    }

    public function getPlayLength()
    {
        return $this->playLength;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": playing time - {$this->playLength}";
        return $base;
    }
}

每个子类在设置自己的属性之前都会调用其父级的构造函数。基类现在只知道自己的数据。子类级通常是其父母的专业。作为一个规则大拇指,您应该避免给父类有关孩子的特殊知识。

请注意,在PHP 5之前,构造函数采用了封闭类的名称。新的统一构造函数 使用名称__construct()。使用旧语法,对父构造函数的调用会将您绑定到该特定对象类:parent :: ShopProduct();。旧的构造函数语法在php 7.0中已弃用,不应使用

调用替代方法

parent关键字可以与任何在父类中覆盖其对应项的方法一起使用。当你重写方法,您可能不希望消除父级的功能,而希望对其进行扩展。您通过在当前对象的上下文中调用父类的方法可以实现此目的。如果您再看一次使用getSummaryLine()方法实现,您将看到它们重复了很多代码。会更好使用而不是复制ShopProduct类中已经开发的功能

//listing 03.42
// ShopProduct class...
    function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        return $base;
    }
// listing 03.43
// BookProduct class...
    public function getSummaryLine()
{
    $base = parent::getSummaryLine();
    $base .= ": page count - $this->numPages";
    return $base;
}

我在ShopProduct基类中为getSummaryLine()方法设置了核心功能。而是而不是在CdProduct和BookProduct子类中重现它,我只是在调用父方法之前继续向摘要字符串添加更多数据
既然您已经了解了继承的基础知识,那么我将重新检查属性和方法的可视性。

公共、私有和受保护的:管理对类的访问

到目前为止,我已经宣布所有财产公开。公共访问是方法和方法的默认设置属性,如果在属性声明中使用了旧的var关键字
您的类中的元素可以声明为公共,私有或受保护:
• 可以从任何上下文访问公共属性和方法。
• 只能从封闭的类内部访问私有方法或属性。即使是子类也无法访问。
• 受保护的方法或属性只能从以下任一位置访问封闭类或子类。没有外部代码被授予访问权限
那么这对我们有什么用呢?可见性关键字允许您仅公开类的以下方面:客户要求。这为您的对象设置了清晰的界面
通过阻止客户端访问某些属性,访问控制还可以帮助防止错误您的代码。例如,假设您想允许ShopProduct对象支持折扣。您可以添加$ discount属性和setDiscount()方法

// listing 03.44
// ShopProduct class
public $discount = 0; 
//...
public function setDiscount(int $num)
{
$this->discount = $num; 
}

有了设置折扣的机制,您可以创建一个getPrice()方法,该方法使用已应用折扣的帐户:

public function getPrice()
{
return ($this->price - $this->discount); 
}

此时,您遇到了问题。您只想对外展示调整后的价格,但客户可以轻松地绕过getPrice()方法并访问$ price属性:

print "The price is {$product1->price}\n";

这将打印原始价格,而不是您希望提供的折扣调整后的价格。你可以停下来通过将$ price属性设为私有,可以立即解决此问题。这将阻止直接访问,迫使客户端使用getPrice()方法。ShopProduct类之外的任何尝试访问$ price属性的尝试将失败。就更广阔的世界而言,这种财产已经不复存在。
将属性设置为私有可能是一个过分的策略。私有财产不能由子类使用。想象一下,我们的业务规则规定,仅书籍就没有资格享受折扣。您可以覆盖getPrice()方法,以便它返回$ price属性,不施加折扣

// listing 03.45 // BookProduct
public function getPrice()
{
return $this->price; 
}

由于私有的$ price属性是在ShopProduct类而不是BookProduct中声明的,因此尝试在这里访问将失败。解决此问题的方法是声明$ price受保护protected,从而授予访问权限到后代阶级。请记住,不能从外部访问受保护的属性或方法声明它的类层次结构。只能从其原始类内部或从在原始类的子类中
通常,在隐私方面犯错。首先将私有财产或受保护财产放宽仅在需要时限制。类中的许多(如果不是大多数)方法将是公共的,但是如果毫无疑问,将其锁定。为类中的其他方法提供本地功能的方法没有与类用户的相关性。将其设为私有或受保护

存取器方法

即使客户端程序员需要使用您班级持有的价值观,通常也要拒绝直接访问属性,提供中继所需值的方法。这样的方法是被称为访问器或获取器和设置器
您已经看到访问器方法提供的一项好处。您可以使用访问器来过滤属性值根据情况而定,如getPrice()方法所示
您还可以使用setter方法来强制属性类型。类型声明可用于约束方法参数,但属性可以包含任何类型的数据。记住ShopProductWriter类使用ShopProduct对象输出列表数据?我可以进一步开发它,以便它可以写任意数量的一次ShopProduct对象
现在,ShopProductWriter类更加有用。它可以容纳许多ShopProduct对象,并且一口气为他们写数据。但是,我必须相信我的客户编码人员要尊重课堂的意图。尽管我提供了addProduct()方法,但并未阻止程序员直接操作$ products属性。不仅有人会向对象添加错误的对象$ products数组属性,但他甚至可以覆盖整个数组并将其替换为原始值。一世可以通过将$ products属性设为私有来防止这种情况

class ShopProductWriter { 
  private $products = [];
//...

现在,外部代码不可能损坏$ products属性。所有访问都必须通过addProduct()方法,以及我在方法声明中使用的类类型声明可确保仅ShopProduct对象可以添加到array属性

ShopProduct类

让我们通过修改ShopProduct类及其子级来锁定访问控制来结束本章:

<?php

// listing 03.48
class ShopProduct
{
    private $title;
    private $producerMainName;
    private $producerFirstName;
    protected $price;
    private $discount = 0;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price
    )
    {
        $this->title = $title;
        $this->producerFirstName = $firstName;
        $this->producerMainName = $mainName;
        $this->price = $price;
    }

    public function getProducerFirstName()
    {
        return $this->producerFirstName;
    }

    public function getProducerMainName()
    {
        return $this->producerMainName;
    }

    public function setDiscount($num)
    {
        $this->discount = $num;
    }

    public function getDiscount()
    {
        return $this->discount;
    }

    public function getTitle()
    {
        return $this->title;
    }

    public function getPrice()
    {
        return ($this->price - $this->discount);
    }

    public function getProducer()
    {
        return $this->producerFirstName . " " . $this->producerMainName;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        return $base;
    }
}

// listing 03.49
class CdProduct extends ShopProduct
{
    private $playLength;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $playLength
    )
    {
        parent::__construct(
            $title,
            $firstName,
            $mainName,
            $price
        );
        $this->playLength = $playLength;
    }

    public function getPlayLength()
    {
        return $this->playLength;
    }

    public function getSummaryLine()
    {
        $base = "{$this->title} ( {$this->producerMainName}, ";
        $base .= "{$this->producerFirstName} )";
        $base .= ": playing time - {$this->playLength}";
        return $base;
    }
}


// listing 03.50
class BookProduct extends ShopProduct
{
    private $numPages;

    public function __construct(
        string $title,
        string $firstName,
        string $mainName,
        float $price,
        int $numPages
    )
    {
        parent::__construct(
            $title,
            $firstName,
            $mainName,
            $price
        );
        $this->numPages = $numPages;
    }

    public function getNumberOfPages()
    {
        return $this->numPages;
    }

    public function getSummaryLine()
    {
        $base = parent::getSummaryLine();
        $base .= ": page count - $this->numPages";
        return $base;
    }

    public function getPrice()
    {
        return $this->price;
    }
}


// listing 03.51
$product1 = new BookProduct("book", "The",
    "zph",
    10.99,
    789
);
$product2 = new CdProduct("Exile on Coldharbour Lane", "The",
    "Alabama 3",
    10.99,
    60.33
);

class ShopProductWriter
{
    private $products = [];

    public function addProduct(ShopProduct $shopProduct)
    {
        $this->products[] = $shopProduct;
    }

    public function write()
    {
        $str = "";
        foreach ($this->products as $shopProduct) {
            $str .= "{$shopProduct->getTitle()}: ";
            $str .= $shopProduct->getProducer();
            $str .= " ({$shopProduct->getPrice()})\n";
        }
        print $str;
    }
}

$writer = new ShopProductWriter();
$writer->addProduct($product1);
$writer->addProduct($product2);
$writer->write();

此版本的ShopProduct系列没有实质性的新内容。所有属性都是私有或受保护的,我添加了许多访问器方法来解决问题

摘要

本章涵盖了许多基础知识,使一门课从空的实现到全面特色继承层次结构。您遇到了一些设计问题,尤其是关于类型和类型的问题。遗产。您看到了PHP对可见性的支持,并探讨了其一些用途。在下一章中,我将向您展示更多PHP的面向对象功能

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