对象
Daniel Shiffman丹尼尔老师
在我们开始研究面向对象编程(OOP)在processing中的工作原理之前,让我们先对“对象”本身进行一个简短的概念性讨论。想象一下,你不是在processing中编程,而是在为你的一天写一个程序——一个指令列表,如果你愿意的话。可能是这样开始的:
- 早上起床
- 喝一杯茶
- 吃早餐
- 坐地铁
这里涉及什么?具体来说,涉及哪些方面?首先,虽然从我们写上述指示的方式来看可能不是很明显,但最主要的是你一个人。你展示了某些特性。你看起来有点像;也许你有棕色的头发,戴着眼镜,看起来有点呆板。你也有能力做一些事情,比如起床(想必,你也可以睡觉),吃饭,或者坐地铁。一个物体就像你一样,一个有属性,可以做事情的东西。
那么,这与编程有什么关系呢?对象的属性是变量,对象可以做的事情是函数。面向对象编程是所有编程基础的结合:数据和功能。
让我们为一个非常简单的人类对象绘制数据和函数:
人的数据或属性
•高度。
•重量。
•性别。
•眼睛颜色。
•头发颜色。
人所能做的功能或能力
•睡觉。
•醒来。
•吃。
•乘坐某种交通工具。
现在,在我们进一步深入之前,我们需要开始一个简短的形而上学的离题。上面的结构不是一个人本身;它只是描述了一个人背后的想法或概念。它描述了人的本质。做人就是要有身高,头发,睡觉,吃饭等等。这是编程对象的一个重要区别。这个人类模板被称为类。类与对象不同。你是一个物体。我是一个对象。地铁里的那个家伙是个目标。爱因斯坦是一个物体。我们都是人,真实世界的人的想法的实例。
想想饼干刀。切饼干的人会做饼干,但它本身不是饼干。cookie cutter是类,cookie是对象。
使用一个对象
在我们研究类本身的实际编写之前,让我们简单地看看在主程序中使用对象(即setup()和draw())是如何使世界变得更好的。
考虑一个简单的草图的伪代码,它将一个矩形水平地移动到窗口上(我们将把这个矩形看作一个“汽车”)。
数据(全局变量):
•汽车颜色。
•x车位置。
•y车位置。
•汽车x速度。
设置:
•初始化汽车颜色。
•将车辆位置初始化到起点。
•初始化车速。
绘制:
•填充背景。
•在有颜色的位置展示汽车。
•按速度增加轿厢位置。
为了实现上述伪代码,我们将在程序顶部定义全局变量,在setup()中初始化它们,并调用函数在draw()中移动和显示car。类似于:
color c = color(0); float x = 0; float y = 100; float speed = 1; void setup() { size(200,200); } void draw() { background(255); move(); display(); } void move() { x = x + speed; if (x > width) { x = 0; } } void display() { fill(c); rect(x,y,30,10); }
面向对象编程允许我们从主程序中取出所有变量和函数,并将它们存储在car对象中。一个汽车对象将知道它的数据颜色,位置,速度。对象还将知道它可以做什么,方法(对象内部的功能)-汽车可以驾驶,它可以显示。
使用面向对象的设计,伪代码改进为如下所示:
数据(全局变量):
•汽车物件。
设置:
•初始化车辆对象。
绘制:
•填充背景。
•显示车辆对象。
•驾驶汽车物体。
注意:我们从第一个示例中删除了所有全局变量。我们现在只有一个变量:汽车变量,而不是汽车颜色、汽车位置和车速的单独变量!而且,我们没有初始化这三个变量,而是初始化一个东西:Car对象。这些变量去了哪里?它们仍然存在,只是现在它们生活在Car对象中(并将在Car类中定义,稍后我们将讨论)。
超越伪代码,草图的实际主体可能看起来像:
Car myCar; void setup() { myCar = new Car(); } void draw() { background(255); myCar.drive(); myCar.display(); }
我们稍后将详细介绍上述代码,但在我们这样做之前,让我们先看看Car类本身是如何编写的。
上面的简单Car示例演示了如何在processing中使用对象来生成干净、可读的代码。困难的工作是编写对象模板,也就是类本身。当你第一次学习面向对象编程的时候,经常会有一个很有用的练习,那就是编写一个没有对象的程序,并且在不改变功能的情况下,用对象重写它。我们将在汽车示例中这样做,以面向对象的方式重新创建完全相同的外观和行为。
所有类都必须包含四个元素:名称、数据、构造函数和方法。(从技术上讲,唯一实际需要的元素是类名,但进行面向对象编程的目的是包括所有这些元素。)
下面是我们如何从一个简单的非面向对象的草图中提取元素并将它们放入Car类中,然后我们就可以从中创建Car对象了。
类名:名称由“Class WhateverNameYouChoose”指定。然后,我们将该类的所有代码放在name声明之后的花括号中。类名通常是大写的(以区别于变量名,变量名通常是小写的)。
数据:类的数据是变量的集合。这些变量通常称为实例变量,因为对象的每个实例都包含这组变量。
构造函数(功能):构造函数是类内部的一个特殊函数,用于创建对象本身的实例。在这里,您可以给出如何设置对象的说明。它就像处理的setup()函数一样,只是在这里,当从这个类创建新对象时,它被用来在草图中创建一个单独的对象。它始终与类同名,并通过调用新的运算符来调用:“Car myCar=new Car();”。
功能:我们可以通过编写方法向对象添加功能。
请注意,类的代码作为自己的块存在,可以放在setup()和draw()之外的任何地方。
void setup() { } void draw() { } class Car { }
使用对象:细节
在前面,我们快速浏览了一个对象如何能够极大地简化处理草图的主要部分(即setup()和draw())。
//第一步:声明一个对象。 Car myCar; void setup() { // 第二步:初始化对象。 myCar = new Car(); } void draw() { background(255); //第三步:对象调用方法。 myCar.drive(); myCar.display(); }
让我们看一下上面三个步骤后面的细节,概述如何在草图中使用对象。
第一步.声明对象变量
变量总是通过指定类型和名称来声明的。对于基本数据类型(如整数),它如下所示:
//变量声明 int var;//类型名
原始数据类型是奇异的信息片段:整数、浮点、字符等。声明一个保存在对象上的变量非常相似。不同之处在于,这里的类型是类名,在本例中是“Car”。顺便说一句,对象不是原语,被视为复杂的数据类型。(这是因为它们存储多条信息:数据和功能。原语只存储数据。)
第二步.初始化对象。
为了初始化一个变量(即给它一个起始值),我们使用赋值操作变量等于某个值。对于一个基元(例如整数),它看起来如下:
//变量初始化 var=10;//var被赋予10
初始化对象有点复杂。我们必须构造对象,而不是像使用整数或浮点数那样简单地给它赋值。使用新运算符生成对象。
//对象初始化 myCar=new Car();//new运算符用于生成新对象。
在上面的示例中,“myCar”是对象变量名,“=”表示我们将其设置为某个值,即某个值是Car对象的新实例。我们真正要做的是初始化一个Car对象。当初始化一个原始变量(如整数)时,只需将其设置为一个数字。但一个对象可能包含多个数据片段。回忆一下Car类,我们看到这行代码调用构造函数,一个名为Car()的特殊函数,它初始化对象的所有变量,并确保Car对象准备就绪。
还有一件事:对于原始整数“var”,如果您忘记初始化它(将其设置为10),那么处理将为它分配一个默认值0。但是,对象(例如“myCar”)没有默认值。如果忘记初始化对象,则处理将为其提供空值。空的意思是没有。不是零。不是消极的。完全的虚无。空虚。如果在消息窗口中遇到一个表示“NullPointerException”的错误(这是一个非常常见的错误),则该错误很可能是由于忘记初始化对象而导致的。
第三步。使用对象
一旦我们成功地声明并初始化了一个对象变量,我们就可以使用它。使用对象涉及调用内置于该对象中的函数。人可以吃东西;汽车可以开;狗可以叫。在对象内部调用函数是通过点语法完成的:variableName.objectFunction(函数参数);
对于汽车,所有可用的函数都没有参数,因此看起来像:
//函数使用“点语法”调用。 myCar.drive(); myCar.display();
构造函数参数
在上面的例子中,car对象使用new运算符和类的构造函数初始化。
Car myCar=新车();
当我们学习面向对象编程的基础知识时,这是一个有用的简化。然而,上面的代码有一个相当严重的问题。如果我们想写一个有两个car对象的程序呢?
//创建两个汽车对象 Car myCar1=新车(); Car myCar2=新车();
这实现了我们的目标;代码将生成两个car对象,一个存储在变量myCar1中,另一个存储在变量myCar2中。但是,如果你学习汽车课程,你会注意到这两辆车是一样的:每辆车都是白色的,从屏幕中间开始,速度为1。在英语中,上面写着:
造一辆新车。
我们想说:
在位置(0,10)以1的速度制造一辆新的红色汽车。
所以我们也可以说:
在位置(0,100)以2的速度制造一辆新的蓝色汽车。
我们可以通过在构造函数方法中放置参数来实现这一点。
Car myCar = new Car(color(255,0,0),0,100,2);
必须重写构造函数以合并这些参数:
Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) { c = tempC; xpos = tempXpos; ypos = tempYpos; xspeed = tempXspeed; }
根据我的经验,使用构造函数参数初始化对象变量可能有些令人困惑。请不要自责。这段代码看起来很奇怪,而且看起来非常多余:“我需要为每个变量在构造函数中放置参数?”
然而,这是一项非常重要的学习技能,并且最终是使面向对象编程强大的因素之一。但现在,它可能会感到痛苦。让我们看看参数在这种情况下是如何工作的。
参数是在函数体中使用的局部变量,当调用函数时,这些局部变量将被值填充。在这些例子中,它们只有一个目的:初始化对象内部的变量。这些是计算汽车实际颜色、汽车实际x位置等的变量。构造函数的参数只是临时的,并且只存在于将值从对象生成的位置传递到对象本身。
这允许我们使用同一个构造函数创建各种对象。你也可以在你的参数名中写下temp这个词来提醒你发生了什么(c vs.tempC)。您还将看到程序员在许多示例中使用下划线(c与c)。当然,你想叫什么就叫什么。然而,明智的做法是选择一个对你有意义的名字,并且要保持一致。
现在,我们可以查看具有多个对象实例的同一草图,每个实例都具有唯一的属性。
//示例:两个汽车对象 Car myCar1; Car myCar2; // Two objects! void setup() { size(200,200); // 构造对象时,参数位于括号内。 myCar1 = new Car(color(255,0,0),0,100,2); myCar2 = new Car(color(0,0,255),0,10,1); } void draw() { background(255); myCar1.drive(); myCar1.display(); myCar2.drive(); myCar2.display(); } //即使有多个对象,我们仍然只需要一个类。 //不管我们做了多少曲奇,只需要一个曲奇切割器。 class Car { color c; float xpos; float ypos; float xspeed; // 构造函数是用参数定义的。 Car(color tempC, float tempXpos, float tempYpos, float tempXspeed) { c = tempC; xpos = tempXpos; ypos = tempYpos; xspeed = tempXspeed; } void display() { stroke(0); fill(c); rectMode(CENTER); rect(xpos,ypos,20,10); } void drive() { xpos = xpos + xspeed; if (xpos > width) { xpos = 0; } } }
对象也是数据类型!
假设这是您第一次体验面向对象编程,那么很重要的一点是要轻松。这里的示例只有一个类,最多可以从该类生成两个或三个对象。然而,没有实际的限制。一个处理草图可以包含你想写的任意多个类。
例如,如果您正在编程“太空入侵者”游戏,则可以为游戏中的每个实体使用一个对象创建一个太空船类、一个敌人类和一个子弹类。
此外,尽管类不是原始的,但它是数据类型,就像整数和浮点数一样。由于类是由数据组成的,因此一个对象可以包含其他对象!例如,假设您刚刚完成了一个Fork-and-Spoon类的编程。转到PlaceSetting类,您可能会在该类本身中同时包含Fork对象和Spoon对象的变量。这在面向对象编程中是非常合理和常见的。
class PlaceSetting { Fork fork; Spoon spoon; PlaceSetting() { fork = new Fork(); spoon = new Spoon(); } }
对象,就像任何数据类型一样,也可以作为参数传递给函数。在太空入侵者游戏的例子中,如果宇宙飞船向敌人发射子弹,我们可能会想在敌人类中编写一个函数来确定敌人是否被子弹击中。
void hit(Bullet b) { //确定是否 //子弹击中了敌人 }
在函数中传递原语值(整数、浮点数等)时,将进行复制。对于对象,情况并非如此,结果更直观一些。如果在对象传递到函数后对其进行了更改,则这些更改将影响整个草图中其他任何地方使用的对象。这称为按引用传递,因为对实际对象本身的引用将传递给函数,而不是副本。