第6章数据和逻辑:使用对象

到目前为止,您已经看到的数据和逻辑的基础知识足以让您在PHP中完成大量工作。另一个概念- 面向对象的编程将数据与在其上运行的逻辑相结合 – 有助于组织代码。 特别是,对象非常适合制作可重用的代码包,因此熟悉它们的工作方式将使得使用大量现有的PHP附加组件和库变得更加容易。

在编程世界中,对象是将关于事物(例如主菜中的成分)的数据与对该事物的动作(例如确定某个成分是否在主菜中)组合的结构。在程序中使用对象提供了一种组织结构,用于将相关变量和函数组合在一起。

以下是使用对象时需要了解的一些基本术语:


描述一种对象的变量和函数的模板或配方。例如,一个Entree类将包含保存其名称和成分的变量。Entree课堂上的功能可以用于烹饪主菜,提供服务,以及确定特定成分是否在其中。
方法
定义了一个函数 在课堂上。
属性
变量定义 在课堂上。

一个人的个人用法。如果您在课程中为三个晚餐提供晚餐,您将创建三个Entree课程实例 。虽然这些实例中的每一个都基于相同的类,但它们在内部通过具有不同的属性值而不同。每个实例中的方法包含相同的指令,但可能产生不同的结果,因为它们各自依赖于其实例中的特定属性值。创建类的新实例称为“实例化对象”。
构造函数
实例化对象时自动运行的特殊方法。通常,构造函数设置对象属性并执行使对象可供使用的其他内务处理。
静态方法
一种特殊的方法即可 在没有实例化类的情况下调用。 静态方法不依赖于特定实例的属性值。

对象基础知识

例6-1定义了一个Entree表示主菜的类。

例6-1。定义一个类

在例6-1中,类定义以special关键字class开头,后跟我们给类的名称。在类名之后,花括号之间的所有内容都是类的定义 – 类的属性和方法。该类有两个属性($name和$ingredients)和一个方法()。hasIngredient()该public关键字告诉你的程序的一部分被允许访问关键字连接到特定的属性或方法的PHP引擎。稍后我们将在“属性和方法可见性”中介绍。

该hasIngredient()方法看起来大致像常规函数定义,但它的主体包含一些新的东西:$this。这是一个特殊变量,指的是类调用函数的任何实例。例6-2显示了两个不同实例的实际操作。

例6-2。创建和使用对象

该new运营商返回一个新Entree对象,因此,在实施例6-2,$soup和$sandwich每个参考的不同实例Entree的类。

箭头操作符(->),由 连字符和大于号,是通往对象内属性(变量)和方法(函数)的途径。 要访问属性,请将箭头放在对象的名称后面,然后将属性放在箭头后面。 要调用方法,请将方法名称放在箭头后面,然后是表示函数调用的括号。

请注意,用于访问属性和方法的箭头操作者是从分隔阵列的键和值在操作者不同的 array()或foreach()。数组箭头有一个等号:=>。对象箭头有一个连字符:->。

为属性赋值就像为任何其他变量赋值一样,但使用箭头语法指示属性名称。 表达式$soup->name表示“ 变量保存name的对象实例内的属性$soup”,表达式$sandwich->ingredients表示“ 变量保存ingredients的对象实例内的属性$sandwich”。

在foreach()循环内部,每个对象的hasIngredient()方法都被调用。该方法传递成分的名称,并返回该成分是否在对象的成分列表中。在这里,您可以看到特殊$this变量的工作原理。什么时候$soup->hasIngredient()叫,$this指的是$soup体内hasIngredient()。当$sandwich->hasIngredient()被调用时,$this指的是$sandwich。该$this变量并不总是指向同一个对象实例,而是指该实例的方法被调用的。这意味着当示例6-2运行时,它会打印:

在实施例6-2中,当$ing是chicken,则两个$soup->hasIngredient($ing) 和$sandwich->hasIngredient($ing)回报true。两个对象的$ingredients属性都包含具有值的元素chicken。但是,只有$soup->ingredients拥有water,只有$sandwich->ingredients拥有bread。无论对象都有lemon其$ingredients属性。

类也可以包含静态方法。这些方法不能使用$this变量,因为它们不会在特定对象实例的上下文中运行,而是在类本身上运行。静态方法对于与类的内容相关的行为很有用,但对任何一个对象都没有。例6-3添加了一个静态方法Entree,返回可能的主菜大小列表。

例6-3。定义静态方法

例6-3中静态方法的声明与其他方法定义类似,static前面添加了关键字function。要调用静态方法,请::在类名和方法名之间放置,而不是->,如例6-4所示。

