在Java学校的危险中, Joel讨论了他在Penn的经历以及“分段错误”的难度。他说
[在您之前,段错误是很难的]“深呼吸,真正尝试迫使您的思想同时在两个不同的抽象级别上工作。”
给出了导致段错误的常见原因的清单,我不明白我们必须如何在2个抽象级别上工作。
由于某种原因,Joel认为这些概念是程序员抽象能力的核心。我不想承担太多。那么,指针/递归到底有什么困难呢?例子会很好。
在Java学校的危险中, Joel讨论了他在Penn的经历以及“分段错误”的难度。他说
[在您之前,段错误是很难的]“深呼吸,真正尝试迫使您的思想同时在两个不同的抽象级别上工作。”
给出了导致段错误的常见原因的清单,我不明白我们必须如何在2个抽象级别上工作。
由于某种原因,Joel认为这些概念是程序员抽象能力的核心。我不想承担太多。那么,指针/递归到底有什么困难呢?例子会很好。
Answers:
我首先注意到指针和递归在大学里很困难。我参加了一些典型的第一年课程(一个是C和Assembler,另一个是Scheme)。这两门课程都是从数百名学生开始的,其中许多人具有多年的高中级编程经验(当时通常是BASIC和Pascal)。但是,一旦在C课程中引入了指针,并且在Scheme课程中引入了递归,则大量学生-甚至可能是大多数学生都被完全弄糊涂了。这些孩子以前曾经写过很多代码,一点也没有问题,但是当他们碰到指针和递归时,他们的认知能力也会碰壁。
我的假设是,指针和递归是相同的,因为它们要求您同时保留两个抽象级别。关于多层次的抽象需要某种精神才能,这是某些人永远不可能拥有的。
我也非常愿意接受有可能向任何人教授指针和/或递归...我没有任何证据可以证明。我确实知道,凭经验,能够真正理解这两个概念是一般编程能力的非常非常好的预测指标,并且在本科CS训练的正常过程中,这两个概念是最大的障碍。
递归不仅是“调用自身的函数”。您不会真正理解为什么递归比较困难,直到您发现自己画出堆栈帧来找出递归下降解析器出了什么问题。通常,您将拥有相互递归的函数(函数A调用函数B,后者调用函数C,后者可能调用函数A)。当您在一个相互递归的函数系列中有N个堆栈帧很深时,很难弄清楚出了什么问题。
同样,对于指针,指针的概念非常简单:存储内存地址的变量。但是同样,当void**
指向不同节点的指针的复杂数据结构出了问题时,您会发现为什么当您难以弄清为什么其中一个指针指向垃圾地址时,它变得棘手。
goto
。
goto
。
int a() { return b(); }
可以递归,但这取决于的定义b
。所以它并不像看起来那么简单...
Java支持指针(它们称为引用),并且支持递归。因此从表面上看,他的论点显得毫无意义。
他真正在说的是调试能力。确保Java指针(err,引用)指向有效对象。AC指针不是。假设您不使用valgrind之类的工具,那么C编程中的诀窍是找出指针的确切位置(很少在stacktrace中找到该点)。
指针和递归的问题并不在于它们不一定很难理解,而是对它们的教学不好,尤其是对于诸如C或C ++之类的语言(主要是因为对语言本身的理解很差)。每当我听到(或读到)某人说“数组只是一个指针”时,我就会在里面死掉一点。
同样,每次有人使用斐波那契函数来说明我想尖叫的递归时。这是一个不好的例子,因为迭代版本不难编写,并且其性能至少与递归版本相同或更好,并且它没有使您真正了解为何递归解决方案有用或令人期望。快速排序,树的遍历,等等,都远远的原因和递归如何更好的例子。
必须处理指针是使用暴露于指针的编程语言的人工产物。几代Fortran程序员在不需要专用指针类型(或动态内存分配)的情况下构建列表,树,堆栈和队列,而且我从未听说过有人指责Fortran是玩具语言。
GOTO target
) 。我认为我们必须构建自己的运行时堆栈。这已经很久了,我再也记不清细节了。
指针有几个困难:
这就是为什么程序员在使用指针时必须更深入地思考(我不知道抽象的两个层次)。这是一个新手犯下的典型错误的示例:
Pair* make_pair(int a, int b)
{
Pair p;
p.a = a;
p.b = b;
return &p;
}
请注意,上述代码在没有指针概念而是名称(引用),对象和值之一的语言中是完全合理的,就像函数编程语言和带有垃圾回收的语言(Java,Python)一样。
递归函数的困难发生在没有足够数学背景(递归性是常识和必需知识)的人试图接近他们时,认为该函数的行为将取决于之前被调用的次数而有所不同。由于可以确实以某种方式创建递归函数,而您必须以这种方式来理解它们,因此该问题更加严重。
考虑一下传递指针的递归函数,例如在Red-Black Tree的过程实现中,就地修改了数据结构;与功能对应者相比,它很难考虑。
问题中没有提到它,但是新手遇到的另一个重要问题是并发性。
正如其他人提到的那样,某些编程语言构造还有一个附加的,非概念上的问题:即使我们了解这些构造的简单而诚实的错误,也很难调试。
malloc()
这样做的可能性不比任何其他函数大。)
指针和递归是两个独立的野兽,并且有不同的原因使它们各自“难于”。
通常,指针需要的心理模型与纯变量分配不同。当我有一个指针变量时,就是这样:一个指向另一个对象的指针,它包含的唯一数据是它指向的内存地址。因此,例如,如果我有一个int32指针并直接为其分配一个值,则我没有更改int的值,而是指向一个新的内存地址(您可以使用许多巧妙的技巧来完成此操作)。更有趣的是有一个指向指针的指针(当您在C#中将Ref变量作为函数Parameter传递时,会发生这种情况,该函数可以为Parameter分配一个完全不同的对象,并且当函数使用该值时,该值仍在范围内退出。
初次学习时,递归在思想上会略有飞跃,因为您是根据自身定义函数的。当您第一次遇到它时,这是一个疯狂的概念,但是一旦您掌握了这个想法,它就会成为第二天性。
但是回到眼前的话题。Joel的论点与其本身无关的是指针或递归,而是这样的事实,即学生将被进一步从计算机的实际工作方式中移除。这是计算机科学中的科学。学习编程和学习程序的工作方式有明显的区别。我认为“我以这种方式学习它,所以每个人都必须以这种方式学习”并不是什么大问题,因为他认为许多CS计划正成为光荣的贸易学校。
我给P. Brian一个+1,因为我感觉像他一样:递归是一个基本概念,他对此有丝毫困难,应该更好地考虑在Mac Donalds找工作,但是,即使有递归:
make a burger:
put a cold burger on the grill
wait
flip
wait
hand the fried burger over to the service personel
unless its end of shift: make a burger
当然,缺乏理解也与我们的学校有关。这里应该介绍Peano,Dedekind和Frege这样的自然数,这样以后我们就不会有太多困难了。
goto top
由于某种原因,我们似乎想要IME付出更多。
我不同意乔尔(Joel)的观点,问题是在多个抽象层次上进行思考,我认为,更多的是指针和递归是问题的两个很好的例子,这些问题需要人们改变关于程序工作方式的思维模型。
我认为,指针是更简单的例子。处理指针需要程序执行的心理模型,该模型说明程序实际使用内存地址和数据的方式。我的经验是,很多时候程序员在学习指针之前甚至都没有想到这一点。即使他们从抽象的角度了解它,也没有将其纳入程序运作方式的认知模型中。引入指针时,它要求他们对代码的工作方式进行根本性的转变。
递归是有问题的,因为有两个概念上的块需要理解。首先是在机器级别,并且与指针很像,可以通过对程序实际存储和执行方式的深入了解来克服它。我认为,递归的另一个问题是人们很自然地试图将递归问题解构为非递归问题,这使人们对递归函数作为格式塔的理解变得混乱。这可能是由于数学背景不足的人遇到的问题,或是不是将数学理论与程序开发联系在一起的思维模型。
问题是,我不认为指针和递归对于陷入思维模式不足的人们来说是唯一有问题的两个领域。并行性似乎是另一个被人们卡住并且难以适应其心理模型的领域,这只是指针和递归经常在面试中容易测试的时候。
DATA | CODE
|
pointer | recursion SELF REFERENTIAL
----------+---------------------------------
objects | macro SELF MODIFYING
|
|
自引用数据和代码的概念分别构成了指针和递归的定义。不幸的是,对命令式编程语言的广泛了解使计算机科学专业的学生认为,当他们应该将此谜团相信于语言的功能方面时,他们必须通过运行时的操作行为来理解实现。将所有数字相加到一百似乎是一个简单的问题,即从一个数字开始并将其添加到序列中的下一个数字,然后借助循环自参考函数向后进行计算,这对于许多不习惯于安全性的人来说似乎是错误的甚至是危险的纯函数。
自我修改数据和代码的概念分别是对象(即智能数据)和宏的定义的基础。我提到这些是因为它们甚至更难以理解,尤其是当希望从所有四个概念的组合中获得对运行时的操作理解时-例如,宏生成一组对象,该对象集借助指针树实现递归体面的解析器。命令式程序员不仅要一次一步地遍历每个抽象层来跟踪程序状态的整个操作,还需要学会相信自己的变量在纯函数内仅被分配一次,并且使用相同的纯函数重复调用该函数。即使在同样支持不纯函数的语言(例如Java)中,相同的参数也始终会产生相同的结果(即引用透明性)。在运行时之后绕圈跑是徒劳的。抽象应该简化。
与Anon的答案非常相似。
除了新手的认知困难之外,指针和递归都非常强大,并且可以以隐秘的方式使用。
强大功能的缺点在于,它们给您强大的功能,可以以微妙的方式修改程序。
将伪造的值存储到普通变量中已经足够糟糕,但是将伪造的值存储在指针中可能会导致各种延迟的灾难性事件发生。
更糟糕的是,当您尝试诊断/调试异常程序行为的原因时,这些影响可能会改变。
与递归类似。通过将棘手问题填充到隐藏的数据结构(堆栈)中,这可能是组织棘手事务的一种非常有效的方法。
但是,如果某件事做得很微妙,那可能很难弄清楚发生了什么。