diff --git a/3.编程思维体系构建/3.4.6.10.增添属性.md b/3.编程思维体系构建/3.4.6.10.增添属性.md index d830f1f..7b7cce2 100644 --- a/3.编程思维体系构建/3.4.6.10.增添属性.md +++ b/3.编程思维体系构建/3.4.6.10.增添属性.md @@ -14,12 +14,12 @@ 假设我们的洞口被警卫挡住了。玩家就过不去,我们可以简单地将通道的目的地更改为终点位置(或 NULL),但这会导致对诸如 go cave 和 look cave 这样的命令做出不正确的回应:“你在这里看不到任何洞穴。我们需要一个将通道的实际终点和虚假终点分开的单独属性。为此,我们将引入一个属性 prospect 来表示后者。 -1. 在许多冒险中,玩家以及游戏中的 NPC 在携带量方面受到限制。给每件物品一个重量,角色库存中所有物品的总重量不应超过该角色所能承载的最大重量。当然,我们也可以给一个物体一个非常高的重量,使它不可移动(一棵树,一座房子,一座山)。 -2. RPG 式的冒险游戏需要角色的整个属性范围( 玩家与非玩家 ),例如 HP。HP 为零的对象要么死了,要么根本不是角色。 +1. 在许多冒险中,玩家以及游戏中的 NPC 在携带量方面受到限制。给每件物品一个重量,角色库存中所有物品的总重量不应超过该角色所能承载的最大重量。当然,我们也可以给一个物体一个非常高的重量,使它不可移动(一棵树,一座房子,一座山)。 +2. RPG 式的冒险游戏需要角色的整个属性范围 ( 玩家与非玩家 ),例如 HP。HP 为零的对象要么死了,要么根本不是角色。 我们在 object.txt 中定义了七个新属性: -``` +```c #include #include "object.h" @@ -115,13 +115,14 @@ extern OBJECT objs[]; textGo "Solid rock is blocking the way." ``` -注意:textGo 不仅对通道对象有用,而且对非通道对象也有用( 在这种情况下,以后我们将介绍“墙”这个概念) +注意:textGo 不仅对通道对象有用,而且对非通道对象也有用 ( 在这种情况下,以后我们将介绍“墙”这个概念) -思考题:你能否自行实现上述伪代码? +::: warning 🤔 思考题:你能否自行实现上述伪代码? +::: -现在,我们已经可以使用新属性(如果你完成了上面的思考题),details 用于新识别的命令外观``,textGo 在我们的命令 go 实现中替换固定文本“OK”。 +现在,我们已经可以使用新属性 (如果你完成了上面的思考题),details 用于新识别的命令外观``,textGo 在我们的命令 go 实现中替换固定文本“OK”。 -# location.c +## location.c ```c #include @@ -195,7 +196,7 @@ void executeGo(const char *noun) 属性权重和容量一起成为不能将某些对象移动到周围的可能原因。而 HP 检查代替了角色的硬编码白名单。 -# move.c +## move.c ```c #include @@ -264,7 +265,7 @@ void moveObject(OBJECT *obj, OBJECT *to) 这里还有一个模块可以使用 HP 来识别角色。 -# inventory.c +## inventory.c ```c #include @@ -327,7 +328,8 @@ void executeInventory(void) } ``` -思考题:仔细观察这段代码,看看与你写的有何不同? +::: warning 🤔 思考题:仔细观察这段代码,看看与你写的有何不同? +::: 权重检查利用了新功能 weightOfContents,它将在misc.c中实现。在同一模块中,我们还对一些现有函数进行了修改,以支持最后几个属性。 @@ -335,7 +337,7 @@ void executeInventory(void) 在函数 getPassage 中我们将属性目标替换为 prospect,并改进对所有命令(而不仅仅是 go and look)的响应,这些命令应用于位于“隐藏通道”另一端的位置。 -# misc.h +## misc.h ```c typedef enum { @@ -357,7 +359,7 @@ extern OBJECT *actorHere(void); extern int listObjectsAtLocation(OBJECT *location); ``` -# misc.c +## misc.c ```c #include @@ -432,11 +434,11 @@ int listObjectsAtLocation(OBJECT *location) } ``` -思考题:为什么上面的 getPassage 函数使用了函数指针这种语法? +::: warning 🤔 思考题: +为什么上面的 getPassage 函数使用了函数指针这种语法? -``` - 函数指针和指针函数有什么区别? -``` +函数指针和指针函数有什么区别? +::: 为了使整个画面完整,最好扩展前面生成的地图,我们可以用虚线表示“明显”的通道。 @@ -462,7 +464,7 @@ function outputEdge(from, to, style) - 尽量不要太担心浪费仅在某些类型的对象中使用的属性上的内存空间(例如,textGo仅用于通道),或者许多重复的字符串文本。 - 为了演示属性 prospect 的使用,我们使洞穴无法访问。当您查看新地图时,这一点立即变得很明显。进入洞穴的箭头是虚线的,这意味着这是一个虚假的通道,但不是实际的通道。请放心,洞穴将在下一章重新开放。 -- 请注意,更详细的描述往往需要一个更大的字典(更多的对象,更多的标签)。例如,命令 look silver coin 现在返回 "该硬币的正面有一只鹰"。玩家通过输入一个命令 look eagle 来查看银币,但程序并不知道鹰是什么意思(显然这样子是不行的)。 +- 请注意,更详细的描述往往需要一个更大的字典(更多的对象,更多的标签)。例如,命令 look silver coin 现在返回 "该硬币的正面有一只鹰"。玩家通过输入一个命令 look eagle 来查看银币,但程序并不知道鹰是什么意思 (显然这样子是不行的)。 输出样例 diff --git a/3.编程思维体系构建/3.4.6.2.探索未知.md b/3.编程思维体系构建/3.4.6.2.探索未知.md index a4a8279..27b9b29 100644 --- a/3.编程思维体系构建/3.4.6.2.探索未知.md +++ b/3.编程思维体系构建/3.4.6.2.探索未知.md @@ -1,8 +1,6 @@ # 2.探索未知 -::: tip 提醒 - -## 驾驭项目,而不是被项目驾驭 +::: tip 驾驭项目,而不是被项目驾驭 你和一个项目的关系会经历 4 个阶段: @@ -31,10 +29,10 @@ 那么,当命令很多的时候,如果你将他写在一起,一个文件有五六千行,我相信这样的情况你是不愿意去看的,因此,我们引入了函数的概念。 -自行了解函数的概念,同时去了解当我需要引用别的文件的函数时该怎么办? - -了解一下什么是“驼峰原则”,我们为什么要依据它命名函数? +::: warning 🤔 自行了解函数的概念,同时去了解当我需要引用别的文件的函数时该怎么办? + 了解一下什么是“驼峰原则”,我们为什么要依据它命名函数? +::: 下面的代码示例包含三个函数,每个步骤一个函数: 1. 函数getInput。 @@ -69,7 +67,8 @@ int main() 注意:某些老版本的 C 语言不支持 bool 选项,你将他改为 int 是一样的。 -思考题:static 是什么意思?我为什么要用他? +::: warning 🤔 思考题:static 是什么意思?我为什么要用他? +::: ## parsexec.h @@ -77,13 +76,15 @@ int main() extern bool parseAndExecute(char *input); ``` -思考题:extern 是干什么的?.h 文件又在干嘛? +::: warning 🤔 思考题: +extern 是干什么的?.h 文件又在干嘛? 哇,我用了一个指针!input 前面是个指针!!! 指针是啥?[C 指针详解](https://www.runoob.com/w3cnote/c-pointer-detail.html) STFW(虽然都给你了) 在这里用指针是为了传参的时候可以传字符串哦 +::: ## parsexec.c diff --git a/3.编程思维体系构建/3.4.6.3.指明地点.md b/3.编程思维体系构建/3.4.6.3.指明地点.md index a6fe8ea..7d6b06b 100644 --- a/3.编程思维体系构建/3.4.6.3.指明地点.md +++ b/3.编程思维体系构建/3.4.6.3.指明地点.md @@ -1,10 +1,10 @@ # 3.指明地点 -某种极其糟糕的编程习惯 - -## Copy-paste +::: warning 某种极其糟糕的编程习惯 +Copy-paste 我们很多同学在编程的过程中,可能会写出一大堆重复性很强的代码,在最近看的 pa 中,举了这样一个例子,你不需要看懂只需要感受到就可: +::: ```c if (strcmp(s, "$0") == 0) @@ -86,7 +86,7 @@ bx = torch.cat((xs[0], bs[0], xs[1], bs[1], xs[2], bs[2], xs[3], bs[3], xs[4], b xs[24], bs[24], xs[25], bs[25], xs[26], bs[26], xs[27], bs[27], xs[28], bs[28], xs[29], bs[29], xs[30], bs[30], xs[31], bs[31]), 1) ``` -你想想,你遇到这么长的代码,你愿意看他吗? +::: tip 你想想,你遇到这么长的代码,你愿意看他吗? 更可怕的是,这种编码模式可能会导致意想不到的 bug。 @@ -97,6 +97,7 @@ bx = torch.cat((xs[0], bs[0], xs[1], bs[1], xs[2], bs[2], xs[3], bs[3], xs[4], b 后来周源源教授发现,相比于操作系统,应用程序的源代码中 Copy-Paste 的现象更加普遍。于是她们团队把 CP-Miner 的技术应用到应用程序的源代码中,并创办了 PatternInsight 公司。很多 IT 公司纷纷购买 PatternInsight 的产品,并要求提供相应的定制服务,甚至 PatternInsight 公司最后还被 VMWare 收购了。 这个故事折射出,大公司中程序员的编程习惯也许不比你好多少,他们也会写出 Copy-Paste 这种难以维护的代码。但反过来说,重视编码风格这些企业看中的能力,你从现在就可以开始培养。 +::: 传统上,文本冒险是由(许多)不同位置组成的虚拟世界。虽然这不是必需的(一些冒险发生在一个房间里!),但这是解释数据结构使用的好方法。 @@ -112,7 +113,7 @@ struct location { }; ``` -::: warning 🤔思考题: +::: warning 🤔 思考题: 我们为什么要用结构体来保存位置? 这样子做有什么好处? diff --git a/3.编程思维体系构建/3.4.6.4.创建对象.md b/3.编程思维体系构建/3.4.6.4.创建对象.md index d4af0c3..3a7d1ab 100644 --- a/3.编程思维体系构建/3.4.6.4.创建对象.md +++ b/3.编程思维体系构建/3.4.6.4.创建对象.md @@ -1,6 +1,6 @@ # 4.创建对象 -在我们继续之前,我在这里使用的是哲学意义上的“对象”一词。它与面向对象编程无关,也与JavaC#Python等编程语言中预定义的“对象”类型没有任何共同之处。下面,我将定义一个名为 object 的结构体。。 +在我们继续之前,我在这里使用的是[哲学意义上](https://en.wikipedia.org/wiki/Object_(philosophy))的“对象”一词。它与[面向对象编程](https://en.wikipedia.org/wiki/Object-oriented_programming)无关,也与JavaC#Python等编程语言中预定义的“对象”类型没有任何共同之处。下面,我将定义一个名为 object 的结构体。 冒险游戏中的大多数谜题都围绕着物品。例子: @@ -47,9 +47,11 @@ objs[] = { 我们发现 OBJECT 的结构体里面有一个指针和自己长得一样,不用担心,这和链表的操作类似。 -思考题:链表是什么,为什么要有这么一个操作指针? +::: warning 🤔 思考题: +链表是什么,为什么要有这么一个操作指针? 链表和数组有什么异同点,他们分别在增删改查上有什么优劣? +::: 为了更容易地用那些所谓的物品或者是地点,我们将为每个元素定义一个名字 @@ -80,7 +82,8 @@ for (obj = objs; obj < objs + 5; obj++) } ``` -暂停理解一下吧 +::: warning 🤔 暂停理解一下吧 +::: 那么,我们有合并这个物品(或地点)列表有什么好处呢?答案是这会让我们的代码变得更加简单,因为许多函数(如上面的函数通过这样的列表)只需要扫描单个列表就可以实现,而不是三个列表。有人可能会说没必要,因为每个命令仅适用于一种类型的对象: @@ -96,7 +99,8 @@ for (obj = objs; obj < objs + 5; obj++) 将所有对象放在一个大列表中,很容易添加一个名为“type”的属性来构造对象,以帮助我们区分不同类型的对象。 -怎么做怎么遍历呢?先思考吧 +::: warning 🤔 怎么做怎么遍历呢?先思考吧 +::: 但是,对象通常具有同样有效的其他特征: @@ -169,7 +173,8 @@ OBJECT objs[] = { extern OBJECT *getVisible(const char *intention, const char *noun); ``` -## 指针?函数?希望你已经掌握这是什么了 +::: warning 🤔 指针?函数?希望你已经掌握这是什么了 +::: ## noun.c diff --git a/3.编程思维体系构建/3.4.6.5.捡起物品.md b/3.编程思维体系构建/3.4.6.5.捡起物品.md index 9dc1d45..a2ea2bb 100644 --- a/3.编程思维体系构建/3.4.6.5.捡起物品.md +++ b/3.编程思维体系构建/3.4.6.5.捡起物品.md @@ -26,9 +26,11 @@ 你可以尝试去使用这些命令(上面的前两个示例已经在上一章中实现了)。现在,我们将为玩家和非玩家角色介绍一些典型的物品栏操作(命令获取掉落给予询问物品栏)。 -思考题:你能不能尝试自己实现一下上面的命令? +::: warning 🤔 思考题: +你能不能尝试自己实现一下上面的命令? 如果你可以在不参考下面内容的情况下就写出基本内容会有很大收获的 +::: ## parsexec.c @@ -160,9 +162,11 @@ void executeInventory(void) 注意:由于动词名词比较好弄,命令 askgive 只有一个参数:item。 -思考题:为什么我们要这样设计? +::: warning 🤔 思考题: +为什么我们要这样设计? 你能否为这些命令多加几个参数? +::: 从本质上讲,get, drop, give and ask 这些命令除了将项目从一个地方移动到另一个地方之外,什么都不做。单个函数 move 对象可以对所有四个命令执行该操作。 @@ -222,7 +226,8 @@ void moveObject(OBJECT *obj, OBJECT *to) } ``` -思考题:识别一些我们拿不了的物品需要考虑什么因素? +::: warning 🤔 思考题:识别一些我们拿不了的物品需要考虑什么因素? +::: 命令“get”使用函数getVisible将名词转换为 object,就像命令“go”一样;请参阅上一章。但是对于对玩家(或其他一些参与者)已经持有的对象进行drop, ask, give 等命令时,我们需要稍微不同的东西。我们将在 noun.c 中添加一个函数 getPossession。 @@ -362,7 +367,8 @@ int listObjectsAtLocation(OBJECT *location) } ``` -思考题:上面第四行中的函数 actorHere 返回的指针指向什么? +::: warning 🤔 思考题:上面第四行中的函数 actorHere 返回的指针指向什么? +::: 在第 9 行中,有一个详尽的,硬编码的非玩家角色列表(到目前为止,只有一个:守卫)。 diff --git a/3.编程思维体系构建/3.4.6.6.绘制地图.md b/3.编程思维体系构建/3.4.6.6.绘制地图.md index 015960c..882e064 100644 --- a/3.编程思维体系构建/3.4.6.6.绘制地图.md +++ b/3.编程思维体系构建/3.4.6.6.绘制地图.md @@ -28,11 +28,12 @@ struct object { - 通道总是朝一个方向运行;要双向连接两个位置,我们总是必须创建两个单独的通道。乍一看,这似乎很笨拙,但它确实给了我们很大的灵活性来完善命令“go”的行为 - 在大地图上,你可能会发现手动创建所有通道很乏味。所以,我强烈建议你使用自定义工具生成地图中重复性更强的部分。这里不会介绍这一点,但您可能会在第 9 章中找到一些灵感,我们将在其中讨论自动胜场。 -思考题:为什么创建两个通道可以使我们的程序更加灵活? +::: warning 🤔 思考题:为什么创建两个通道可以使我们的程序更加灵活? +::: 接下来我们将展开对象数组 -# object.h +## object.h ```c typedef struct object { @@ -56,7 +57,7 @@ extern OBJECT objs[]; #define endOfObjs (objs + 8) ``` -# object.c +## object.c ```c #include @@ -76,7 +77,7 @@ OBJECT objs[] = { 我们将在 misc.c 中添加一个小的帮助函数,以确定两个给定位置之间是否存在通道。 -# misc.h +## misc.h ```c extern OBJECT *getPassage(OBJECT *from, OBJECT *to); @@ -84,7 +85,7 @@ extern OBJECT *actorHere(void); extern int listObjectsAtLocation(OBJECT *location); ``` -# misc.c +## misc.c ```c #include @@ -140,14 +141,14 @@ int listObjectsAtLocation(OBJECT *location) 我们将在命令“go”的实现中使用新功能getPassage来确定是否存在可以将玩家带到所需位置的通道。 -# location.h +## location.h ```c extern void executeLook(const char *noun); extern void executeGo(const char *noun); ``` -# location.c +## location.c ```c #include @@ -177,7 +178,7 @@ void executeGo(const char *noun) // already handled by getVisible } else if (getPassage(player->location, obj) != NULL) - //go只会在有地方的时候才会运行起来 + //go 只会在有地方的时候才会运行起来 { printf("OK.\n"); player->location = obj; @@ -202,14 +203,14 @@ void executeGo(const char *noun) 我们还将使用新功能getPassage来确定从玩家站立的位置是否可以看到某个位置。未通过通道连接到当前位置的位置不被视为可见。 -# noun.h +## noun.h ```c extern OBJECT *getVisible(const char *intention, const char *noun); extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun); ``` -# noun.c +## noun.c ```c #include @@ -294,11 +295,12 @@ OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun) 显然,此示例中的地图是微不足道的:只有两个位置,并且它们在两个方向上都连接在一起。第 12 章将增加第三个地点。 -思考题:你能否绘制一张更精细的地图,并将其变成对象列表(位置和通道) +::: warning 🤔 思考题: +你能否绘制一张更精细的地图,并将其变成对象列表(位置和通道) -``` - 注:不用当成任务,自行实验即可 -``` +注:不用当成任务,自行实验即可 + +::: 输出样例 diff --git a/3.编程思维体系构建/3.4.6.7.增大距离.md b/3.编程思维体系构建/3.4.6.7.增大距离.md index bcb0ce3..361b834 100644 --- a/3.编程思维体系构建/3.4.6.7.增大距离.md +++ b/3.编程思维体系构建/3.4.6.7.增大距离.md @@ -1,6 +1,6 @@ # 7.增大距离 -一个典型的冒险包含许多谜题。众所周知,Infocom的冒险很难完成。解决每个难题可能需要数周甚至数月的反复试验。 +一个典型的冒险包含许多谜题。众所周知,[Infocom](https://en.wikipedia.org/wiki/Infocom)的冒险很难完成。解决每个难题可能需要数周甚至数月的反复试验。 当玩家操纵角色失败后,如果只是返回“你不能这么操作”来回应玩家,会很 nt,很没意思 @@ -10,7 +10,7 @@ 冒险游戏至少应该做的是解释为什么玩家的命令无法完成:“你不能这样做,因为......”这有助于使虚拟世界更具说服力,故事更可信,游戏更有趣。 -我们已经付出了相当大的努力让游戏解释为什么某些命令是无效的。只需看看名词.c,inventory.c,location.cmove.c中的许多printf调用。 但随着游戏变得越来越复杂,这正成为一个相当大的负担。我们需要一种更结构化的方法来检测和处理错误情况。这就是我们在本章中将要讨论的内容。 +我们已经付出了相当大的努力让游戏解释为什么某些命令是无效的。只需看看名词.c,inventory.c,location.cmove.c中的许多printf调用。但随着游戏变得越来越复杂,这正成为一个相当大的负担。我们需要一种更结构化的方法来检测和处理错误情况。这就是我们在本章中将要讨论的内容。 大多数命令对一个或多个对象进行操作,例如: @@ -64,7 +64,8 @@ typedef enum { } DISTANCE; ``` -typedef 以及枚举类 enum 之前有接触过吗?没有接触过的话就去学习一下吧。 +::: warning 💡 typedef 以及枚举类 enum 之前有接触过吗?没有接触过的话就去学习一下吧。 +::: 在最右边的列中,我们为每个情况提出了一个满足条件。通过一些重新洗牌,我们可以很容易地将其转换为计算对象“距离”的函数(从玩家的角度来看): @@ -84,9 +85,11 @@ DISTANCE getDistance(OBJECT *from, OBJECT *to) } ``` -思考题:你是否有其他方法实现这个功能? +::: warning 🤔 思考题: +你是否有其他方法实现这个功能? 注:自行实验即可 +::: 就这样!我们可以调用此函数并对其返回值进行比较。例如,我们在 noun.c中有以下代码: @@ -110,7 +113,7 @@ else if (!(getDistance(player, obj) == distSelf || getDistance(player, obj) == distHere || getDistance(player, obj) == distOverthere || getDistance(player, obj) == distHeldContained || - getDistance(player, obj) == distHereContained) + getDistance(player, obj) == distHereContained)) ``` 这可以简化为: @@ -119,13 +122,14 @@ else if (!(getDistance(player, obj) == distSelf || else if (getDistance(player, obj) >= distNotHere) ``` -尝试理解一下这样做的意义 +::: warning 🤔 尝试理解一下这样做的意义 +::: 这只是一个例子,让你对这个概念有所了解;您将在下面找到noun.c的实际实现,看起来略有不同。 是时候把事情落实到位了。枚举 DISTANCE 和函数 getDistance 的定义被添加到 misc.hmisc.c 中,因为我们将在多个模块中使用它们。 -# misc.h +## misc.h ```c typedef enum { @@ -150,7 +154,7 @@ extern OBJECT *actorHere(void); extern int listObjectsAtLocation(OBJECT *location); ``` -# misc.c +## misc.c ```c #include @@ -226,7 +230,7 @@ int listObjectsAtLocation(OBJECT *location) 注意:isHolding 这个函数之后我们将在各个地方使用 -# location.h +## location.h ```c extern void executeLook(const char *noun); @@ -235,7 +239,7 @@ extern void executeGo(const char *noun); 在函数 executeGo 中,我们可以用检查距离来替换大多数 if 条件。 -# location.c +## location.c ```c #include @@ -290,11 +294,12 @@ void executeGo(const char *noun) } ``` -思考题:你能否为 switch 函数增加更多 case 来完善判断条件? +::: warning 🤔 思考题:你能否为 switch 函数增加更多 case 来完善判断条件? +::: 函数 executeGet 也是如此。 -# inventory.h +## inventory.h ```c extern void executeGet(const char *noun); @@ -304,7 +309,7 @@ extern void executeGive(const char *noun); extern void executeInventory(void); ``` -# inventory.c +## inventory.c ```c #include @@ -369,14 +374,14 @@ void executeInventory(void) 最后,我们将调整 noun.c中的约束。我们正在向函数getObject添加两个参数,以便找到特定名词的匹配项,同时忽略任何被认为不存在的对象。这将在下一章中得到真正的回报,我们将在下一章中介绍具有相同标签的不同对象。 -# noun.h +## noun.h ```c extern OBJECT *getVisible(const char *intention, const char *noun); extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun); ``` -# noun.c +## noun.c ```c #include @@ -455,7 +460,8 @@ OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun) } ``` -思考题:你能理解什么时候加 const,什么时候不用吗? +::: warning 🤔 思考题:你能理解什么时候加 const,什么时候不用吗? +::: 在本章中,距离的概念主要用于在游戏可以给用户的不同响应之间进行选择。但是,距离的好处并不局限于输出端;它可以同样很好地用于在输入端进行改进。在下一章中,我们将使用距离来提高对用户输入的名词的识别。 diff --git a/3.编程思维体系构建/3.4.6.8.移动方向.md b/3.编程思维体系构建/3.4.6.8.移动方向.md index 443c99e..a7220d9 100644 --- a/3.编程思维体系构建/3.4.6.8.移动方向.md +++ b/3.编程思维体系构建/3.4.6.8.移动方向.md @@ -1,13 +1,14 @@ # 8.移动方向 -传统的文本冒险使用指南针方向进行导航。 +传统的文本冒险使用[指南针方向](https://en.wikipedia.org/wiki/Cardinal_direction)进行导航。 例如,我们在第 6 章中绘制的地图上,玩家可能想向东移动,从田野移动到洞穴。我们可以通过给连接Cave的通道标上“east”来实现这一点。但是,我们首先需要解决两个问题。 1. 我们可能仍然想把这段通道称为“entrance”和“east”。但现在,一个对象只能有一个标签。 2. 在更大的地图上,具有更多的位置和道路,标签“east”将被定义多次。到目前为止,标签在我们的游戏中是全球独一无二的,没有重复项。 -思考题:你能否想出解决办法? +::: warning 🤔 思考题:你能否想出解决办法? +::: 这些问题同样适用于其他对象,而不仅仅是通道。 @@ -17,11 +18,11 @@ 这立即将我们带到了解析器的第三个问题: -1. 一个标签只能是一个单词;“ sliver coin”这是俩单词,他不接受啊 +1. 一个标签只能是一个单词;“sliver coin”这是俩单词,他不接受啊 所有三个问题都将在本章中解决,从问题#1 开始。它通过为每个对象提供一个标签列表来解决,而不仅仅是一个标签。 -# object.h +## object.h ```c typedef struct object { @@ -47,7 +48,7 @@ extern OBJECT objs[]; #define endOfObjs (objs + 10) ``` -# object.c +## object.c ```c #include @@ -63,7 +64,7 @@ static const char *tags6[] = {"east", "entrance", NULL}; static const char *tags7[] = {"west", "exit", NULL}; static const char *tags8[] = {"west", "north", "south", "forest", NULL}; static const char *tags9[] = {"east", "north", "south", "rock", NULL}; -//我们不固定标签长度,在结束的时候用NULL来标记 +//我们不固定标签长度,在结束的时候用 NULL 来标记 OBJECT objs[] = { {"an open field" , tags0, NULL , NULL }, {"a little cave" , tags1, NULL , NULL }, @@ -75,22 +76,22 @@ OBJECT objs[] = { {"an exit to the west" , tags7, cave , field }, {"dense forest all around" , tags8, field, NULL }, {"solid rock all around" , tags9, cave , NULL } - //我们用NULL来阻绝进入一个你不知道的地方 + //我们用 NULL 来阻绝进入一个你不知道的地方 }; ``` 当然,要让这个改动生效,我们还需要调整noun.c中的objectHasTag函数。 -同时,我们将让函数 getVisiblegetPossession 告知玩家他必须更具体的选择你到底是银币还是金币 ,而不是随机选择任何一个对象。 +同时,我们将让函数 getVisiblegetPossession 告知玩家他必须更具体的选择你到底是银币还是金币,而不是随机选择任何一个对象。 -# noun.h +## noun.h ```c extern OBJECT *getVisible(const char *intention, const char *noun); extern OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun); ``` -# noun.c +## noun.c ```c #include @@ -107,7 +108,7 @@ static bool objectHasTag(OBJECT *obj, const char *noun) for (tag = obj->tags; *tag != NULL; tag++) { if (strcmp(*tag, noun) == 0) return true; - }//扫描对象的tag列表 + }//扫描对象的 tag 列表 } return false; } @@ -187,9 +188,9 @@ OBJECT *getPossession(OBJECT *from, const char *verb, const char *noun) } ``` -问题 #3 可以通过从函数parseAndExecute中删除一个 [空格](http://en.wikipedia.org/wiki/Space_(punctuation))字符来解决(下面的第 10 行)。这个解决方案远非完美('silver' 和 'coin' 之间的双空格是大咩的),但直到我们在第 13 章中让自己成为一个更好的解析器之前。 +问题 #3 可以通过从函数parseAndExecute中删除一个 [空格](http://en.wikipedia.org/wiki/Space_(punctuation))字符来解决(下面的第 10 行)。这个解决方案远非完美('silver' 和 'coin' 之间的双空格是打咩的),但直到我们在第 13 章中让自己成为一个更好的解析器之前。 -# parsexec.c +## parsexec.c ```c #include @@ -249,4 +250,5 @@ bool parseAndExecute(char *input) 现在对象数组 ( object.c ) 开始在多个维度上增长(特别是在引入多个标签的情况下),我们需要一种使其更易于维护的方法。 -猜猜看该怎么办? +::: warning 🤔 猜猜看该怎么办? +::: diff --git a/3.编程思维体系构建/3.4.6.9.练习:生成代码.md b/3.编程思维体系构建/3.4.6.9.练习:生成代码.md index fe7f8c0..37fae34 100644 --- a/3.编程思维体系构建/3.4.6.9.练习:生成代码.md +++ b/3.编程思维体系构建/3.4.6.9.练习:生成代码.md @@ -1,10 +1,10 @@ -# 9.练习:生成代码 +# 9.练习:生成代码 -*到目前为止,我们的冒险游戏有10个对象。每个对象由有5 个属性组成。一个真正的文本冒险可能有数百个甚至数千个对象,并且每个对象的属性数量也可能增加(请参阅下一章)。在目前的形式下,维护如此庞大的对象和属性列表将很困难。* +*到目前为止,我们的冒险游戏有 10 个对象。每个对象由有 5 个属性组成。一个真正的文本冒险可能有数百个甚至数千个对象,并且每个对象的属性数量也可能增加(请参阅下一章)。在目前的形式下,维护如此庞大的对象和属性列表将很困难。* -例如,当我们在添加对象 *wallField* 和 *wallCave* 时,我们必须在三个不同的位置执行此操作:一次在 *object.h* 中(作为*#define*),两次在 *object.c* 中(数组 *objs* 中的一个元素,以及一个单独的标签数组)。这显然十分笨拙并且容易出错。 +例如,当我们在添加对象 *wallField* 和 *wallCave* 时,我们必须在三个不同的位置执行此操作:一次在 *object.h* 中(作为#define),两次在 *object.c* 中(数组 *objs* 中的一个元素,以及一个单独的标签数组)。这显然十分笨拙并且容易出错。 -我们将不再手工维护object. h和object. c,而是从更适合我们需要的单一源开始生成文件。这个新的源文件可以用你喜欢的任何语言( 典型的是某些特定领域的语言 ),只要你有工具把它转换回C。下面是一个简单的例子,考虑下列布局来组织我们的对象: +我们将不再手工维护 object. h 和 object. c,而是从更适合我们需要的单一源开始生成文件。这个新的源文件可以用你喜欢的任何语言 ( 典型的是某些特定领域的语言 ),只要你有工具把它转换回 C。下面是一个简单的例子,考虑下列布局来组织我们的对象: ```txt /* Raw C code (declarations) */ @@ -21,7 +21,7 @@ 根据到目前为止收集的对象,我们可以构造以下源文件。文件名并不重要;我只是简单地将其命名为*object.txt*,以明确它与*object.h*和*object.c*相关。 -# object.txt +## object.txt ```txt #include @@ -87,4 +87,5 @@ extern OBJECT objs[]; location cave ``` -思考题:你能否自己用C来实现这段伪代码? \ No newline at end of file +::: warning 🤔 思考题:你能否自己用 C 来实现这段伪代码? +::: diff --git a/3.编程思维体系构建/3.6.5.1CS61A Sec1.md b/3.编程思维体系构建/3.6.5.1CS61A Sec1.md new file mode 100644 index 0000000..a14bee7 --- /dev/null +++ b/3.编程思维体系构建/3.6.5.1CS61A Sec1.md @@ -0,0 +1,420 @@ +# CS61A Sec1 + +观前须知: + +本章节内容基于 Berkeley 大学的教材 [Composing Programs](http://www.composingprograms.com/) by [John DeNero](http://www.denero.org/),并在此基础上做了部分修改,是在[知识共享协议](https://creativecommons.org/licenses/by-nc-sa/3.0/deed.zh)下所许可的。 + +计算机科学的高生产力之所以可能,是因为该学科是建立在一套优雅而强大的基本思想之上。所有的程序都从信息的表示开始,然后寻找一种逻辑来处理这些信息,并设计抽象概念来解释和控制这种逻辑。有了这些认识后,我们就需要准确的理解计算机是如何解释我们写的程序并进行计算的。 + +> A language isn't something you learn so much as something you join.
—[Arika Okrent](http://arikaokrent.com/) + +为了定义计算过程,我们需要一种编程语言;最好是许多人类和大量的计算机都能理解的语言。所以在 cs61a 中,伯克利主要使用 Python 语言来进行教学。 + +(在之前的 cs61a 课程中,Berkeley 大学主要使用 Scheme 来进行教学,(可能会写一篇文章来说说 Python 和 Scheme 语言和编程上的区别?) 不过在现在的课程中还是有关于 Scheme 的内容,所以并不用太过伤心?🙄) + +## 学习目标 + +在这一部分我们要学习的内容主要是函数(Functions)和控制(Control) + +### 一个简单的例子 + +为了给 Python 一个适当的介绍,我们将从一个使用几种语言特征的例子开始。 + +Python 内置了对广泛的常见编程的支持,如操作文本、显示图形和通过互联网进行通信。 + +```python +from urllib.request import urlopen +``` + +这个 Python 代码是一个导入语句,加载了在互联网上访问数据的功能。实际上,它提供了一个叫做 urlopen 的函数,它可以在一个[统一资源定位符(URL)](https://developer.mozilla.org/zh-CN/docs/Learn/Common_questions/Web_mechanics/What_is_a_URL)上访问内容,可以通过它来访问互联网上的数据。 + +语句和表达式 + +Python 代码由语句和表达式组成。大体上,计算机程序由以下指令组成 + +1. 计算一些值 +2. 进行一些操作 + +语句通常描述行动;当 Python 解释器执行一个语句时,它执行相应的动作。另一方面,表达式通常描述的是计算;当 Python 评估一个表达式时,它计算该表达式的值。在这篇文章下,介绍了几种类型的声明和表达方式。 + +赋值语句 + +```python +shakespeare = urlopen('http://www.composingprograms.com/shakespeare.txt') +``` + +注意:在伯克利大学的教材中,上述代码中的 url 并没有添加"www.",导致现在(至少在写这篇文章的时候)无法打开原文中的 url(可能还会写一篇文章来讲解"www."?) + +将变量名 `shakespeare` 用 `=` 和后面的表达式的值联系起来。该表达式将 `urlopen` 函数应用于一个 URL,该 URL 包含威廉 - 莎士比亚 37 部戏剧的完整文本,全部保存在一个文本文件中。 + +函数 + +函数封装了操作数据的逻辑。 + +这句话告诉我们,可以从两个角度来看函数: + +- 在调用函数的时候,我们关注的是要处理的数据 +- 在定义函数的时候,我们关注的是如何处理数据 + +```python +shakespeare = urlopen('http://www.composingprograms.com/shakespeare.txt') +``` + +`urlopen` 是一个函数。一个网络地址是一种数据,而莎士比亚戏剧的文本是另一种数据。从网络地址到文本的过程可能很复杂,但我们可以只用一个简单的表达式来应用这个过程,因为这个复杂性被藏在一个函数中。 + +你可能不了解 `urlopen` 这个函数背后的逻辑,但这不影响你去调用这个函数,这就是函数封装的好处之一。 + +因此,函数是本章节关注的重点。 + +我们来看另一个赋值语句: + +```python +words = set(shakespeare.read().decode().split()) +``` + +这个语句将名字词与莎士比亚戏剧中出现的所有出现过的词(重复出现的词只统计一次)的集合联系起来,其中有 33,721(?) 个词。上述语句包含一个读取、解码和分割的命令链,每个命令都在一个中间计算实体上操作:我们从打开的 URL 中读取数据,然后将数据解码成文本,最后将文本分割成单词。所有这些词都被放在一个集合(Set,Python 中的一种数据类型)中。 + +对象 + +前文中提到的 Set,不仅仅是数据类型,也是一个对象。对象用一种能同时处理两者复杂性的方式,把数据和操作该数据的逻辑无缝衔接在一起。 + +对象会是我们后面章节所要讨论的内容。 + +现在让我们来看这个例子中的最后一个语句: + +```python +>>> {w for w in words if len(w) == 6 and w[::-1] in words} +{'redder', 'drawer', 'reward', 'diaper', 'repaid'} +``` + +第一行的">>>"表示输入,第二行则是交互式会话的输出 + +这是一个复合表达式,其值是所有长度为 6 的、本身和反向拼写都在原集合中的词组成的集合。其中的 `w[::-1]` 是一种隐式表达,它枚举了 `w` 中的所有字母,但因为 `step = -1` 规定了步长是反方向的。 + +解释器 + +计算复合表达式需要一个精确的程序,以可预测的方式解释代码。一个能实现程序和计算符合表达式的程序被称为解释器;没错,其实解释器是程序(可能再写一篇文章来讲讲解释器和编译器的区别?) + +与其他计算机程序相比,编程语言的解释器在通用性方面是独一无二的。Python 的设计没有考虑到莎士比亚,然而,它的不可思议的灵活性使我们能够只用几个语句和表达式来处理大量的文本。 + +最后,我们会发现所有这些核心概念都是密切相关的:函数是对象,对象是函数,而解释器是两者的实例。然而,要掌握编程的艺术,关键是要清楚地理解每个概念及其在组织代码中的作用。 + +解释器的设计和实现也是我们之后的主要议题。 + +### 编程原本 + +编程语言不仅仅是指示计算机执行任务的一种手段,同时也是一个框架,我们在这个框架内组织我们关于计算过程的想法。程序的作用是在编程社区的成员之间交流这些想法,所以,编写的程序必须让人们容易阅读,而且只是顺便让机器执行。 + +当我们描述一种语言时,我们应该特别注意该语言为结合简单的想法以形成更复杂的想法所提供的手段。 + +每种强大的语言都有三种这样的机制: + +- 原始的表达式和语句,代表了该语言提供的最简单的构建模块。 +- 组合的方式,由较简单的元素建立成复合元素。 +- 抽象的手段,通过它,复合元素可以作为单位被命名和操作。 + +在编程中,我们处理两种元素:函数和数据。(很快就会发现,它们其实并不那么明显)。不那么正式地说,数据是我们想要操作的东西,而函数描述了操作数据的规则。因此,任何强大的编程语言都应该能够描述原始数据和原始函数,以及有一些方法来组合和抽象函数和数据。 + +在上一小节中对 Python 解释器进行了实验后,我们现在重新开始,有条不紊地逐个开发 Python 语言元素。如果例子看起来很简单,那就耐心一点,因为更多令人兴奋的点很快就会出现。 + +我们从原始表达式开始。一种原始表达式是数字。更确切地说,你输入的表达式由代表十进制的数字组成。 + +```python +>>> 42 +42 +``` + +代表数字的表达式可以与数学运算符相结合,形成一个复合表达式,解释器将对其进行计算。 + +```python +>>> -1 - -1 +0 +>>> 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 +0.9921875 +``` + +这些数学表达式使用中缀符号,其中运算符(例如,+,-,*,或/)出现在操作数(数字)之间。Python 包括许多形成复合表达式的方法。我们不会试图立即列举它们,而是会随着我们的学习引入新的表达形式,以及它们所支持的语言特性。 + +最重要的一种复合表达式是调用表达式,它将一个函数应用于一些参数。回顾一下代数,函数的数学概念是一个从一些自变量到因变量的映射。例如,一个求最大值的函数将其的多个输入映射到当中最大值的一个单一的输出。Python 表达函数应用的方式与传统数学中相同。 + +```python +>>> max(7.5, 9.5) +9.5 +``` + +这个调用表达式有子表达式:操作符是括号前的表达式,它包含了一个用逗号分隔的操作数列表。 + +![](static/call_expression.png) + +运算符指定了一个函数。当这个调用表达式被评估时,我们说对参数`7.5` 和 `9.5`调用函数 `max`,并返回一个 9.5 的返回值。 + +调用表达式中参数的顺序很重要。例如,函数 `pow` 计算第一个参数的第二个参数次方。 + +```python +>>> pow(100, 2) +10000 +>>> pow(2, 100) +1267650600228229401496703205376 +``` + +与中缀表示法的数学约定相比,函数表示法有三个主要优点。首先,函数可以接受任意数量的参数: + +```python +>>> max(1, -2, 3, -4) +3 +``` + +不会产生歧义,因为函数名总是优先于其参数。 + +此外,函数符号以一种直接的方式延伸到嵌套表达式,其中的元素本身就是复合表达式。在嵌套的调用表达式中,与复合的中缀表达式不同,嵌套的结构在括号中是完全明确的。 + +```python +>>> max(min(1, -2), min(pow(3, 5), -4)) +-2 +``` + +对于这种嵌套的深度以及 Python 解释器可以计算的表达式的整体复杂性,(原则上)没有限制。然而,人类很快就会被多级嵌套所迷惑。作为一个程序员,你的一个重要作用是构造表达式,使它们仍然可以由你自己、你的编程伙伴和其他将来可能阅读你的表达式的人来解释。 + +同时,数学符号有各种各样的形式:乘法出现在术语之间,指数显示为上标,除法显示为斜杠,平方根显示为有斜边的屋顶。其中一些符号是很难打出来的!然而,所有这些复杂性都可以通过调用表达式的符号来统一。虽然 Python 支持使用中缀表达式的常见数学运算符(如 `+` 和 `-`),但任何运算符都可以表示为一个有名称的函数。 + +### 导入库函数 + +Python 定义了大量的函数,包括上一节中提到的运算符函数,但默认不提供它们的所有名称。作为替代,它将函数和其他量组织到模块中,这些模块共同构成了 Python 库。为了使用这些元素,人们将它们导入。例如,数学模块提供了各种熟悉的数学相关的函数。 + +```python +>>> from math import sqrt +>>> sqrt(256) +16.0 +``` + +而运算符模块提供了对应于中缀表达式的函数的访问: + +```python +>>> from operator import add, sub, mul +>>> add(14, 28) +42 +>>> sub(100, mul(7, add(8, 4))) +16 +``` + +一个导入语句指定了一个模块的名称(例如,`operator` 或 `math`),然后列出要导入的该模块的命名属性(例如,`sqrt`)。一旦一个函数被导入,它可以被多次调用。 + +使用这些运算符函数(如 `add`)和运算符符号本身(如 `+`)之间没有区别。传统上,大多数程序员使用符号和中缀表达式来表达简单的算术。 + +[Python 3 库文档](https://docs.python.org/3/library/index.html)列出了每个模块所定义的功能,如[数学模块](https://docs.python.org/3/library/math.html)。然而,这个文档是为那些对整个语言很了解的开发者编写的。现在,你可能会发现,对一个函数进行实验比阅读文档能告诉你更多关于它的行为。随着你对 Python 语言和词汇的熟悉,这个文档将成为有价值的参考来源。 + +### 变量名和环境 + +编程语言的一个关键方面是它提供了使用变量名来指代计算对象的手段。如果一个值被赋予了一个变量名,我们就说这个变量名与这个值绑定了。 + +在 Python 中,我们可以使用赋值语句建立新的绑定,其中包含左边的变量名 `=` 右边的值。 + +```python +>>> radius = 10 +>>> radius +10 +>>> 2 * radius +20 +``` + +变量名也是可以通过导入语句来绑定的。 + +```python +>>> from math import pi +>>> pi * 71 / 223 +1.0002380197528042 +``` + +`=` 符号在 Python(以及许多其他语言)中被称为赋值操作符。赋值是我们最简单的抽象手段,因为它允许我们使用简单的名称来指代复合操作的结果。用这种方式,复杂的程序就是通过一步一步地建立复杂度越来越高的计算对象来构建的。 + +将变量名与值绑定,然后通过变量名检索这些值意味着解释器必须保持某种内存,以跟踪变量名、值和绑定。这样的内存空间被称为环境。 + +变量名也可以被绑定到函数上。例如,变量名 `max` 与我们使用的求最大值的函数绑定。与数字不同的是,函数在呈现为文本时很棘手,所以当被要求描述一个函数时,Python 会打印一个识别描述。 + +```python +>>> max + +``` + +我们可以使用赋值语句给现有的函数起别名。 + +函数也可以看作是值。 + +```python +>>> f = max +>>> f + +>>> f(2, 3, 4) +4 +``` + +在同一个环境下的连续的赋值语句可以将一个名字重新绑定到一个新的值。 + +```python +>>> f = 2 +>>> f +2 +``` + +在 Python 中,名称通常被称为变量名变量,因为它们在执行程序的过程中可能被绑定到不同的值。当一个名称通过赋值被绑定到一个新的值时,它就不再被绑定到任何以前的值。人们甚至可以将内置名称与新值绑定。 + +```python +>>> max = 5 +>>> max +5 +``` + +在将 `max` 赋值为 5 后,`max` 这个名称不再与函数绑定,因此试图调用 `max(2, 3, 4)` 会造成错误。 + +在执行赋值语句时,Python 在改变对左边变量名的绑定之前,对 `=` 右边的表达式进行计算。因此,人们可以在右侧表达式中引用一个变量名,即使它是要被赋值语句绑定的变量名。 + +```python +>>> x = 2 +>>> x = x + 1 +>>> x +3 +``` + +我们还可以在一个语句中给多个变量名赋值,其中左边的变量名和右边的表达式分别用逗号隔开。 + +```python +>>> area, circumference = pi * radius * radius, 2 * pi * radius +>>> area +314.1592653589793 +>>> circumference +62.83185307179586 +``` + +改变一个变量的值并不影响其他变量。下面,尽管变量名 `area` 被绑定到一个最初以 `radius` 定义的值,但 `area` 的值并没有改变。更新 `area` 的值需要另一个赋值语句。 + +```python +>>> radius = 11 +>>> area +314.1592653589793 +>>> area = pi * radius * radius +380.132711084365 +``` + +通过多重赋值的语句,在左边的任何变量名被绑定到这些值之前,右边的所有表达式都将被计算。由于这个规则,交换绑定在两个变量名上的值可以在一个语句中进行。 + +```python +>>> x, y = 3, 4.5 +>>> y, x = x, y +>>> x +4.5 +>>> y +3 +``` + +### 计算嵌套表达式 + +我们在本小节的目标之一是分离出关于像程序一样思考的问题。从下面这个例子中,我们应该意识到,在计算嵌套调用表达式时,解释器本身是在遵循某种步骤。 + +为了计算一个调用表达式,Python 将做按以下规则来工作: + +1. 计算运算符和操作数的子表达式,然后 +2. 将作为运算符子表达式的值的函数应用于作为运算符子表达式的值的参数。 + +即使这是个简单的程序也说明了关于一般过程的一些重要观点。第一步决定了为了完成一个调用表达式的计算过程,我们必须首先计算其他表达式。因此,计算过程在本质上是递归的;也就是说,作为其步骤之一,它也包括调用规则本身。 + +例如,计算 + +```python +>>> sub(pow(2, add(1, 10)), pow(2, 5)) +2016 +``` + +需要这个按照上述过程重复四次。如果我们画出每个被计算的表达式,我们就可以直观地看到这个过程的层次结构。 + +![](static/expression_tree.png) + +这张插图被称为表达式树。在计算机科学中,树(Tree,一种数据结构,我们将在后续的章节中进行讨论)通常是自上而下生长的。树中每一点的对象被称为节点;在这张插图的情况下,节点是与值配对的表达式。 + +计算它的根,即顶部的完整表达式,需要首先计算作为其子表达式的分支。叶表达式(即没有分支的节点)代表函数或数字。内部节点有两个部分:我们的计算规则所适用的调用表达式,以及该表达式的结果。从这棵树的计算来看,我们可以想象操作数的值是向上渗滤的,从末端节点开始,然后在越来越高的层级上进行组合。 + +接下来,观察一下,步骤一的重复应用使我们需要计算的不是调用表达式,而是数字(如 `2`)和名称(如 `add`)等原始表达式。 + +我们通过规定以下几点来处理这种情况: + +- 数字计算为它的名称所代表的数量 +- 名称计算为与当前环境中的名称相关的值。 + +请注意环境在决定表达式中符号的含义方面的重要作用。在 Python 中,在没有给定环境或是明确所有名称所指代的内容时,谈论一个表达式的价值是没有意义的,比如 + +```python +>>> add(x, 1) +``` + +而不指定任何关于环境为名称 `x`(甚至是名称 `add`)提供意义的信息。环境提供了计算发生的背景,这对我们理解程序执行起着重要作用。 + +上述的计算过程不足以计算所有的 Python 代码,只计算调用表达式、数字和名称。 + +例如,它不处理赋值语句 + +```python +>>> x = 3 +``` + +这个语句不返回一个值,也不在某些参数上调用一个函数,因为赋值的目的是将一个变量名绑定到一个值上。 + +一般来说,赋值语句不是被计算而是被执行;它们不产生一个值,而是做一些改变。每种类型的表达式或语句都有自己的计算或执行过程。 + +### 纯函数和非纯函数 + +在本小节中,我们将区分两种函数 +**纯函数** +函数有一些输入(它们的参数)并返回一些输出(应用它们的结果)。 +例如内置函数 + +```python +>>> abs(-2) +2 +``` + +可以被描述为一台接受输入并产生输出的小型机器。 +![](static/function_abs.png) +函数 `abs` 是*纯函数*。纯函数的特性是,调用它们除了返回一个值之外没有任何影响。此外,当用相同的参数调用两次时,一个纯函数必须总是返回相同的值。 + +**非纯函数** +除了返回一个值之外,应用一个非纯函数会产生副作用,从而使解释器或计算机的状态发生一些变化。一个常见的副作用是,使用`print`函数,在返回值之外产生额外的输出。 + +```python +>>> print(1, 2, 3) +1 2 3 +``` + +虽然`print`和`abs`在这些例子中可能看起来很相似,但它们的工作方式根本不同。打印返回的值总是`None`,这是一个特殊的 Python 值,不代表任何东西。交互式 Python 解释器不会自动打印值`None`。在`print`的情况下,函数本身是打印输出,也是被调用的副作用。 +![](static/function_print.png) + +对`print`函数的嵌套调用突出了纯函数和非纯函数的区别 + +```python +>>> print(print(1), print(2)) +1 +2 +None None +``` + +如果你发现这个输出出乎意料,可以画一个表达式树来弄清楚为什么计算这个表达式会产生这个奇特的输出。 + +请注意!`print`函数的返回值`None`意味着它*不应该*是赋值语句中的表达式。 + +```python +>>> two = print(2) +2 +>>> print(two) +None +``` + +纯函数是被限制的,因为它们不能有副作用或随时间改变行为。施加这些限制会产生巨大的好处。 +首先,纯函数可以更可靠地组成复合调用表达式。我们可以在上面的非纯函数例子中看到,`print`在操作数表达式中使用时并没有返回一个我们期望的结果。另一方面,我们已经看到,像`max`、`pow`和`sqrt`这样的函数可以有效地用于嵌套表达式。 +其次,纯函数往往更容易测试。一个参数列表将总是导致相同的返回值,这可以与预期返回值进行比较。关于测试将在之后的章节详细讨论。 + +在之后的章节中,我们将说明纯函数对于编写并发程序的重要性,其中多个调用表达式可以同时被计算。 +与之对应的,我们也将研究非纯函数并了解他们的用途。 + +出于这些问题的考虑,我们将在下一章节中着重讨论创建和使用纯函数。`print`函数的使用只是为了让我们看到计算的中间结果。 + +## 课后作业 + +一个好的课程怎么能少得了精心准备的课后作业呢?🤗 diff --git a/3.编程思维体系构建/static/call_expression.png b/3.编程思维体系构建/static/call_expression.png new file mode 100644 index 0000000..cfdd487 Binary files /dev/null and b/3.编程思维体系构建/static/call_expression.png differ diff --git a/3.编程思维体系构建/static/expression_tree.png b/3.编程思维体系构建/static/expression_tree.png new file mode 100644 index 0000000..0da9aca Binary files /dev/null and b/3.编程思维体系构建/static/expression_tree.png differ diff --git a/3.编程思维体系构建/static/function_abs.png b/3.编程思维体系构建/static/function_abs.png new file mode 100644 index 0000000..ab459aa Binary files /dev/null and b/3.编程思维体系构建/static/function_abs.png differ diff --git a/3.编程思维体系构建/static/function_print.png b/3.编程思维体系构建/static/function_print.png new file mode 100644 index 0000000..b75e447 Binary files /dev/null and b/3.编程思维体系构建/static/function_print.png differ