例6-4。调用静态方法

构造函数

类可以有一个特殊的方法,称为构造函数,它在创建对象时运行。 构造函数通常处理使对象准备好使用的设置和内务处理任务。例如,我们可以更改Entree类并为其提供构造函数。此构造函数接受两个参数:主菜的名称和成分列表。通过将这些值传递给构造函数,我们避免在创建对象后设置属性。在PHP中,始终调用类的构造方法__construct()。例6-5显示了更改的类及其构造方法。

例6-5。使用构造函数初始化对象

在例6-5中,您可以看到该__construct()方法接受两个参数并将其值分配给类的属性。参数名称与属性名称匹配的事实只是一个方便 – PHP引擎不要求它们匹配。在构造函数内部,$this关键字引用正在构造的特定对象实例。

要将参数传递给构造函数,请在调用new运算符时将类名称视为函数名称,方法是在其后面添加括号和参数值。例6-6显示了我们的类,其中构造函数通过创建$soup和$sandwich与我们之前使用的对象相同的对象来实现。

例6-6。调用构造函数

构造函数由new运算符调用,作为PHP引擎创建新对象的一部分,但构造函数本身不会创建对象。这意味着构造函数不返回值,也不能使用返回值来表示出错的地方。这是异常的工作,将在下一节中讨论。

指出异常问题

在例6-5中,如果作为$ingredients参数传入除数组之外的其他内容会发生什么?由于代码是在例6-5中编写的,没有!无论它是什么,都$this->ingredients被分配了价值$ingredients。但是如果它不是数组,hasIngredient()则在调用时会导致问题- 该方法假定$ingredients属性是数组。

构造函数非常适合验证提供的参数是正确的类型还是其他适当的类型。但如果出现问题,他们需要一种抱怨的方法。这是异常的来源。异常是一个特殊的对象,可用于指示发生了异常。创建异常会中断PHP引擎并将其设置在不同的代码路径上。

Entree如果$ingredients参数不是数组,示例6-7修改构造函数以引发异常。(“抛出”异常意味着您使用异常告诉PHP引擎出错的地方。)

例6-7。抛出异常

例外由Exception班级表示。Exception构造函数的第一个参数是一个描述错误的字符串。因此,该行throw new Exception(‘$ingredients must be an array’);创建一个新Exception对象,然后将其交给throw构造,以便中断PHP引擎。

如果$ingredients是数组,那么代码就像以前一样运行。如果它不是数组,则抛出异常。例6-8显示了创建Entree带有错误$ingredients参数的对象。

例6-8。引发异常被抛出

例6-8显示了这样的错误消息(假设代码位于名为exception-use.php的文件中,并且Entree类定义位于名为construct-exception.php的文件中):

在该错误输出中,有两个独立的事物需要识别。第一个是错误消息 来自PHP引擎:PHP Fatal error: Uncaught exception ‘Exception’ with message ‘$ingredients must be an array’ in construct-exception.php:9。这意味着在construct-exception.php(定义Entree类的文件)的第9行中,抛出了一个异常。 因为没有额外的代码来处理该异常(我们将很快看到如何做到这一点),它被称为“未被捕获”并导致PHP引擎停止尖叫 – 一个“致命”错误,立即停止程序执行。

该错误输出中的第二件事是堆栈跟踪:PHP引擎停止时活动的所有函数的列表。这里只有一个:Entree从中调用的构造函数new Entree。{main}堆栈跟踪中的行表示在执行任何其他操作之前的第一级程序执行。你总会在任何堆栈跟踪的底部看到它。

这是好的,我们阻止hasIngredient()被调用,因此它不在非数组成分上运行,但完全停止程序与这样一个严厉的错误消息是矫枉过正。抛出异常的另一面是抓住它们 – 在PHP引擎获取它之前抓住异常并退出。

要自己处理异常, 做两件事:

将可能引发异常的代码放入try块中。
catch在潜在的异常抛出代码之后放置一个块以便处理问题。
例6-9添加try和catch阻止处理异常。

例6-9。处理异常

在例6-9中,try和catch块一起工作。块中的每个语句try都会运行,如果遇到异常则停止。如果发生这种情况,PHP引擎会跳转到catch块,设置变量$e以保存Exception创建的对象。catch块内的代码使用Exception类的getMessage()方法来检索在创建异常时给出的消息的文本。例6-9打印:

扩展对象

使它们对组织代码非常有用的对象的一个​​方面是子类的概念,它允许您在添加一些自定义功能时重用类。子类(有时称为子类)以现有类(父类)的所有方法和属性开始,但随后可以更改它们或添加它自己的类。

