如何编写代码来解决问题,初学者指南 第二部分:语法错误





5.00/5 (5投票s)
读完本文,你将再也不会遇到编译程序的问题了。好吧,也许不会。嗯,可能还会有一点点。这取决于编译器。
引言
这是(计划中的)一系列文章中的一篇,旨在涵盖“入门开发”的至少基本知识,并为你提供工具——包括思维模式——以便在你上手的同时解决自己的问题。
目前,计划有五篇文章:
- 如何编写代码来解决问题,初学者指南[^]:本文介绍了如何从需求(可能是家庭作业)到可以编写代码的东西。
- 如何编写代码来解决问题,初学者指南 第二部分:语法错误[^]:你正在阅读的就是它!
- 第三部分:让它工作。调试运行中的代码。
- 第四部分:寻求帮助。当你完全卡住的时候!
- 第五部分:测试,当你意识到它实际上并没有想象中那么好用的时候……
简而言之
这是你将定期要做的事情:我们都会犯错误,我们都会遇到语法错误——而且通常很快就能修复它们,随着你越来越熟悉,修复它们的速度也会提高!这绝对是一项值得学习的技能——如果别的没有,它能让你不必花半小时等别人帮你修复!:laugh: 所以试试看——看看你能走多远——如果你自己修不好,就给我们提供实际的信息。
- 错误消息。复制并粘贴它!
- 它发生在哪里。应该与消息一起提供,但如果没有,我们仍然需要知道。
- 代码行加上几行上面的和下面的以供参考。
没有这些……我们也无法修复!
概述
代码已经写好,应用程序也准备好了——但编译器就是不让你运行它。为什么呢?让我们来找出原因。
这里的代码很可能不是你选择的语言——但它们都是相当简单的东西,而且任何语言的处理过程都是一样的。如果你不理解括号和分号,就“跳过”它们就好了!
让我们暂时退一步,从更宏观的角度来看——这应该有助于你理解正在发生的事情。
当你编写应用程序代码时,你需要三种软件:
- 文本编辑器,用于实际将代码输入文件
- 编译器,用于检查你输入的代码,并生成机器可以理解的东西
- 应用程序本身,以机器可以理解的形式
通常,前两种软件会“捆绑在一起”(以及其他非常有用的软件,我希望在下一部分中讲到),在一个“IDE”中——它代表集成开发环境——这样你就不用费事去处理编译器开关、makefile,或者为了做任何事情而离开这个环境——因此,“IDE”中的“集成”部分。
编译型与解释型
但即使没有,应用程序语言也有两种类型:编译型和解释型。第一种是将你的源代码通过一个名为编译器的程序进行处理,该程序会检查指令,然后发出错误消息或创建一个可以运行的可执行文件。解释型代码则跳过这个步骤,在应用程序运行时逐行检查指令,而不是提前检查。它仍然会发出错误消息,但你的代码会一直运行直到遇到一个错误。这些错误被称为“语法错误”,因为几乎毫无例外,它们发生的原因是你输入的代码系统无法理解:就像写一个单词顺序错误的句子。
Cup may I? a have please coffee of
这没有任何意义,即使所有的单词都能理解——它不符合“标准英语语法”。重新排列单词,你就能得到一杯好喝的饮料。
May I have a cup of coffee please?
所以,一个编译型程序只有在没有语法错误的情况下才能运行,而一个解释型程序则会运行。
编译型语言包括:
- C#
- C++
- C
- Visual Basic
- Java
- Objective C
解释型语言包括:
- JavaScript
- Python
- PHP
那么我想要一种解释型语言?
嗯……不是。
我看得出来,能够不经过麻烦的先正确输入代码就能运行你的代码似乎是个好主意,但这实际上是一个巨大的缺点。
我给出的第一个原因是简单的:如果你在解释型语言中拼写错误了一个变量,那么直到你尝试使用它时才会被发现(在某些语言中,甚至根本不会被发现)。这是一个问题,因为在你输入信息到应用程序的这段时间里,可能直到那一行代码被执行时才会发现错误,然后你的应用程序就会以错误消息终止。编译器会在你开始运行任何代码之前就发现它,所以你知道只有你的设计(或数据)中的错误才会导致你的应用程序行为异常。所以,在使用解释型语言时,只有当用户导致那一行代码被执行时,你才会收到一个错误——而且如果你没有测试代码中的每一个路径,这可能意味着它在你的用户那里崩溃,并浪费了数小时的工作。总的来说,这不会鼓励你的用户继续使用……LOL。
你宁愿做什么:输入一行代码立即收到错误,还是输入一行代码几个小时甚至几个月后才发现?
还有很多其他原因,但解释起来会很复杂,因为你需要理解很多初学者还没有遇到的概念——所以我不会详细说明,以免弄混。
总的来说,我推荐使用编译型语言,特别是对于初学者——所以我将重点关注它。解释器也可以生成相同的错误,但会在开发过程的后期。
那么什么是语法错误?为什么它们很重要?
如果我写一个数学表达式像这样:
2 * 3 + 4
你可以理解它——但有两种评估方式:一种得到结果10,另一种得到14。总的来说,大多数成年人“知道”乘法比加法“更重要”,所以会先进行乘法运算。
2 * 3 + 4
6 + 4
10
无论我们怎么写,我们得到的结果都一样。
4 + 2 * 3
4 + 6
10
但孩子可能会这样做:
4 + 2 * 3
6 * 3
18
为了消除误解,我们使用括号。
(2 * 3) + 4
6 + 4
10
4 + (2 * 3)
4 + 6
10
但还有另一种写法:逆波兰表示法(RPN)(这在上一世纪受到HP计算器用户的欢迎,但几乎没有其他人使用)。在这种表示法中,求值完全是基于堆栈的,不需要运算符优先级,因为运算符从堆栈中弹出两个值,对它们进行求值,然后将结果推回堆栈。
4, 2, 3 * +
总是等于10。
要得到18,我们必须这样写:
2, 4 + 3 *
你如何写一个表达式的语法取决于你使用哪种表示法,并且除非你理解所使用的表示法,否则它看起来就像是垃圾——如果你问普通人评估一个RPN表达式,你只会得到一个困惑的表情。
那个表情就是**语法错误消息**,它在说“我无法处理那个,我不理解你想做什么”。
暂时忽略电脑,看看我妻子留给我的购物清单。不,认真地说——我们在手机上有一个电子购物清单(OurGroceries——免费,适用于Android、iPhone和网页,它允许全家人添加到同一个列表中,并实时更新:如果我在超市购物,我们用完了牛奶,她可以添加,我在离开商店前就能收到)。
但是……她有点守旧,不会拼写,打字也很糟糕,所以我收到这样的自动纠正的购物项目:
Helmand ceased smoked garlic
现在,我习惯了她的思维方式和奇怪的项目,所以我可以根据她当时在做什么来推断出她缺了什么。
Hellmann's Caesar with Smoked Garlic Salad Dressing
但是……对你来说,“Helmand ceased smoked garlic
”(赫尔曼德停止熏大蒜)可能毫无意义。我拥有你不知道的信息。
而对于电脑来说?那就更糟糕了,因为它试图将她写的内容翻译成一种完全不同的语言并使其有意义。
当你试图弄清楚这句话的意思时,想象一下编译器:
Al fine spelling and grandmother forget
我最初写的是:
Find all spelling and grammar mistakes
但自动纠正介入了,我最终得到了有效的英语单词:
Fined Al spelling and grandma Miss steaks
如果我逐字翻译,它就会变成法语:
Amende Al orthographe et grand-mère oublier
再翻译回英语就是:
Al fine spelling and grandma forget
我相信你能理解编译器惊恐地举起双手说“够了!我不懂你”的样子。
编译器已经尽力了——而且像C#这样的现代语言现在都有相当好的错误消息。老旧的东西通常很晦涩:C、C++、SQL……
查看语法错误
如今,大多数系统都非常努力地提供可理解的错误消息,在很大程度上它们也做到了——除了SQL,其错误消息几乎总是:
Msg 102, Level 15, State 1, Line n
Incorrect syntax near: 'seemingly random word'.
但说实话,即使这样也在努力帮助你!
第一件要注意的事情是,语法错误几乎总是会产生大量错误消息,而不是只有一个——因为编译器试图弄清楚你哪里错了,并假装你做对了!但这确实非常困难,而且它们通常是错误的——这会产生更多的错误。修复一个语法错误可以消除很多其他错误,所以总是从第一个开始,而不是最后一个!
即使是SQL消息,也有信息:
Msg 102, Level 15, State 1, Line 1
Incorrect syntax near: 'WHERE'.
它告诉你错误在哪一行文本上:“第1行
”。
它告诉你错误大概在哪一行:“靠近 'WHERE'
”。
它还告诉你,它在该行的那个部分不期望“WHERE
”。
所以看看第1行:
INSERT INTO MyTable WHERE ID=0 VALUES (666, 777)
SQL INSERT
的正式语法是:
INSERT INTO <table name> {(<columns list>)} VALUES (<values list>}
没有提到 WHERE
子句(因为 INSERT
操作的是新数据,而不是修改现有数据),所以问题是它根本不期望“WHERE
”这个词。
如果你让它发生……一个编译器可以从非常少的代码中生成很多错误。
C++ 示例
这段代码实际上来自一个最近的问题:这是什么错误?我无法运行程序[^],也是促使我花时间开始撰写这篇文章的部分原因。它是C++代码,并且是导致大量语法错误的一个典型简单错误。
这是完整的错误消息:
main.cpp:56:9: error: no match for ‘operator<<’
(operand types are ‘std::istream’ {aka ‘std::basic_istream’} and ‘int’)
56 | cin<<pgibg;
| ~~~^~~~~~~
| | |
| | int
| std::istream {aka std::basic_istream<char>}
这里有很多信息:
main.cpp:56:9:
这是文件名,以及检测到错误的行号和列号。所以去那里:大多数编辑器都支持 CTRL + G 直接跳转到行号。就是这一行:
cin<<pgibg;
error: no match for ‘operator<<’
(operand types are ‘std::istream’ {aka ‘std::basic_istream’} and ‘int’)
这是它检测到的内容的描述。在这种情况下,它看起来帮助不大,但它告诉你一件事:对于输入流和整数,没有定义“operator<<
”。
56 | cin<<pgibg;
| ~~~^~~~~~~
| | |
| | int
| std::istream {aka std::basic_istream<char>}
其余的只是显示了那一行,并指出了它不理解的部分。现在看看那一行以及前后几行作为上下文:
cout<<"PhilHealth Monthly Contribution: "<<((rate*day)+(ot*otrate)*.035);
cin>>phil;
cout<<"\nPAGIBIG Monthly Contribution: "<<((rate*day)+(ot*otrate)*.02);
cin<<pgibg;
嗯……cin
不应该是将数据**输入**到一个变量,而不是将一个变量输入到cin
吗?将小于号变成大于号,错误应该就会消失。
cin >> pgibg;
这个错误就修正了!
但我没有用C++
C# 编译器、Java 编译器,几乎所有现代语言的编译器都会提供非常相似的错误信息——文件名、行号、字符号和消息——所以从第一个开始,并按照上面的步骤对它进行重复:
- 找到文件和行号。
- 阅读编译器认为是什么问题。
- 查看“有问题的”行以及它周围的几行。
- 阅读错误消息,并尝试将其与你看到的和写过的代码联系起来。
- 将它与你对该语言的了解(或使用文档)进行比较。
- 尝试修复它。
- 重新编译,然后看看会出现什么错误。
当错误消失后,重复这个检查和思考的过程来处理每一个错误消息,你很快就能全部修复它们!
避免语法错误的方法
你可以做一些事情来避免一开始就出现语法错误,或者在出现时更容易找到它们。
第一个很简单:
缩进你的代码。
在Python中,正确的缩进是绝对必要的,因为语言使用缩进来决定哪些代码行构成一个“块”——如果相邻的两行缩进相同,它们就是一个块。
但其他语言则不是这样(谢天谢地——这也会给Python带来问题)。相反,缩进是可选的——但非常值得开启。缩进后的代码不仅更易读:
private void ShowWords()
{
IEnumerable<string> canBe = words;
foreach (Control c in gbAttempts.Controls)
{
if (c is WordleLine wl && !wl.IsEmpty)
{
canBe = wl.ApplyFilter(canBe);
}
}
tbPossibles.Lines = canBe.ToArray();
IEnumerable<string> suggested = GetSuggestions(canBe);
tbRecommended.Lines = suggested.ToArray();
}
与
private void ShowWords()
{
IEnumerable<string> canBe = words;
foreach (Control c in gbAttempts.Controls)
{
if (c is WordleLine wl && !wl.IsEmpty)
{
canBe = wl.ApplyFilter(canBe);
}
}
tbPossibles.Lines = canBe.ToArray();
IEnumerable<string> suggested = GetSuggestions(canBe);
tbRecommended.Lines = suggested.ToArray();
}
而且也更容易发现遗漏的括号。
private void ShowWords()
{
IEnumerable<string> canBe = words;
foreach (Control c in gbAttempts.Controls)
{
if (c is WordleLine wl && !wl.IsEmpty)
{
canBe = wl.ApplyFilter(canBe);
}
tbPossibles.Lines = canBe.ToArray();
IEnumerable<string> suggested = GetSuggestions(canBe);
tbRecommended.Lines = suggested.ToArray();
}
private void ShowWords()
{
IEnumerable<string> canBe = words;
foreach (Control c in gbAttempts.Controls)
{
if (c is WordleLine wl && !wl.IsEmpty)
{
canBe = wl.ApplyFilter(canBe);
}
tbPossibles.Lines = canBe.ToArray();
IEnumerable<string> suggested = GetSuggestions(canBe);
tbRecommended.Lines = suggested.ToArray();
}
特别是当那个遗漏的括号导致一个与括号完全无关、且发生在不同方法中的错误时!
所以选择一个适合你的缩进风格,并每次都使用它:我不太在意你选择哪种(我使用Whitesmiths),只要你保持一致就行。大多数现代IDE会在你告诉它你希望遵循的缩进规则时提供帮助。
不要使用单字符变量名。
它们很容易拼写错误,最终变成一个完全不同的变量——而且它可能不支持你期望的属性、方法或事件。使用描述性的名称会提醒你变量中存储的内容,从而告诉你应该对它做什么:`booksList`比`a`更好,因为你知道它是一个列表,所以它包含多个项目,可以添加、删除、排序等等。`a`可以是任何东西——所以它不会帮助你避免对它调用错误的方法而产生一个无意义的语法错误!
历史
- 2022-02-16 T由Colins2捕获并修复。
- 2022-02-14 添加了Deeksha遗漏的一些内容,并添加了“避免……”部分。
- 2022-02-14 由Deeksha整理。
- 2022年2月14日:首发版本