8.1 索引与迭代

在接着讲容器之前,我们先来说说索引和迭代。它们都属于比较基础的知识,起码对于容器来说是如此。

8.1.1 索引与可索引对象

你现在应该已经对索引这个词比较熟悉了。所谓的索引就是一种编号机制。这种编号机制可以对一个值中的某种整齐且独立的组成部分(或称索引单元)进行编号。像字符串中的字节以及元组中的元素值都属于索引单元。

在 Julia 中,索引单元的编号都是从1开始的,并以值中索引单元的总数作为最后一个编号。由于有着这样的编号风格,这种索引也被称为线性索引(linear indexing),其编号也被叫做线性索引号或简称为索引号。

我们之前也说过,索引表达式通常由一个可索引对象以及一个由中括号包裹的索引号组成。这里所说的可索引对象其实就是我刚刚讲的包含了索引单元的那种值。我们讲过的字符串、元组以及后面将要讲到的数组都是非常典型的可索引对象。字典也是可索引对象,但是它的索引机制并不是依据线性索引建立的。

从根本上讲,一个值是否属于可索引对象,完全依赖于是否存在针对其类型的索引方法。若我们在一个不属于可索引对象的值上应用索引表达式,就立即会引发一个错误。例如:

  1. julia> Set(1)[1]
  2. ERROR: MethodError: no method matching getindex(::Set{Int64}, ::Int64)
  3. # 省略了一些回显的内容。
  4. julia>

函数调用Set(1)会返回一个只包含了元素值1的集合,而集合并不属于可索引对象。所以,在我对其应用了索引表达式之后,Julia 就立即报错了。

在阅读了错误信息后我们会发现,报错的原因是没有针对Set{Int64}类型的getindex方法(即getindex函数的衍生方法)。而这个getindex方法恰恰就是那个最重要的索引方法。如果我们想让某个类型的值成为可索引对象就至少要实现对应的getindex方法。因此,有一种更好的方式可以判断某类值是否属于可索引对象,示例如下:

  1. julia> applicable(getindex, (1,), 1)
  2. true
  3. julia> applicable(getindex, Set(1), 1)
  4. false
  5. julia>

函数applicable可以接受若干个参数值。第一个参数值必须是某个函数(以下称目标函数)的名称,而后续的参数值(以下称目标参数值)则应该是需要依次传给目标函数的参数值。applicable函数的功能是,判断是否已经存在目标函数的某个衍生方法,这个衍生方法恰恰可以接受那些目标参数值。在这里,这些目标参数值具体是什么其实并不重要,重要的是它们的类型都是什么。因此我们也可以讲,applicable函数可以检查是否存在基于某个函数的、具有特定参数类型的衍生方法。

具体到上面的例子,第一行代码会判断有没有名称为getindex的衍生方法。具体的要求是,它的第一个参数的类型是Tuple,并且其第二个参数的类型是Int。由于判断的结果是true,所以元组一定属于可索引对象。类似的,第二行代码判断的是有没有针对Set类型的getindex方法。由结果可知,集合肯定不属于可索引对象。

幸好applicable函数返回的结果值不是true就是false。所以,我们可以很安全地进行判断,而不用担心像使用索引表达式那样引发错误。

除了getindex方法之外,如果是可变的可索引对象,那么通常还会实现setindex!方法。这个方法应该可以被用来改变容器中的与某个或某些索引号对应的元素值。

注意,虽然字符串和元组都属于可索引对象,但是却没有与String类型或Tuple类型对应的setindex!方法。因为这两个类型的值都是不可变的。所以说,判断一个值是否属于可索引对象还是要以是否存在对应的getindex方法为准。

8.1.2 迭代与可迭代对象

迭代(iteration)这个词我们在之前没有提到过。什么叫迭代呢?简单来说,迭代指的就是根据反馈重复地执行相同操作的过程。如此执行的目的往往是一步一步地逼近并达成某个既定的目标。由于迭代会重复地执行操作,所以它通常都会被放到一个循环当中。在这种情况下,每执行一次操作(即每循环一次)都可以说是一次迭代。除了最后一次迭代,每一次迭代的结束点都会成为下一次迭代的起始点。

在 Julia 中,我们可以使用for语句来实现循环。for语句是控制代码的执行流程的一种方式。它可以重复地执行语句中的代码,直到满足完全结束的条件为止。由于在后面会有专门的一章介绍 Julia 代码的流程控制,其中也有对for语句的阐述,所以我们当下只聚焦于怎样用for语句迭代容器从而取出其中的元素值。请看下面的示例:

  1. julia> tuple3 = (10, 20, 30, 40, 50);
  2. julia> for e in tuple3
  3. println(e)
  4. end
  5. 10
  6. 20
  7. 30
  8. 40
  9. 50
  10. julia>

我使用上面的这条for语句打印出了tuple3中的每一个元素值,并且每个元素值的展示都独占一行。更具体地讲,其中的每一次迭代都会打印出某一个元素值,并且打印的顺序完全依从于线性索引的顺序。也就是说,第一次迭代会打印出与索引号1对应的那个元素值,第二次迭代会打印出与索引号2对应的元素值,以此类推。直到打印出tuple3中的最后一个元素值,也就是与索引号5对应的元素值,这个循环才完全结束。

我们可以看到,这条for语句的代码占用了三行。第一行是以关键字for开头的,后面跟着迭代变量e、关键字in和被迭代的对象tuple3,它们之间都由空格分隔。与很多其他的代码块一样,for语句也是以独占一行的end作为结尾的。

所谓的迭代变量是一种局部变量。它的作用域是当前的for语句所代表的代码块。换句话讲,它在当前的for语句之外是不可见的。或者说,该语句之外的代码是无法引用到它的。如果被迭代的对象是一个容器,那么迭代变量在每一次迭代中都会被分别赋予该容器中的某一个元素值。对于元组来说,其中的元素值会被按照线性索引的顺序依次地赋给迭代变量。这也是上述示例能够打印出这般内容的根本原因。

可索引对象基本上都是可迭代对象。因为它们都有索引机制的加持,支持迭代很容易。除此之外,集合也是可迭代对象,虽然它并不是可索引对象。

我们如果要判断一个值是否属于可迭代对象,那么可以这样做:

  1. julia> applicable(iterate, (1,))
  2. true
  3. julia> applicable(iterate, Set(1))
  4. true
  5. julia>

其中的函数iterate对于可迭代对象来说非常的重要。倘若我们要让某个类型的值成为可迭代对象,那么实现与之对应的衍生方法是必不可少的。因此,iterate函数以及相应的衍生方法也就成为了辨别可迭代对象的黄金标准。

我们稍后就会讲到怎样对字典或集合做迭代。请接着往下看。