面向对象编程 (OOP) 的关键特征
OOP 为程序增加了额外的结构。我们引入了两个新的结构特征:对象和类。我们还了解了普通变量和过程如何获得额外的规则,并被重新命名为属性和方法。OOP 语言通常也具有普通变量和过程,但我们主要使用面向对象的版本;这就是我们所说的“面向对象” - 我们正在积极地在程序设计和实现中使用对象。
这种额外的结构使我们能够在编程中做一些用普通面向过程的编程无法做到的事情。在后面的部分,我们将看到这些如何帮助构建更大更复杂的程序,同时减少错误数量,并使它们更容易使用。
我们有两个新的结构化概念:类和对象。我们如何创建它们?
类是一种新的数据类型,因此我们在源代码中指定它。通常,我们创建一个新的源文件,并将其命名为我们想要为类命名的名称。因此,“汽车”类可以用名为“Car.src”的源文件描述(在 Java 中:“Car.java”,在 Python 中:“Car.py”等)。
但是我们如何创建新的对象 - 我们如何使用类作为模板为我们创建多个对象?
这个过程被称为实例化:当程序运行时,我们调用类中的一种特殊方法(即过程)来实例化一个新对象。(创建的对象被称为类的实例)这些特殊方法与普通方法相同,但在某些语言中它们具有不同的语法或额外的标签,以明确它们是用于创建新对象的。
因为这些方法具有特殊目的,为了更容易地谈论它们,而不会与对象上的通用方法混淆,我们为可以实例化新对象的方法起了特殊的名字。这些方法被称为构造函数。
回顾
- 构造函数是一种方法。它具有创建(实例化)新对象的额外功能。
- 方法是一种过程。它具有必须是类和/或对象的一部分的额外规则。
- OOP 中的过程与非 OOP 语言中的过程相同。
示例:实例化 dim polo as new car 'instantiation 1
dim escort as new car 'instantiation 2
上面的代码创建了名为 polo 和 escort 的对象,它们都是 car 类类型的(我们之前声明过)。我们现在可以使用所有公共属性和方法 polo.refuel(100) 'assuming fuel starts at 0
polo.drive()
escort.refuel(50) 'assuming fuel starts at 0
escort.drive()
for x as integer = 1 to 20
escort.drive()
polo.drive()
next
polo.refuel(10)
console.writeline("polo: " & polo.getFuel())
console.writeline("escort: " & escort.getFuel())
这将输出以下内容 代码输出
加油! |
练习:实例化一辆汽车 编写你自己的甲壳虫汽车的实例化
答案 dim beetle as new car 'instantiation
以下内容将输出什么 dim ka as new car
dim montego as new car
ka.refuel(10) 'assuming fuel starts at 0
montego.refuel(50) 'assuming fuel starts at 0
montego.drive()
montego.drive()
for x = 0 to 10
montego.drive()
next
ka.refuel(10)
console.writeline("ka: " & ka.getFuel())
console.writeline("montego: " & montego.getFuel())
答案 代码输出
加油! |
在面向过程的编程中,一个方法可用的数据通常对所有方法都可用。这可以手动限制,例如使用私有/受保护/等,参见封装 [此处需要链接],但这可选。通常唯一的限制是提供给每个过程的数据必须匹配特定的 A-level 计算机/AQA/试卷 1/编程基础/数据类型。在定义过程时,我们选择它将接受哪些数据类型(即我们限制了 参数)。
示例:---数据类型:2 个过程,例如 int int 和 string int
当有人尝试使用过程时,计算机会查看提供的数据,并将数据类型与 ....(只要他们提供的数据类型与我们选择的数据类型完全匹配,过程就会执行。)
在面向对象的编程中,所有数据默认情况下都封装在 [出于多种原因 - 需要链接]。与面向过程的编程不同,在面向过程的编程中,任何过程都可以访问程序中任何地方的任何数据,而方法只能直接访问其自身对象中的数据。要访问不同对象中的数据,我们必须将整个对象传递给需要对该数据进行操作的任何方法(或过程)。
这会产生一个问题:参数的数据类型现在将是对象类的类型,每个类都是一个唯一的数据类型。对于简单的案例,这工作得很好,但对于更大的问题,它会阻止我们重复使用方法。这对 OOP 将是灾难性的:我们将需要不断地从一个类复制/粘贴方法到另一个类,调整参数类型,而不是重复使用代码。这将是编程简便性和减少错误方面的倒退。
示例:多态性 例如,如果我们有一个 Pet 类,它具有 Year_of_Birth 变量,还有一个 Owner 类,它也具有 Year_of_Birth 变量,我们有一个方法可以根据 Year_of_Birth 计算当前年龄......它要么可以作用于 Pet 类,要么可以作用于 Owner 类,但它不能同时作用于两者。 |
为了解决这个问题,OOP 语言有一个被称为 多态性 的基本特性。多态性有很多种,但大多数情况下我们只使用其中两种。共同的概念是,一个事物可以假装是多个事物。
多态性的最简单形式是 w:特设多态性,当程序员编写过程的多个不同版本时:例如一个接受类型 A 的对象,一个接受类型 B 的对象。这两个版本具有相同的名称,OOP 语言知道将它们视为相同,但根据过程在运行时调用的方式,智能地使用其中一个或另一个。
OOP 中大量使用的更强大的形式是 w:子类型化。使用子类型多态性,程序员将不同的数据类型相互关联,向计算机承诺 - 在某种程度上 - 这些数据类型可以互换使用。这种机制是继承(见下文)。
示例:多态性 再次考虑我们的汽车示例。当我们创建 public sub refuel(byVal x as integer) as integer
console.writeline("pumping gas!")
fuel = fuel + x
end sub
这不行。我们正在创建一辆电动汽车,我们不想说我们正在加油;我们穿着凉鞋吃酸奶的朋友会怎么说!所以对于 class electricCar
inherits car
private numBatteries as integer
public sub setnumBatteries(byVal n as integer)
numBatteries = n
end sub
public function getnumBatteries() as integer 'interface
return numBatteries
end sub
'###### overrides morphs the inherited version, replacing it
public overrides sub refuel(byVal x as integer)
console.writeline("pure renewable energy!")
fuel = fuel + x
end sub
'######
end class
|
练习:多态性 在多态性中,我们使用什么关键字来重新定义子例程
答案 重写 编写一个使用多态性的豪华轿车类的定义,确保最大速度不超过 100
答案 class limo
inherits car
public overrides sub setSpeed(byVal s as integer)
if s > 100 then
maxSpeed = 100
else
maxSpeed = s
end if
end sub
end class
编写一个肌肉车的定义,它继承了汽车,但每次行驶时使用 30 个单位的燃料,并显示“vroom vroom!”。它还应存储有关真皮座椅的详细信息,允许你与之交互。
答案 class musclecar
inherits car
private leatherSeating as boolean
public overrides sub drive()
fuel = fuel - 30
console.writeline("vroom vroom!")
end sub
public sub setLeather(s as boolean)
leatherSeating = s
end sub
public function getLeather()
return leatherSeating
end function
end class
描述多态性
答案 多态性允许你从父类继承属性,但重新定义某些方法或属性 |
在实践中,上面的多态性的简单版本如果我们最终为每种类型编写了不同版本的类似方法,并不会节省很多程序员的时间。
为了解决这个问题,OOP 语言使用继承(如果类型是作为类实现的,也称为子类化)。
当一个类从另一个类继承时,它会采用另一个类的类型,并采用所有方法和属性。新类可以被视为旧类 - 过程不需要知道它们是看到一个新类还是一个旧类。
原始类通常被称为超类,而新类(继承)类被称为子类。
至关重要的是,子类被允许在继承的属性和方法之上添加额外的属性和方法。因为这些新功能扩展了超类的行为,所以我们通常说子类“扩展”了超类。在某些 OOP 语言中,创建子类的语法是使用关键字“extends”,以及你想要从中继承的超类。
示例:继承 例如,Shape 类可以定义任何形状共有的变量和方法(例如顶点数、颜色、位置),这些变量和方法由子类继承,然后在适当的情况下访问相同的代码,同时提供子类特定的面积方法 - 请参见下面的覆盖。 |
示例:继承 例如,theShape.area() 将调用 theShape 所属类的某个方法,当 theShape 是多边形或椭圆的实例时,这将是不同的方法。 子类型是指类型的层次结构,其中 ellipse 和 polygon 都是超类型 Shape 的子类型。使用的方法可以在运行时选择(在某些语言中),因此调用 theShape.area() 的代码不必知道 theShape 属于什么子类型,只要它提供了一个名为 area 的方法即可。 |
然而,这样做要付出代价 - 超类的更改可能会在某些子类中产生意想不到的副作用。
练习:继承 声明一个名为 limo 的新类,该类具有 numSeats 和 colourSeats 属性;并能够与它们交互
答案 class limo
inherits car 'declares what attributes and methods you are inheriting
private numSeats as integer 'must be private
private colourSeats as integer 'must be private
public sub setnumSeats(byVal s as integer) 'interface set
numSeats = s
end sub
public function getnumSeats() 'interface get
return numSeats
end function
public sub setcolourSeats(byVal c as string) 'interface set
colourSeats = c
end sub
public function getcolourSeats() 'interface get
return colourSeats
end function
end class
使用继承的好处是什么?
答案 从父类创建新类非常快且容易。它允许以模块化方式创建类,在这种情况下,你可能根本不会使用基类。 |
练习:继承 基于上面的汽车示例,如果我们想声明一辆电动汽车会发生什么?嗯,我们可能想存储一些关于它有多少电池的信息。 class electricCar
private maxSpeed as integer
private fuel as integer 'fixed!
private numBatteries as integer 'added
public sub setnumBatteries(byVal n as integer)
numBatteries = n
end sub
public function getnumBatteries()
return numBatteries
end sub
public sub setSpeed(byVal s as integer)
maxSpeed = s
end sub
public function getSpeed() as integer
return maxSpeed
end function
public sub refuel(byVal x as integer) as integer
'.....
'HOLD ON!
end class
这似乎是一项非常漫长而乏味的任务,需要重新编写所有相同的代码。你说得对!如果我们只需要声明所有我们想添加的新内容,那就好多了。OOP 允许继承,其中一个新类可以继承父类的属性和方法。 class electricCar
inherits car 'declares what attributes and methods you are inheriting
private numBatteries as integer 'added
public sub setnumBatteries(byVal n as integer) 'interface
numBatteries = n
end sub
public function getnumBatteries() 'interface
return numBatteries
end function
end class
这意味着 electricCar 现在可以访问 car 声明的所有内容,以及新的 numBatteries 属性和方法。让我们实例化这个例子,看看可能发生什么。 dim gwiz as new electricCar
gwiz.setnumBatteries(6) 'from electricCar
gwiz.setSpeed(60) 'from car
gwiz.drive() 'from car
|
覆盖
[edit | edit source]继承(又名子类化)和多态性在很大程度上解决了方法和属性比普通过程和变量更受限制的问题。但我们很快发现,我们不仅想要扩展超类(即向其现有方法/属性添加内容),而且还想要修改其现有行为。
这非常有用,以至于大多数 OOP 语言都将其作为核心功能支持,这称为覆盖。
当一个子类扩展超类时,它可以选择用新的自定义版本替换超类的任何方法。这种替换行为称为覆盖,旧版本被称为被覆盖。子类中的替换方法可以(但不必)调用超类的覆盖方法,以及执行子类所需的额外操作。
重载
[edit | edit source]大多数 OO 语言允许重新定义标准算术运算符 (+,-,*,/,...),以便它们可以应用于类的成员,或者应用于类和标准变量类型(实数、整数,..)。与多态性一样,适当的方法(必须是类定义的一部分)可以在运行时选择。
例如,表示复数的类 Complex 可以为其实例实现标准算术运算。
从技术上讲,重载是一种 特定多态性,但它被广泛使用,因此拥有自己的名称。