文章

Nim基础教程

Nim是一种相对较新的编程语言,它允许用户编写易于阅读的高性能代码。 不过,如果您正在阅读本Nim教程,那么您很可能已经了解Nim。

本教程正在编写中:如果您发现任何错误或有更好的想法,请在问题跟踪中报告。

这是为谁准备的?

  • 没有或仅有极少编程经验的人

  • 具有其他编程语言编程经验的人员

  • 希望从零开始首次探索Nim的人

如何使用本教程?

本教程的目的是向您介绍编程和Nim语法的基础知识,以便您能更轻松地学习其他教程和自己进一步探索。

最好的办法不是照本宣科,而是自己去尝试,修改例子,想一些自己的例子,总之要有好奇心。 有些章节末尾的练习应该是不容错过的——不要跳过。

安装

安装Nim

Nim有适用于所有三大操作系统的现成发行版,安装Nim时有多种选择。

你可以按照官方的安装程序安装最新的稳定版。也可以使用一个名为choosenim的工具,如果你对最新的功能和错误修正感兴趣,它可以让你在稳定版和最新的开发版之间轻松切换。

无论您选择哪种方式,只需按照每个链接中说明的安装步骤进行操作,Nim就会安装完毕。我们将在下一章中检查安装是否顺利。

如果你使用的是Linux,你的发行版很有可能在软件包管理器中包含了Nim。 如果你通过这种方式安装,请确保它是最新版本(请参阅网站了解什么是最新版本),否则请通过上述两种方法之一进行安装。

在本教程中,我们将使用稳定版。本教程最初是为Nim 0.19(2018年9月发布)编写的,它应该适用于任何较新版本,包括Nim1.0。

安装其他工具

您可以在任何文本编辑器中编写Nim代码,然后在终端编译和运行。 如果您需要语法高亮显示和代码自动补全功能,流行的代码编辑器都有提供这些功能的插件。

大多数Nim用户喜欢使用VS Code编辑器,其中的Nim扩展提供语法高亮和代码自动补全功能,Code Runner扩展可用于快速编译和运行。

作者个人使用NeoVim编辑器,该插件提供语法高亮和代码自动补全等附加功能。

如果您使用的是其他代码编辑器,请参阅维基,了解可用的编辑器支持。

测试安装

在Nim中打印(如:显示在屏幕上;而不是用打印机打印在纸上)短语Hello World!非常简单,不需要任何模板代码。

在一个名为helloworld.nim的新文本文件中,我们只需编写一行代码:

helloworld.nim

1
echo "Hello World!"

注意:要打印的短语必须跟在echo命令之后,并用双引号(”)括起来。

首先,我们需要编译我们的程序,然后运行它,看看它是否按预期运行。

在文件所在的同一目录下打开终端(在Linux 下,右键单击文件管理器中的目录,即可获得 “在此打开终端 “选项;在 Windows下,应使用Shift + 右键单击,获得打开命令行的菜单选项)。

我们在终端键入程序,进行编译:

1
nim c helloworld.nim

编译成功后,我们就可以运行程序了。 在Linux系统中,我们可以在终端输入./helloworld运行程序;在 Windows系统中,我们可以输入helloworld.exe 运行程序。

此外,还可以使用一条命令同时编译和运行程序。我们需要键入:

1
nim c -r helloworld.nim

注意:c表示告诉Nim编译文件,-r表示告诉Nim立即运行文件。要查看所有编译器选项,请在终端中键入 nim --help

如果你使用的是VSCode和前面提到的Code Runner扩展,只需按下Ctrl+Alt+N,你的文件就会被编译并运行。

无论您选择哪种方式运行程序,在输出窗口(或终端)中短暂停留后,您应该会看到:

1
Hello World!

恭喜您,您已经成功运行了第一个Nim程序!

现在你知道如何在屏幕上打印一些内容(使用echo命令)、编译你的程序(在终端输入nim c programName.nim)和运行它(各种可能性)。

现在我们可以开始探索基本要素,它们将帮助我们编写简单的Nim程序。

赋值

给程序中的值命名通常有助于我们跟踪事物。如果我们询问一个用户的姓名,我们希望将其存储起来以便以后使用,而不是每次需要对其进行计算时都反复询问。

pi = 3.14的例子中,名称pi与数值3.14 相连。根据我们的经验,我们可以知道变量pi的类型是(十进制)数。

另一个例子是firstName = Alice,其中firstName是值为Alice的变量名。我们可以说这个变量的类型是一个单词。

在编程语言中也是如此,这些赋值语句有名称类型组成。

变量声明

Nim是一种静态类型编程语言,这意味着在使用赋值之前需要声明变量的类型。

在Nim中,我们还将可以将可变和不可变的变量区分开来,稍后再详述。 我们可以使用var关键字声明一个变量(可变),只需使用此语法说明其名称和类型即可(值可稍后添加):

1
var <name>: <type>

如果我们已经知道它的值,就可以立即声明一个变量并赋予它一个值:

1
var <name>: <type> = <value>

注意:角括号(<>)用于表示可以更改的内容。因此,<name>并不是字面上的名字,而是任何实际的名字。

Nim还具有类型推断能力:编译器可以根据名称赋值的值类型自动推断变量的类型,而无需明确说明类型。我们将在下一章详细介绍各种类型。

因此,我们可以像这样给变量赋值,而不需要明确的类型:

1
var <name> = <value>

Nim 中的一个例子是这样的:

1
2
var a: int  # 变量a的类型是int(整数),没有明确设置值。
var b = 7   # 变量b的值为7,其类型被自动检测为整数。

在指定名称时,重要的是要选择对程序有意义的名称。简单地命名为abc 等,很快就会变得混乱。 名称中不能使用空格,因为空格会将名称一分为二。 因此,如果您选择的名称由多个单词组成,通常的写法是使用camelCase风格(注意名称中的第一个字母应小写)。

但请注意,Nim对大小写和下划线都不敏感,也就是说,helloWorldhello_world是同一个名字。 但第一个字符是例外,它对大小写敏感。名字中还可以包含数字和其他UTF-8字符,甚至表情符号,但请记住,你和其他人都必须能够键入它们。

在同一个var块中可以声明多个变量(不一定是同一类型),而不是为每个变量键入var。 在Nim中,块是代码的一部分,具有相同的缩进(第一个字符前的空格数相同),默认缩进级别为两个空格。在 Nim程序中随处可见这样的块,而不仅仅是用于分配名称。

1
2
3
4
var
  c = -11
  d = "Hello"
  e = '!'

注意:在 Nim 中,制表符不允许作为缩进。您可以设置代码编辑器,将按下Tab键转换为任意数量的空格。 在VS代码中,默认设置是将Tab键转换为四个空格,但可以通过Ctrl+,打开设置,然后将”editor.tabSize”设置为2。

如前所述,变量是可变的,即其值可以改变(多次),但其类型必须与声明的类型保持一致。

1
2
3
4
5
6
var f = 7    # 变量f的初始值为7,类型推断为int。