例如,考虑一个主菜不仅仅是一道菜,而是一些菜的组合,如一碗汤和一个三明治。我们现有的Entree课程将被迫通过将“汤”和“三明治”作为成分处理或通过列举所有汤成分和夹心成分作为该组合的成分来对其进行建模。这两种解决方案都不理想:汤和三明治本身不是成分,重新计算所有成分意味着我们需要在任何成分发生变化时更新多个地方。

我们可以通过创建一个子类来更清晰地解决问题,该子类Entree期望将Entree对象实例作为成分,然后修改子类的hasIngredient()方法以检查那些对象实例的成分。ComboMeal该类的代码如例6-10所示。

例6-10。扩展Entree类

在例6-10中,ComboMeal后跟类名extends Entree。这告诉PHP引擎ComboMeal类应该继承所有的方法和属性的的Entree类。对于PHP引擎来说,就好像你重新定义了定义Entree内部的定义一样ComboMeal,但是你实际上不需要做那些繁琐的输入。然后,唯一需要在ComboMeal定义的花括号内的东西是变化或添加。在这种情况下,唯一的变化是一种新hasIngredient()方法。它不是$this->ingredients作为一个数组进行检查,而是将其视为一个Entree对象数组,并hasIngredient()在每个对象上调用该方法。如果任何这些呼叫返回true,这意味着在组合的主菜中的一个具有指定的成分,所以ComboMeal的hasIngredient()方法返回true。如果在迭代完所有主菜之后没有返回任何内容true,则该方法返回false,这意味着没有主菜包含其中的成分。例6-11显示了工作中的子类。

例6-11。使用子类

因为汤和三明治都含有chicken,汤包含water但不包含pickles,例6-11打印:

这很好用,但我们不保证传递给ComboMeal构造函数的项目确实是Entree对象。如果它们不是,那么调用hasIngredient()它们可能会导致错误。要解决这个问题,我们需要添加一个自定义构造函数来ComboMeal检查这个条件,并调用常规Entree构造函数以便正确设置属性。示例6-12中ComboMeal显示了此构造函数的一个版本。

例6-12。将构造函数放在子类中

例6-12中的构造函数使用特殊语法parent::__construct()来引用构造函数Entree。正如$this在对象方法中具有特殊含义一样,也是如此parent。它指的是当前类是子类的类。因为ComboMeal延伸Entree,parent内部ComboMeal指Entree。所以,parent::__construct()里面ComboMeal指的__construct()是Entree类里面的方法。

在子类构造函数中,重要的是要记住必须显式调用父构造函数。如果省略调用parent::__construct(),父构造函数永远不会被调用,并且它可能是重要的行为永远不会被PHP引擎执行。在这种情况下,Entree构造函数确保它$ingredients是一个数组并设置$name和$ingredients属性。

在调用之后parent::__construct(),ComboMeal构造函数确保组合的每个提供的成分本身都是一个Entree对象。它使用instanceof运算符。表达式$entree instanceof Entree求值为trueif $entree引用Entree类的对象实例。1如果任何提供的成分(对于a ComboMeal,实际上是主要成分)不是Entree对象,则代码会抛出异常。

属性和方法可见性

例6-12中的ComboMeal构造函数可以很好地确保a 只被赋予其成分的实例。ComboMealEntree但那后会发生什么?后续代码可以将$ingredients属性的值更改为任何内容 – 非Entrees,数字或甚至数组false。

我们通过更改属性的可见性来防止此问题。而不是public,我们可以将它们标记为private或protected。 这些其他可见性设置不会更改类中的代码可以执行的操作 – 它始终可以读取或写入自己的属性。的private能见度防止类以外的任何码从访问属性。该protected知名度意味着该类以外的唯一的代码可以访问属性是子类代码。

实施例6-13示出了修改后的版本Entree,其中,类$name属性是private与$ingredients属性protected。

例6-13。改变财产可见度

因为$name是private在实施例6-13,没有办法来读取或外部从它的代码改变Entree。但是,添加的getName()方法为非Entree代码提供了获取值的方法$name。这种方法称为访问器。它提供了一个否则将被禁止的财产的访问权限。在这种情况下,private可见性和返回属性值的访问器的组合允许任何代码读取值$name,但在设置之后,任何外部都Entree不能更改$name其值。

$ingredients另一方面,该属性protected允许$ingredients从子类访问。这可以确保hasIngredient()方法ComboMeal正常工作。

相同的可见性设置同样适用于方法以及属性。标记的方法public可以由任何代码调用。标记的方法private只能由同一类中的其他代码调用。标记方法protected可以被调用仅由其他代码相同类的内部或内部子类。