f = -3       # f的值首先变为-3, 
f = 19       # 然后变为19。这两个值都是整数,与原始值类型相同。
f = "Hello"  # 如果尝试将f的值改为"Hello",则会产生错误,
             # 因为Hello不是数字,而这将使f的类型从整数变为字符串。

不可更改的赋值

与使用var关键字声明的变量不同,Nim中还存在两种赋值类型,一种是使用const关键字声明的变量,另一种是使用let关键字声明的变量。

常量

使用const关键字声明的不可变赋值的值必须在编译时(程序运行前)已知。

例如,我们可以将重力加速度声明为const g = 9.81或将圆周率声明为const pi = 3.14,因为我们事先知道它们的值,而且这些值在程序执行过程中不会改变。

1
2
3
4
5
6
7
const g = 35
g = -27         # 常量的值不可更改。 

var h = -5
const i = h + 7 # 变量h在编译时不能被求值
                #(它是一个变量,其值在程序执行过程中可能发生变化),
                # 因此常量i的值在编译时无法知道,这将导致错误。 

在某些编程语言中,常量的名称通常使用ALL_CAPS书写。在Nim中,常量的书写方式与其他变量相同。

不可变变量

let声明的不可变赋值不需要在编译时就知道,它们的值可以在程序执行过程中随时设置,但一旦设置,其值就不能改变。

1
2
3
4
5
let j = 35
j = -27       # 错误,不可变的值不可更改。 

var k = -5
let l = k + 7 # 与上面的例子相比,这个方法很有效。

在实践中,letconst 更常见。

虽然你可以对所有变量都使用var,但你的默认选择应该是let。 仅对将被修改的变量使用var

基本数据类型

整型

如前一章所述,整数是不带分数和小数点的数字。

例如32-174010_000_000都是整数。请注意,我们可以使用_作为千位分隔符,使较大的数字更易读(写成10_000_000而不是10000000时,更容易看出我们说的是1000万)。

通常的数学运算符–加法 (+)、减法 (-)、乘法 (*) 和除法(/) 的运算结果与我们所期望的一样。 前三种运算总是产生整数,而两个整数相除的结果总是浮点数(带小数点的数),即使两个数相除没有余数。

整除(舍去小数部分的除法)可以用除法运算符div来实现。 如果对整除的余数(模)感兴趣,可以用运算符mod来实现。 这两种运算的结果总是整数。

integers.nim

1
2
3
4
5
6
7
8
9
10
11
let
  a = 11
  b = 4

echo "a + b = ", a + b
echo "a - b = ", a - b
echo "a * b = ", a * b
echo "a / b = ", a / b
echo "a div b = ", a div b
echo "a mod b = ", a mod b

echo命令会将后面用逗号分隔的内容打印到屏幕上。在本例中,它首先打印字符串a + b =,然后在同一行打印表达式a + b的结果。

我们可以编译并运行上述代码,输出结果应该是

1
2
3
4
5
6
a + b = 15
a - b = 7
a * b = 44
a / b = 2.75
a div b = 2
a mod b = 3

浮点型

浮点数,简称浮点,是实数的近似表示

例如:2.73-3.145.04e7都是浮点数。 注意,我们可以使用科学记数法来表示大的浮点数,其中e后面的数字是指数。 在这个例子中,4e7是表示4 * 10^7 的记数法。

我们还可以在两个浮点数之间使用四种基本数学运算,但没有为浮点数定义运算符divmod

floats.nim

1
2
3
4
5
6
7
8
let
  c = 6.75
  d = 2.25

echo "c + d = ", c + d
echo "c - d = ", c - d
echo "c * d = ", c * d
echo "c / d = ", c / d

输出:

1
2
3
4
c + d = 9.0  
c - d = 4.5
c * d = 15.1875
c / d = 3.0  

请注意,在加法和除法的示例中,尽管我们得到的数字没有小数部分,但结果仍然是浮动类型的。

数学运算的优先级正如人们所期望的那样:乘法和除法的优先级高于加法和减法。

1
2
echo 2 + 3 * 4
echo 24 - 8 / 4

输出:

1
2
14
22.0

浮点数和整型的转换

在Nim中,不同数值类型的变量之间无法进行数学运算,并且会产生错误:

1
2
3
4
5
let
  e = 5
  f = 23.456

echo e + f   # 错误

变量的值需要转换成相同的类型。转换很简单:要转换成整数,我们使用int函数;要转换成浮点数,我们使用float函数。

1
2
3
4
5
6
7
8
9
let
  e = 5
  f = 23.987

echo float(e)     # 打印整数e 的浮点型(e仍为整数类型)
echo int(f)       # 打印浮点数f 的int版本。

echo float(e) + f # 两个操作数都是浮点数,可以相加。
echo e + int(f)   # 两个操作数都是整数,可以相加。

输出:

1
2
3
4
5.0
23
28.987
28

注意:当使用int函数将浮点数转换为整数时,不会执行四舍五入。结果只是去掉了任何小数点。要执行四舍五入,我们必须调用另一个函数,但为此我们必须进一步了解如何使用Nim。

字符