命名空间

从5.4版开始,PHP引擎允许您将代码组织到命名空间中。命名空间提供了一种对相关代码进行分组的方法,并确保您编写的类的名称不会与其他人编写的同名类冲突。2

熟悉命名空间非常重要,因此您可以将其他人编写的包合并到您的程序中。第16章详细介绍了如何使用Composer包管理系统。本节将帮助您熟悉命名空间的语法。

将命名空间视为可以容纳类定义或其他命名空间的容器。这是一种语法上的便利,而不是提供新的功能。当您看到namespace关键字或某些似乎是类名的反斜杠时,您遇到了一个PHP命名空间。

要在特定命名空间内定义类,请使用namespace带有命名空间名称的文件顶部的关键字。然后,文件中稍后的类定义将定义该命名空间内的类。例6-14定义了命名空间Fruit内的一个类Tiny。

例6-14。在命名空间中定义类

要使用命名空间中定义的类,需要将命名空间合并到引用类的方式中。最明确的方法是从\(顶级命名空间)开始,然后写入类所在的命名空间的名称,然后添加另一个\,然后编写类名。例如,要调用示例6-14中定义munch()的Fruit类,请编写:

命名空间还可以包含其他命名空间。如果示例6-14开头namespace Tiny\Eating;,则您将该类称为\Tiny\Eating\Fruit。

如果没有这种引导\,如何解析对类的引用取决于当前命名空间 – 在引用时是否处于活动状态。在顶部没有名称空间声明的PHP文件中,当前名称空间是顶级名称空间。类名称的行为类似于您在没有名称空间的情况下遇到的常规类名。该namespace关键字,然而,改变当前的命名空间。声明namespace Tiny;将当前名称空间更改为Tiny。这就是为什么示例6-14中的class Fruit定义将类放在命名空间内的原因。FruitTiny

但是,这也意味着该文件中的任何其他类名称引用都是相对于Tiny命名空间解析的。Tiny\Fruit包含代码的类中的方法$soup = new Entree(‘Chicken Soup’, array(‘chicken’,’water’));告诉PHP引擎在命名空间内查找Entree类。这就好像代码是作为。要明确引用顶级命名空间中的类,您需要在类名前面加一个前导。Tiny$soup = new \Tiny\Entree(‘Chicken Soup’, array(‘chicken’,’water’));\

一遍又一遍地键入所有这些反斜杠和命名空间名称是很痛苦的。PHP引擎为您提供use关键字简化事情。例6-15显示了如何使用use。

例6-15。使用use关键字

编写use Tiny\Eating\Fruit as Snack;告诉PHP引擎,“对于这个文件的其余部分,当我说Snack作为类名时,我的意思是\Tiny\Eating\Fruit。”如果没有as,那么PHP引擎会从给出的最后一个元素中推断出类的“昵称”。use。所以,use Tiny\Fruit;告诉PHP引擎,“对于这个文件的其余部分,当我说Fruit作为类名时,我的意思是\Tiny\Fruit。”

这些类型的use声明对许多现代PHP框架特别有用,它们将各种类放入名称空间和子名称空间。通过use文件顶部的几行,您可以将冗长的咒语转换为\Symfony\Component\HttpFoundation\Response更简洁的咒语Response。

章节总结

本章包括:

  • 了解对象如何帮助您组织代码
  • 使用方法和属性定义类
  • 使用new运算符创建对象
  • 使用箭头运算符访问方法和属性
  • 定义和调用static方法
  • 使用构造函数初始化对象
  • 抛出异常以指示问题
  • 捕获异常来处理问题
  • 使用子类扩展类
  • 通过更改可见性来控制对属性和方法的访问
  • 将代码组织到命名空间中

演习

1. 创建一个名为的类Ingredient。该类的每个实例代表一种成分。实例应该跟踪成分的名称和成本。

2. 在IngredientCost类中添加一个方法,可以更改成分的成本。

3. 创建Entree本章中使用的类的子类,它接受Ingredient对象而不是字符串成分名称来指定成分。为您的Entree子类提供一个返回主菜总成本的方法。

4.将您的Ingredient类放入其自己的命名空间,并修改IngredientCost用于正常工作的其他代码。

 

如果有不懂的地方请留言,SKY8G网站编辑者专注于研究IT源代码研究与开发。希望你下次光临,你的认可和留言是对我们最大的支持,谢谢!

上一篇: PHP | strrev()函数

下一篇: 如何解决Magento 1图片上传按钮丢失问题

登录 评论
avatar