字符写在两个单钩(')之间。 字符可以是字母、符号或单个数字。多个数字或多个字母会产生错误。

1
2
3
4
5
6
let
  h = 'z'
  i = '+'
  j = '2'
  k = '35' # 错误
  l = 'xy' # 错误

字符串

字符串可以描述为一系列字符,其内容写在两个双引号(")之间。

我们可能认为字符串就是单词,但它们可能包含不止一个单词、符号或数字。

strings.nim

1
2
3
4
5
6
let
  m = "word"
  n = "A sentence with interpunction."
  o = ""    # 空字符串。
  p = "32"  # 这不是一个数字(int)。它在双引号内,因此是一个字符串。
  q = "!"   # 尽管这只是一个字符,但它不是字符,因为它是用双引号括起来的。

特殊字符

如果我们尝试打印以下字符串:

1
echo "some\nim\tips"

结果可能会让我们大吃一惊:

1
2
some
im	ips

这是因为有几个字符具有特殊含义,使用时需要在它们前面加上转义字符\

  • \n是换行符

  • \t是一个制表符

  • \\ 是反斜杠(因为一个\用作转义字符)

如果我们想打印上面的例子,有两种可能:

  • 使用\\代替\打印反斜线,或者

  • 使用语法为r"..."的原始字符串(在第一个引号前加上字母r)的原始字符串,其中没有转义字符,也没有特殊含义:所有内容都按原样打印。

1
2
echo "some\\nim\\tips"
echo r"some\nim\tips"

输出:

1
2
some\nim\tips
some\nim\tips

除上述特殊字符外,还有更多特殊字符,它们都可以在Nim 手册中找到。

字符串连接

Nim中的字符串是可变的,这意味着它们的内容可以改变。使用add函数,我们可以向现有字符串添加(追加)另一个字符串或字符。如果我们不想改变原始字符串,也可以使用&运算符连接字符串,这将返回一个新字符串。

stringConcat.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var                     # 如果我们计划修改字符串,则应将其声明为var。
  p = "abc"
  q = "xy"
  r = 'z'

p.add("def")            # 添加另一个字符串会就地修改现有字符串p,改变其值。
echo "p is now: ", p

q.add(r)                # 我们还可以在字符串中添加字符。
echo "q is now: ", q

echo "concat: ", p & q  # 将两个字符串连接起来会产生一个新字符串,而不会修改原始字符串。

echo "p is still: ", p
echo "q is still: ", q

输出:

1
2
3
4
5
p is now: abcdef
q is now: xyz
concat: abcdefxyz
p is still: abcdef
q is still: xyz

布尔型

布尔(或简称bool)数据类型只能有两个值:truefalse。 布尔值通常用于控制流(见下一章),它们通常是关系运算符的结果。

布尔变量的通常命名规则是将它们写成简单的是/否(真/假)问题,例如isEmptyisFinishedisMoving 等。

关系运算符

关系运算符测试两个实体之间的关系,这两个实体必须具有可比性。

要比较两个值是否相同,可以使用==(两个等号)。 不要将其与= 混淆,后者用于赋值,如我们前面看到的。

下面是为整数定义的所有关系运算符:

relationalOperators.nim

1
2
3
4
5
6
7
8
9
10
let
  g = 31
  h = 99

echo "g is greater than h: ", g > h
echo "g is smaller than h: ", g < h
echo "g is equal to h: ", g == h
echo "g is not equal to h: ", g != h
echo "g is greater or equal to h: ", g >= h
echo "g is smaller or equal to h: ", g <= h

输出:

1
2
3
4
5
6
g is greater than h: false
g is smaller than h: true
g is equal to h: false
g is not equal to h: true
g is greater or equal to h: false
g is smaller or equal to h: true

我们还可以比较字符和字符串:

relationalOperators.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let
  i = 'a'
  j = 'd'
  k = 'Z'

echo i < j
echo i < k  # 所有大写字母都在小写字母之前。

let
  m = "axyb"
  n = "axyz"
  o = "ba"
  p = "ba "

echo m < n  # 字符串比较是逐个字符进行的。前三个字符相同,字符b小于字符z。
echo n < o  # 如果字符不完全相同,字符串长度在比较中并不重要。
echo o < p  # 较短的字符串比较长的字符串小。

逻辑运算符

逻辑运算符用于检验由一个或多个布尔值组成的表达式的真假。

  • 逻辑and只有当两个成员都为true才返回true

  • 逻辑or如果至少有一个成员为false,则返回true

  • 逻辑xor如果一个成员为真,而另一个成员为假,则返回true

  • 逻辑not否定其成员的真假:将true变为false,反之亦然(它是唯一只接受一个操作数的逻辑运算符)

logicalOperators.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
echo "T and T: ", true and true
echo "T and F: ", true and false
echo "F and F: ", false and false
echo "---"
echo "T or T: ", true or true
echo "T or F: ", true or false
echo "F or F: ", false or false
echo "---"
echo "T xor T: ", true xor true
echo "T xor F: ", true xor false
echo "F xor F: ", false xor false
echo "---"
echo "not T: ", not true
echo "not F: ", not false

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
T and T: true
T and F: false
F and F: false
---
T or T: true
T or F: true
F or F: false
---
T xor T: false
T xor F: true
F xor F: false
---
not T: false
not F: true

关系运算符和逻辑运算符可以组合在一起,形成更复杂的表达式。

例如:(5 < 7) and (11 + 9 == 32 - 2*6)将变为true and (20 == 20),进一步又变为true and true,最终得出true的最终结果。

回顾

这一章是本教程中最长的一章,我们涉及了很多内容。 请慢慢阅读每种数据类型,并尝试使用每种数据类型做什么。

类型初看起来似乎是一种限制,但它们允许Nim编译器加快代码速度,并确保您不会意外做错什么——这对大型代码库尤其有益。

现在,您已经知道了基本数据类型和对它们的几种操作,这应该足以在Nim中进行一些简单的计算。 通过做下面的练习来测试您的知识。

练习

  1. 创建一个包含您年龄(以年为单位)的不可变变量。以天为单位打印您的年龄。(1年 = 365天)

  2. 检查您的年龄是否能被3整除(提示:使用mod

  3. 创建一个不可变变量,其中包含以厘米为单位的身高。以英寸为单位打印您的身高。(1英寸 = 2.54厘米)

  4. 一根管道的直径是3/8英寸。请用厘米表示直径。

  5. 创建一个包含姓名的不可变变量,另一个包含姓氏。将前两个变量连接起来,创建一个变量fullName。别忘了在中间留一个空格。打印你的全名。

  6. 爱丽丝每15天赚400美元。鲍勃每小时挣3.14美元,每周工作7天,每天工作8小时。30天后,爱丽丝的收入比鲍勃多吗?(提示:使用关系运算符)

控制流

到目前为止,在我们的程序中,每一行代码都会在某个时刻被执行。 控制流语句允许我们将代码的某些部分设置为只有在满足某个布尔条件时才会被执行。

如果把我们的程序看作一条路,我们就可以把控制流看作不同的分支,并根据某些条件选择我们的路径。 例如,只有当鸡蛋的价格小于某个值时,我们才会买鸡蛋;或者,如果下雨了,我们会带雨伞,否则(else)我们会带太阳镜。

伪代码写成的这两个示例如下:

1
2
3
4
5
6
7
if eggPrice < wantedPrice:
  buyEggs

if isRaining:
  bring umbrella
else:
  bring sunglasses

Nim的语法非常相似,如下所示。

if语句

如上图所示的if语句是分支程序的最简单方法。

编写if语句的Nim语法是

1
2
if <condition>:     # 条件必须是布尔类型:布尔变量或逻辑表达式。
  <indented block>  # if行之后缩进两个空格的所有行都是同一个块,只有当条件为真时才会被执行。

if语句可以嵌套,即在一个if块内可以有另一个if语句。

if.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let
  a = 11
  b = 22
  c = 999

if a < b: # 第一个条件为真,第二个条件为假,即不执行下面的echo。
  echo "a is smaller than b"
  if 10*a < b:  
    echo "not only that, a is *much* smaller than b"

if b < c: # 两个条件都为真,两行都打印出来。
  echo "b is smaller than c"
  if 10*b < c:  
    echo "not only that, b is *much* smaller than c"

if a+b > c: # 第一个条件为假,即跳过其块内的所有行,不打印任何内容。
  echo "a and b are larger than c"
  if 1 < 100 and 321 > 123: # 在if语句中使用逻辑和。
    echo "did you know that 1 is smaller than 100?"
    echo "and 321 is larger than 123! wow!"

输出:

1
2
3
a is smaller than b
b is smaller than c
not only that, b is *much* smaller than c

else语句

Else位于if块之后,允许我们在if语句中的条件不为真时执行代码分支。

else.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
let
  d = 63
  e = 2.718

if d < 10:
  echo "d is a small number"
else:
  echo "d is a large number"

if e < 10:
  echo "e is a small number"
else:
  echo "e is a large number"

输出:

1
2
d is a large number
e is a small number

注意:如果只想在语句为假的情况下执行代码块,可以使用not操作符简单地否定条件。

elif语句

Elif是”else if”的缩写,它使我们能够将多个if语句串联起来。

程序会测试每一条语句,直到找到一条为真语句为止。之后,所有语句都会被忽略。

elif.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let
  f = 3456
  g = 7

if f < 10:
  echo "f is smaller than 10"
elif f < 100:
  echo "f is between 10 and 100"
elif f < 1000:
  echo "f is between 100 and 1000"
else:
  echo "f is larger than 1000"

if g < 1000:
  echo "g is smaller than 1000"
elif g < 100:
  echo "g is smaller than 100"
elif g < 10:
  echo "g is smaller than 10"

输出:

1
2
f is larger than 1000
g is smaller than 1000

注意:在g的情况下,即使g满足所有三个条件,也只会执行第一个分支,自动跳过所有其他分支。

case语句

case语句是另一种只从多个可能路径中选择一个的方法,类似于带有多个elif的if语句。不过,case语句并不接受多个布尔条件,而是接受所有可能的状态值,并为每个可能值提供一条路径。

用if-elif块编写的代码是这样的:

1
2
3
4
5
6
7
8
if x == 5:
  echo "Five!"
elif x == 7:
  echo "Seven!"
elif x == 10:
  echo "Ten!"
else:
  echo "unknown number"

可以用这样的case语句来编写:

1
2
3
4
5
6
7
8
9
case x
of 5:
  echo "Five!"
of 7:
  echo "Seven!"
of 10:
  echo "Ten!"
else:
  echo "unknown number"

与if语句不同,case语句必须涵盖所有可能的情况。如果对其中某些情况不感兴趣,则可以使用else: discard

case.nim

1
2
3
4
5
6
7
8
9
10
11
12
let h = 'y'

case h
of 'x':
  echo "You've chosen x"
of 'y':
  echo "You've chosen y"
of 'z':
  echo "You've chosen z"
else: discard  # 尽管我们只对h的三个值感兴趣,
               # 但我们必须包含这一行,以涵盖所有其他可能的情况(所有其他字符)。
               # 否则,代码将无法编译。

如果需要对多个值执行相同的操作,我们也可以对每个分支使用多个值。

multipleCase.nim

1
2
3
4
5
6
7
8
9
10
11
let i = 7

case i
  of 0:
    echo "i is zero"
  of 1, 3, 5, 7, 9:
    echo "i is odd"
  of 2, 4, 6, 8:
    echo "i is even"
  else:
    echo "i is too large"

输出:

1
i is odd

循环

循环是另一种控制流结构,它允许我们多次运行代码的某些部分。

在本章中,我们将遇到两种循环:

  • for循环:运行已知次数

  • while循环:只要满足某些条件就运行

for循环

for循环的语法是

1
2
for <loopVariable> in <iterable>:
  <loop body>

传统上,i通常用作loopVariable的名称,但也可以使用任何其他名称。该变量只能在循环内部使用。 一旦循环结束,变量值将被丢弃。

iterable对象是我们可以迭代的任何对象。 在前面提到的类型中,字符串是可迭代对象(下一章将介绍更多可迭代类型)。

每次循环时,loop body中的所有行都会被执行,这样我们就能高效地编写重复的代码部分。

如果我们想在Nim中遍历一个范围内的(整数)数字,迭代的语法是start ... finish,其中startfinish都是数字。这将遍历startfinish之间的所有数字,包括startfinish。 对于默认范围迭代,start必须小于finish

如果我们想迭代到一个数(不包括它),可以使用..<

for1.nim

1
2
3
4
5
6
7
for n in 5 .. 9:  # 使用“..”迭代数字范围 - 两端都包含在范围内。
  echo n

echo ""

for n in 5 ..< 9: # 使用“..<”在同一范围内迭代-它一直迭代到高端,而不包括高端。
  echo n

输出:

1
2
3
4
5
6
7
8
9
10
5
6
7
8
9

5
6
7
8

如果我们想以不同于1的步长遍历一个数字范围,则需要使用countup。使用countup,我们可以定义起始值、停止值(包含在范围内)和步长。

for2.nim

1
2
for n in countup(0, 16, 4):  # 从0数到16,步长为4。
  echo n

输出:

1
2
3
4
5
0
4
8
12
16

要遍历一个起始值大于终止值的数字范围,需要使用一个名为countdown的类似函数。 即使是倒数,步长也必须是正数。

for2.nim

1
2
3
4
5
6
7
8
for n in countdown(4, 0):   #  要从一个较高的数字迭代到一个较低的数字,我们必须使用countdown
                            #(“..”操作符只能在起始值小于结束值时使用)。
  echo n

echo ""

for n in countdown(-3, -9, 2):  # 即使是倒数,步长也必须是正数。
  echo n

输出:

1
2
3
4
5
6
7
8
9
10
4
3
2
1
0

-3
-5
-7
-9

由于字符串是可迭代的,我们可以使用for循环来迭代字符串中的每个字符(这种迭代有时称为for-each循环)。

for3.nim

1
2
3
4
let word = "alphabet"

for letter in word:
  echo letter

输出:

1
2
3
4
5
6
7
8
a
l
p
h
a
b
e
t

如果我们还需要对迭代次数计数(从零开始),可以通过for <counterVariable>, <loopVariable> in <iterator>:语法来实现。 如果你想迭代一个可迭代对象,同时在同一偏移量访问另一个可迭代对象,这将非常实用。

for3.nim

1
2
for i, letter in word:
  echo "letter ", i, " is: ", letter

输出:

1
2
3
4
5
6
7
8
letter 0 is: a
letter 1 is: l
letter 2 is: p
letter 3 is: h
letter 4 is: a
letter 5 is: b
letter 6 is: e
letter 7 is: t

While循环

While循环与if语句类似,但只要条件为真,它们就会一直执行代码块。 当我们事先不知道循环将运行多少次时,就会使用While循环。

我们必须确保循环在某个时刻终止,而不是成为一个无限循环

while.nim

1
2
3
4
5
6
7
var a = 1

while a*a < 10: # 每次进入新循环并执行其中的代码前,都会检查该条件。
  echo "a is: ", a
  inc a         # inc用于将a递增1。它与a = a + 1或a += 1的写法相同。

echo "final value of a: ", a

输出:

1
2
3
4
a is: 1
a is: 2
a is: 3
final value of a: 4

break和continue

break语句用于提前退出循环,通常是在满足某些条件时。

在下一个示例中,如果没有if语句和break语句,循环将继续运行并打印,直到i变为1000。 有了break语句,当i变为3时,我们将立即退出循环(在打印i 值之前)。

break.nim

1
2
3
4
5
6
7
var i = 1

while i < 1000:
  if i == 3:
    break
  echo i
  inc i

输出:

1
2
1
2

continue语句会立即开始循环的下一次迭代,而不执行当前迭代的剩余行。请注意下面代码的输出中缺少了3和6:

continue.nim

1
2
3
4
for i in 1 .. 8:
  if (i == 3) or (i == 6):
    continue
  echo i

输出:

1
2
3
4
5
6
1
2
4
5
7
8

练习

  1. 科拉茨猜想是一个规则简单的数学难题。首先选择一个数字。如果是奇数,乘以3再加1;如果是偶数,除以2。重复这个过程,直到得出1。例如:5 → 奇数 → 3*5 + 1 = 16 → 偶数 → 16 / 2 = 8 → 偶数 → 4 → 2 → 1 → 结束!
    选择一个整数(作为可变变量),创建一个循环,打印科拉兹猜想的每一步。(提示:使用div表示除法)

  2. 创建一个包含您全名的不可变变量。编写一个for循环,遍历该字符串,只打印元音(a、e、i、o、u)。(提示:在每个分支中使用带有多个值的case语句)

  3. Fizz buzz是一款儿童游戏,有时用于测试基本编程知识。我们从1开始数数字。如果一个数字能被3整除,就用”fizz”代替;如果能被5整除,就用”buzz”代替;如果能被15整除(包括3和5),就用”fizzbuzz”代替。前几轮是这样的1, 2, fizz, 4, buzz, fizz, 7, …
    创建一个程序,打印Fizz buzz的前30轮。(提示:注意试除的顺序)

  4. 在前面的练习中,你已经将英寸换算成厘米,反之亦然。创建一个包含多个值的转换表。例如,表格可能如下所示:

incm
12.54
410.16
717.78
1025.4
1333.02
1640.64
1948.26

容器

容器是一种数据类型,它包含一系列的条目,并允许我们访问这些元素。通常情况下,容器也是可迭代的,这意味着我们可以像在循环章节中使用字符串一样使用它们。

例如,杂货清单是我们想要购买的物品的容器,而素数清单则是数字的容器。用伪代码编写:

1
2
groceryList = [ham, eggs, bread, apples]
primes = [1, 2, 3, 5, 7]

数组

数组是最简单的容器类型。数组是同质的,即数组中的所有元素必须具有相同的类型。数组的大小也是恒定的,这意味着元素的数量(或者说:可能的元素数量)必须在编译时已知。这意味着我们称数组为”长度恒定的同质容器”。

数组类型使用array[<length>, <type>] 声明,其中length是数组的总容量(可容纳的元素个数),type是所有元素的类型。如果长度和类型都可以从传递的元素中推断出来,则可以省略声明。

数组的元素用方括号括起来。

1
2
3
4
5
6
7
8
9
var
  a: array[3, int] = [5, 7, 9]
  b = [5, 7, 9]        # 如果我们提供了值,那么数组b的长度和类型在编译时就已经知道了。
                       # 虽然像数组a那样特别声明它是正确的,但没有必要。

  c = []               # 错误,从这种声明中无法推断元素的长度或类型,因此会产生错误。

  d: array[7, string]  # 声明一个空数组(稍后将被填充)的正确方法是给出它的长度和类型,
                       # 而不提供其元素的值——数组d可以包含7个字符串。

由于数组的长度必须在编译时已知,因此这种方法行不通:

1
2
3
4
5
const m = 3
let n = 5

var a: array[m, char]
var b: array[n, char] # 错误

注意:这会产生一个错误,因为n是用let声明的,编译时并不知道它的值。我们只能使用用const声明的值作为数组初始化的长度参数。

序列

序列是与数组类似的容器,但其长度不必在编译时知道,在运行时也可以改变:我们只用seq[<type>] 声明所含元素的类型。序列也是同质的,即序列中的每个元素都必须是相同的类型。

序列的元素括在@[]之间。

1
2
3
var
  e1: seq[int] = @[]   # 必须声明空序列的类型。
  f = @["abc", "def"]  # 可以推断非空序列的类型。在本例中,它是一个包含字符串的序列。

初始化空序列的另一种方法是调用过程newSeq。我们将在下一章详细介绍过程调用,但现在只需知道这也是一种可能性:

1
var e = newSeq[int]()

提示:将类型参数放在方括号内,可以让过程知道它将返回一个特定类型的序列。经常出现的错误是遗漏了最后的(),这一点必须包括在内。

我们可以使用add函数向序列中添加新元素,这与我们处理字符串的方法类似。序列必须是可变的(用var 定义),而且我们要添加的元素必须与序列中的元素类型相同,这样才能发挥作用。

seq.nim

1
2
3
4
5
6
7
8
9
var
  g = @['x', 'y']
  h = @['1', '2', '3']

g.add('z') # 添加相同类型(char)的新元素。
echo g

h.add(g)   # 添加另一个包含相同类型的序列。
echo h

输出:

1
2
@['x', 'y', 'z']
@['1', '2', '3', 'x', 'y', 'z']

尝试向现有序列传递不同的类型会产生错误:

1
2
3
4
var i = @[9, 8, 7]

i.add(9.81) # 错误,尝试在一个int序列中添加一个浮点数。
g.add(i)    # 错误,尝试将int序列添加到char序列。

由于序列的长度可以变化,我们需要一种方法来获取它们的长度,为此我们可以使用len函数。

1
2
3
4
5
var i = @[9, 8, 7]
echo i.len

i.add(6)
echo i.len

输出:

1
2
3
4

索引和切片

索引允许我们通过索引从容器中获取特定元素。将索引视为容器内部的一个位置。

Nim和许多其他编程语言一样,采用基于零的索引,即容器中的第一个元素的索引为零,第二个元素的索引为一,等等。

如果我们想”从后面”索引,可以使用^前缀。最后一个元素(从后面第一个元素)的索引为^1

索引的语法是<container>[<index>]

indexing.nim

1
2
3
4
let j = ['a', 'b', 'c', 'd', 'e']

echo j[1]   # 基于零的索引:位于索引1的元素是b。
echo j[^1]  # 获取最后一个元素。

输出:

1
2
b
e

切片允许我们通过一次调用获得一系列元素,它使用的语法与范围相同(在for循环部分介绍)。

如果使用start .. stop语法,两端都包含在切片中;如果使用start ..< stop语法,stop索引不包含在切片中。

切片的语法是<container>[<start> .. <stop>]

indexing.nim

1
2
echo j[0 .. 3]
echo j[0 ..< 3]

输出:

1
2
@[a, b, c, d]
@[a, b, c]

索引和切分都可用于为现有的可变容器和字符串分配新值。

assign.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var
  k: array[5, int]
  l = @['p', 'w', 'r']
  m = "Tom and Jerry"

for i in 0 .. 4:  # 长度为5的数组具有从0到4的索引。我们将为数组的每个元素赋值。
  k[i] = 7 * i
echo k

l[1] = 'q'        # 指定(更改)序列的第二个元素(索引1)。
echo l

m[8 .. 9] = "Ba"  # 更改索引8和9处字符串的字符。
echo m

输出:

1
2
3
[0, 7, 14, 21, 28]
@['p', 'q', 'r']
Tom and Barry

元组

到目前为止,我们看到的两种容器都是同质的。而元组则包含异质数据,即元组中的元素可以是不同类型的。与数组类似,元组也有固定大小。

元组的元素用括号括起来。

tuples.nim

1
2
let n = ("Banana", 2, 'c')  # 元组可以包含不同类型的字段。在本例中是字符串、int和char。
echo n

输出:

1
(Field0: "Banana", Field1: 2, Field2: 'c')

我们还可以为元组中的每个字段命名,以区分它们。这可以用来访问元组中的元素,而不是索引。

tuples.nim

1
2
3
4
5
var o = (name: "Banana", weight: 2, rating: 'c')

o[1] = 7          # 使用字段索引更改字段值。
o.name = "Apple"  # 使用字段名称更改字段值。
echo o

输出:

1
(name: "Apple", weight: 7, rating: 'c')

练习

  1. 创建一个可包含十个整数的空数组。

    • 在数组中填入数字10、20……100。(提示:使用循环)

    • 只打印该数组中奇数索引的元素(值 20、40……)。

    • 将偶数索引上的元素乘以5。

  2. 重做科拉茨猜想练习,但这次不是打印每一步,而是将其添加到序列中。

    • 选择一个起始数字。有趣的选择包括 9、19、25和27。

    • 创建一个序列,其唯一成员是起始编号。

    • 使用与之前相同的逻辑,继续向序列中添加元素,直到添加到1。

    • 打印序列的长度和序列本身。

  3. 在2到100的范围内,找出能产生最长科拉茨数列的数字。

    • 计算给定范围内每个数字的科拉茨序列

    • 如果当前序列的长度比上一条记录长,则将当前长度和起始编号保存为一条新记录(可以使用元组(longestLength,startingNumber)或两个单独的变量)

    • 打印出最长序列的起始编号及其长度

过程

过程,或其他编程语言中的函数,是执行特定任务的代码部分,打包成一个单元。 这样将代码组合在一起的好处是,当我们想使用过程的代码时,可以调用这些过程,而不用重新编写所有代码。

在前面的章节中,我们在不同的场景下研究了科拉兹猜想。通过将科拉兹猜想的逻辑封装成一个过程,我们可以在所有练习中调用相同的代码。

到目前为止,我们已经使用了许多内置过程,例如用于打印的echo、用于向序列中添加元素的add、用于增加整数值的inc、用于获取容器长度的len等。

使用过程的优势包括

  • 减少代码重复

  • 更容易阅读代码,因为我们可以根据代码的作用来命名代码片段

  • 将复杂的任务分解为较简单的步骤

正如本节开头提到的,在其他语言中,过程通常被称为函数。 如果我们考虑一下函数的数学定义,这实际上有点名不副实。 数学函数接受一组参数(如f(x,y),其中f是一个函数,xy是它的参数),对于相同的输入,函数总是返回相同的答案。

另一方面,程序过程并不总是为给定的输入返回相同的输出。有时,它们根本不返回任何东西。这是因为我们的计算机程序可以将状态存储在我们前面提到的变量中,而程序可以读取并更改这些变量。在Nim中,func这个词目前被保留作为一种数学上更正确的函数来使用,它不会产生任何副作用。

过程的声明

在使用(调用)过程之前,我们需要创建它并定义它的功能。

使用proc关键字和过程名称声明过程,然后在括号内声明输入参数及其类型,最后是冒号和过程返回值的类型,就像这样:

1
proc <name>(<p1>: <type1>, <p2>: <type2>,...): <returnType>

过程的正文写在声明后面的缩进块中,并用=符号标出。

callProcs.nim

1
2
3
4
5
6
7
proc findMax(x: int, y: int): int =  # 声明名为findMax的过程,它有两个参数x和y,并返回一个int类型。
  if x > y:
    return x  # 要从过程中返回值,我们需要使用return关键字。
  else:
    return y
  # this is inside of the procedure
# this is outside of the procedure
1
2
3
4
5
6
proc echoLanguageRating(language: string) = 
  case language
  of "Nim", "nim", "NIM":
    echo language, " is the best language!"
  else:
    echo language, " might be a second-best language."

过程echoLanguageRating只回显给出的名称,不返回任何内容,因此没有声明返回类型。

通常情况下,我们不能更改给定的任何参数,这样做会导致错误:

1
2
3
4
5
proc changeArgument(argument: int) =
  argument += 5

var ourVariable = 10
changeArgument(ourVariable)

为了实现这一目标,我们需要允许Nim和使用我们过程的程序员通过将参数声明为变量来更改参数:

1
2
3
4
5
6
7
8
proc changeArgument(argument: var int) = 
  argument += 5

var ourVariable = 10
changeArgument(ourVariable)
echo ourVariable
changeArgument(ourVariable)
echo ourVariable

注意参数现在是作为var int声明的,而不仅仅是一个int。

输出:

1
2
15
20

当然,这意味着我们传递给它的名称也必须声明为变量,如果传递用constlet赋值的名称,则会出错。

虽然将变量作为参数传递是一种好的做法,但也可以使用在过程之外声明的变量,包括变量和常量:

1
2
3
4
5
6
7
8
var x = 100

proc echoX() =
  echo x  # 这里我们访问外部变量x。
  x += 1  # 我们还可以更新它的值,因为它被声明为一个变量。

echoX()
echoX()

输出:

1
2
100
101

过程的调用

在声明了过程后,我们就可以调用它了。在许多编程语言中,调用过程和函数的通常方法是说明其名称并在括号中提供参数,就像这样:

1
<procName>(<arg1>, <arg2>, ...)

调用过程的结果可以存储在变量中。

如果我们想调用上例中的findMax过程,并将返回值保存在一个变量中,我们就可以这样做:

callProcs.nim

1
2
3
4
5
6
7
8
9
let
  a = findMax(987, 789)
  b = findMax(123, 321)
  c = findMax(a, b)  # 函数findMax的结果在这里被命名为c,
                     # 并与我们前两次调用的结果(findMax(987, 321))一起调用。

echo a
echo b
echo c

输出:

1
2
3
987
321
987

与许多其他语言不同,Nim还支持统一函数调用语法,允许以多种不同方式调用过程。

这是一种调用,第一个参数写在函数名之前,其余参数用括号注明:

1
<arg1>.<procName>(<arg2>, ...)

在向现有序列添加元素时,我们使用了这种语法(<seq>.add(<element>)),因为它比add(<seq>, <element>)更易于阅读,也更清楚地表达了我们的意图。 我们还可以省略参数周围的括号:

1
<procName> <arg1>, <arg2>, ...

我们在调用echo过程和调用不带任何参数的len过程时看到过这种方式。 这两种方式也可以这样组合,但这种语法并不常见:

1
<arg1>.<procName> <arg2>, <arg3>, ...

统一的调用语法使多个过程的链条更易于阅读:

ufcs.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
proc plus(x, y: int): int =  # 如果多个参数的类型相同,我们可以用这种简洁的方式声明它们的类型。
  return x + y

proc multi(x, y: int): int =
  return x * y

let
  a = 2
  b = 3
  c = 4

echo a.plus(b) == plus(a, b)
echo c.multi(a) == multi(c, a)


echo a.plus(b).multi(c)  # 首先,我们将a和b相加,
                         # 然后将运算结果(2 + 3 = 5)作为第一个参数传递给multi过程,
                         # 再与c相乘(5 * 4 = 20)。
echo c.multi(b).plus(a)  # 首先,我们将c和b 相乘,
                         # 然后将运算结果(4 * 3 = 12)作为第一个参数传递给加法过程,
                         # 再与a相加(12 + 2 = 14)。

输出:

1
2
3
4
true
true
20
14

结果变量

在Nim中,每个返回值的过程都有一个隐式声明和初始化(带有默认值)的result变量。 当过程到达其缩进块的末尾时,即使没有return语句,也会返回该result变量的值。

result.nim

1
2
3
4
5
6
7
8
proc findBiggest(a: seq[int]): int =  #返回类型为int。结果变量初始化为int的默认值:0。
  for number in a:
    if number > result:
      result = number
  # 程序结束时,将返回结果值。

let d = @[3, -5, 11, 33, 7, -15]
echo findBiggest(d)

输出:

1
33

请注意,此过程只是为了演示result变量,并不是100%正确的:如果您传递的序列只包含负数,那么此过程将返回0(序列中包含负数)。

注意:在较早的Nim版本(Nim 0.19.0之前)中,字符串和序列的默认值为nil,当我们使用它们作为返回类型时,结果变量需要初始化为空字符串(”“)或空序列(@[])。

result.nim

1
2
3
4
5
6
7
8
9
proc keepOdds(a: seq[int]): seq[int] =
  # result = @[]          
  for number in a:
    if number mod 2 == 1:
      result.add(number)


let f = @[1, 6, 4, 43, 57, 34, 98]
echo keepOdds(f)

在0.19.0及更新版本的Nim中,不需要result = @[]这一行——序列会自动初始化为空序列。在较早的Nim版本中,序列必须初始化,如果没有这一行,编译器会出错。(注意不要使用var,因为result已经隐式声明)。

输出:

1
@[1, 43, 57]

在过程内部,我们还可以调用其他过程。

filterOdds.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
proc isDivisibleBy3(x: int): bool =
  return x mod 3 == 0

proc filterMultiplesOf3(a: seq[int]): seq[int] =
  # result = @[]  # 同样,在较新版本的 Nim 中不需要这一行。
  for i in a:
    if i.isDivisibleBy3():  # 调用先前声明的过程。它的返回类型是bool,可以在if语句中使用。
      result.add(i)


let
  g = @[2, 6, 5, 7, 9, 0, 5, 3]
  h = @[5, 4, 3, 2, 1]
  i = @[626, 45390, 3219, 4210, 4126]

echo filterMultiplesOf3(g)
echo h.filterMultiplesOf3()
echo filterMultiplesOf3 i   # 调用过程的第三种方法,如上文所述。

输出:

1
2
3
@[6, 9, 0, 3]
@[3]
@[45390, 3219]

前置声明

正如本节开头提到的,我们可以在不使用代码块的情况下声明过程,原因是我们必须先声明过程,然后才能调用它们:

1
2
3
4
echo 5.plus(10) # 错误,由于plus还未定义      

proc plus(x, y: int): int =  # 在这里,我们定义了plus,但由于是在使用之后,Nim还不知道它。
  return x + y

解决这个问题的方法就是所谓的前置声明:

1
2
3
4
5
6
proc plus(x, y: int): int   # 在这里,我们要告诉Nim,它应该根据这个定义来考虑plus过程的存在。

echo 5.plus(10)             # 现在,我们可以在代码中自由使用它,这样就可以了。

proc plus(x, y: int): int = # 这就是plus的实际过程,当然必须符合我们之前的定义。
  return x + y

练习

  1. 创建一个过程,根据所提供的姓名向某人问好(打印 “Hello ")。创建一系列名字。使用创建的过程问候每个人。

  2. 创建过程findMax3,返回三个值中最大的一个。

  3. 二维平面上的点可以用tuple[x, y: float] 表示。请编写一个过程,接收两个点并返回一个新点,新点是这两个点的总和(将x和y分别相加)。

  4. 创建两个程序ticktock,分别打印”tick”和 “tock”。用一个全局变量来记录它们运行了多少次,然后一个接一个地运行,直到计数器达到20。预期的输出结果是”tick”和”tock”交替运行20次。(提示:使用正向声明)。

提示:如果进入无限循环,可以按Ctrl+C停止程序的执行。

使用不同参数调用所有程序,对其进行测试。

模块

到目前为止,我们已经使用了每次启动新的Nim文件时默认提供的功能,这些功能可以通过模块进行扩展,从而为某些特定主题提供更多的功能。

最常用的Nim模块包括

  • strutils: 处理字符串时的附加功能

  • sequtils:序列的附加功能

  • math:数学函数(对数、平方根……)、三角函数(正弦、余弦……)。

  • times:衡量和处理时间

导入模块

如果我们想导入一个模块及其所有功能,只需在文件中输入import <moduleName>即可。 这通常是在文件的顶部进行,这样我们就能很容易地看到我们的代码使用了哪些功能。

stringutils.nim

1
2
3
4
5
6
7
8
9
import strutils       # 导入strutils。

let
  a = "My string with whitespace."
  b = '!'

echo a.split()        # 使用strutils模块中的split。它能将字符串分割成一串单词。
echo a.toUpperAscii() # toUpperAscii将所有ASCII字母转换为大写字母。
echo b.repeat(5)      # repeat也来自strutils模块,它会按要求的次数重复一个字符或整个字符串。

输出:

1
2
3
@["My", "string", "with", "whitespace."]
MY STRING WITH WHITESPACE.
!!!!!

提示:对于来自其他编程语言(尤其是 Python)的用户来说,Nim中的导入方式可能看起来”不对”。如果是这样的话,我们推荐您阅读这篇文章

maths.nim

1
2
3
4
5
6
7
8
9
10
11
import math                 # 导入math模块

let
  c = 30.0 # degrees
  cRadians = c.degToRad()   # 用degToRad将度转换为弧度。

echo cRadians
echo sin(cRadians).round(2) # sin取弧度。我们将结果四舍五入(也来自数学模块)至多保留两位小数。
                            # (否则结果将是:0.4999999999999999)

echo 2^5                    # 数学模块还有用于计算数字幂次的^运算符。

输出:

1
2
3
0.5235987755982988
0.5
32

创建模块

很多时候,我们在一个项目中会有太多的代码,因此有必要将它们分割成若干块,每一块都做某一件事。 如果你在一个文件夹中并排创建了两个文件,让我们称它们为firstFile.nimsecondFile.nim ,你可以从另一个文件中导入一个文件作为模块:

firstFile.nim

1
2
3
4
5
6
proc plus*(a, b: int): int = # 请注意,"plus"过程的名称后面有一个星号(*),
                             # 这告诉Nim,另一个导入此过程的文件将可以使用此过程。
  return a + b

proc minus(a, b: int): int = # 相比之下,在导入该文件时将看不到这个过程。
  return a - b

secondFile.nim

1
2
3
4
import firstFile          # 这里我们导入firstFile.nim。我们不需要在这里添加.nim扩展名。

echo plus(5, 10)          # 这将正常工作,并输出15,因为它已在firstFile中声明,我们也能看到。
echo minus(10, 5)         # 错误,由于减号过程的名称后面没有星号,因此不可见。

如果您的文件不止这两个,您可能需要将它们整理到一个子目录(或多个子目录)中。 目录结构如下:

1
2
3
4
5
6
7
8
.
├── myOtherSubdir
│   ├── fifthFile.nim
│   └── fourthFile.nim
├── mySubdir
│   └── thirdFile.nim
├── firstFile.nim
└── secondFile.nim

如果您想在secondFile.nim中导入所有其他文件,可以这样做:

secondFile.nim

1
2
3
import firstFile
import mySubdir/thirdFile
import myOtherSubdir / [fourthFile, fifthFile]

与用户输入互动

利用我们迄今为止介绍过的内容(基本数据类型和容器、控制流、循环),我们可以制作一些简单的程序。

在本章中,我们将学习如何使我们的程序更具交互性。为此,我们需要一个从文件中读取数据或要求用户输入的选项。

读取文件

假设我们在Nim代码的同一目录下有一个名为people.txt的文本文件。 该文件的内容如下:

people.txt

1
2
3
4
Alice A.
Bob B.
Carol C.

我们希望在程序中使用该文件的内容,即名称列表(序列)。

readFromFile.nim

1
2
3
4
5
6
7
8
9
10
11
import strutils

let contents = readFile("people.txt") # 要读取文件内容,我们使用过程readFile,并提供要读取文件的路径
                                      #(如果文件与我们的Nim程序位于同一目录,提供文件名即可)。
                                      # 结果是一个多行字符串。
echo contents

let people = contents.splitLines()    # 要将多行字符串拆分成一系列字符串
                                      #(每个字符串包含一行的所有内容),
                                      # 我们可以使用strutils模块中的splitLines。
echo people

输出:

1
2
3
4
5
Alice A.
Bob B.
Carol C.
            
@["Alice A.", "Bob B.", "Carol C.", ""] 
  1. 原始文件中有最后一行新内容(最后一行为空),这里也有。
  2. 由于最后一行为新行,得到的序列比预期的要长。

为了解决最后一行新行的问题,我们可以在读取文件后使用strutils中的strip过程。它所做的就是删除字符串开头和结尾的所谓空白。所谓空白,就是任何会产生空格的字符,如新行、空格、制表符等。

readFromFile2.nim

1
2
3
4
5
6
7
import strutils

let contents = readFile("people.txt").strip() # 使用strip可以达到预期效果。
echo contents

let people = contents.splitLines()
echo people

输出:

1
2
3
4
Alice A.
Bob B.
Carol C.
@["Alice A.", "Bob B.", "Carol C."]

读取用户输入

如果我们想与用户交互,就必须能够要求他们输入信息,然后进行处理和使用。我们需要通过将stdin传递给过程readLine来读取标准输入(stdin)

interaction1.nim

1
2
3
4
echo "Please enter your name:"
let name = readLine(stdin)  # name的类型被推断为字符串。

echo "Hello ", name, ", nice to meet you!"

输出:

1
2
Please enter your name:

等待用户输入。写入姓名并按Enter键后,程序将继续运行。 输入“Alice”后:

1
2
3
Please enter your name:
Alice
Hello Alice, nice to meet you!

提示:如果您使用的是过时版本的VS Code,就不能按通常的方法运行(使用Ctrl+Alt+N),因为输出窗口不允许用户输入 - 您需要在终端中运行这些示例。较新版本的VS Code没有这种限制。

处理数字

从文件或用户输入读取的结果总是字符串。如果我们想使用数字,就需要将字符串转换为数字:我们再次使用strutils模块,使用parseInt将字符串转换为整数,或使用parseFloat将字符串转换为浮点数。

interaction2.nim

1
2
3
4
5
6
7
8
9
10
11
import strutils

echo "Please enter your year of birth:"
let yearOfBirth = readLine(stdin).parseInt() # 将字符串转换为整数。
                                             # 这样写时,我们相信用户会给出一个有效的整数。
                                             # 如果用户输入"'79"或"ninety-three",会发生什么?
                                             # 自己试试看。

let age = 2018 - yearOfBirth

echo "You are ", age, " years old."

输出:

1
2
3
Please enter your year of birth:
1934
You are 84 years old.

如果我们在与Nim代码相同的目录下有文件numbers.txt,内容如下:

numbers.txt

1
2
3
4
5
6
27.3
98.24
11.93
33.67
55.01

如果我们想读取该文件,并找出所提供数字的总和与平均值,我们可以这样做:

interaction3.nim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import strutils, sequtils, math     # strutils提供了strip和splitLines,
                                    # sequtils提供了map,而math提供了sum。

let
  strNums = readFile("numbers.txt").strip().splitLines()  # 我们剥离最后一行新行,
                                                          # 并拆分行以创建字符串序列。
  nums = strNums.map(parseFloat)  # map的工作原理是对容器中的每个成员应用一个过程
                                  #(本例中为parseFloat)。
                                  # 换句话说,我们将每个字符串转换为浮点数,
                                  # 然后返回一个新的浮点数序列。

let
  sumNums = sum(nums)                  # 使用math模块中的sum计算序列中所有元素的总和。
  average = sumNums / float(nums.len)  # 我们需要将序列的长度转换为浮点数,因为sumNums是浮点数。

echo sumNums
echo average

输出:

1
2
226.15
45.23

练习

  1. 询问用户的身高和体重。计算他们的体重指数。向他们报告BMI值和类别。

  2. 重复科拉茨猜想练习,让你的程序询问用户一个起始数。打印结果序列。

  3. 要求用户提供一个想要反转的字符串。创建一个接收字符串并返回反转版本的过程。例如,如果用户输入Nim-lang,过程应返回gnal-miN。(提示:使用索引和countdown

结论

本教程到此结束,希望本教程对您有所帮助,让您成功迈出编程和Nim编程语言的第一步。

这些只是基础知识,我们只是触及了皮毛,但这些应该足以让你制作简单的程序,解决一些简单的任务或谜题。Nim还有更多的功能,希望你能继续探索它的可能性。

下一步工作

如果您想继续学习Nim教程:

如果您想解决一些编程难题:

  • 密码降临每年12月发布一系列有趣的谜题。提供旧谜题存档(自2015年起)。

  • 欧拉项目:主要是数学任务。

祝编码愉快!


本文由作者按照 CC BY 4.0 进行授权