过程

大多数编程语言称之为 methods 或 functions 在Nim中称为 过程 。 过程声明由标识符,零个或多个形式参数,返回值类型和代码块组成。

正式参数声明为由逗号或分号分隔的标识符列表。 形参由 : 类型名称 给出一个类型。

该类型适用于紧接其之前的所有参数,直到达到参数列表的开头,分号分隔符或已经键入的参数。

分号可用于使类型和后续标识符的分隔更加清晰。

  1. # 只使用逗号
  2. proc foo(a, b: int, c, d: bool): int
  3.  
  4. # 使用分号进行视觉区分
  5. proc foo(a, b: int; c, d: bool): int
  6.  
  7. # 会失败:a是无类型的,因为 ';' 停止类型传播。
  8. proc foo(a; b: int; c, d: bool): int

可以使用默认值声明参数,如果调用者没有为参数提供值,则使用该默认值。

  1. # b is optional with 47 as its default value
  2. proc foo(a: int, b: int = 47): int

参数可以声明为可变的,因此允许proc通过使用类型修饰符 var 来修改这些参数。

  1. # 通过第二个参数 ``返回`` 一个值给调用者
  2. # 请注意,该函数根本不使用实际返回值(即void)
  3. proc foo(inp: int, outp: var int) =
  4. outp = inp + 47

如果proc声明没有正文,则它是一个 前向 声明。 如果proc返回一个值,那么过程体可以访问一个名为 result 的隐式声明的变量。

过程可能会重载。 重载解析算法确定哪个proc是参数的最佳匹配。

示例:

  1. proc toLower(c: char): char = # toLower for characters
  2. if c in {'A'..'Z'}:
  3. result = chr(ord(c) + (ord('a') - ord('A')))
  4. else:
  5. result = c
  6.  
  7. proc toLower(s: string): string = # 字符串toLower
  8. result = newString(len(s))
  9. for i in 0..len(s) - 1:
  10. result[i] = toLower(s[i]) # calls toLower for characters; no recursion!

调用过程可以通过多种方式完成:

  1. proc callme(x, y: int, s: string = "", c: char, b: bool = false) = ...
  2.  
  3. # call with positional arguments # parameter bindings:
  4. callme(0, 1, "abc", '\t', true) # (x=0, y=1, s="abc", c='\t', b=true)
  5. # call with named and positional arguments:
  6. callme(y=1, x=0, "abd", '\t') # (x=0, y=1, s="abd", c='\t', b=false)
  7. # call with named arguments (order is not relevant):
  8. callme(c='\t', y=1, x=0) # (x=0, y=1, s="", c='\t', b=false)
  9. # call as a command statement: no () needed:
  10. callme 0, 1, "abc", '\t' # (x=0, y=1, s="abc", c='\t', b=false)

过程可以递归地调用自身。

运算符 是具有特殊运算符符号作为标识符的过程:

  1. proc `$` (x: int): string =
  2. # 将整数转换为字符串;这是一个前缀运算符。
  3. result = intToStr(x)

具有一个参数的运算符是前缀运算符,具有两个参数的运算符是中缀运算符。 (但是,解析器将这些与运算符在表达式中的位置区分开来。) 没有办法声明后缀运算符:所有后缀运算符都是内置的,并由语法显式处理。

任何运算符都可以像普通的proc一样用 'opr' 表示法调用。(因此运算符可以有两个以上的参数):

  1. proc `*+` (a, b, c: int): int =
  2. # Multiply and add
  3. result = a * b + c
  4.  
  5. assert `*+`(3, 4, 6) == `+`(`*`(a, b), c)

导出标记

如果声明的符号标有 asterisk 它从当前模块导出:

  1. proc exportedEcho*(s: string) = echo s
  2. proc `*`*(a: string; b: int): string =
  3. result = newStringOfCap(a.len * b)
  4. for i in 1..b: result.add a
  5.  
  6. var exportedVar*: int
  7. const exportedConst* = 78
  8. type
  9. ExportedType* = object
  10. exportedField*: int

方法调用语法

对于面向对象的编程,可以使用语法 obj.method(args) 而不是 method(obj, args)

如果没有剩余的参数,则可以省略括号: obj.len (而不是 len(obj) )。

此方法调用语法不限于对象,它可用于为过程提供任何类型的第一个参数:

  1. echo "abc".len # 与echo len"abc"相同
  2. echo "abc".toUpper()
  3. echo {'a', 'b', 'c'}.card
  4. stdout.writeLine("Hallo") # 与相同writeLine(stdout,"Hallo")

查看方法调用语法的另一种方法是它提供了缺少的后缀表示法。

方法调用语法与显式泛型实例化冲突: pT 不能写为 x.p[T] 因为 x.p[T] 总是被解析为 (x.p)[T]

见: Limitations of the method call syntax

[:] 符号旨在缓解这个问题: xp[:T] 由解析器重写为 pTxp:T 被重写为 pT 。 注意 [:] 没有AST表示,重写直接在解析步骤中执行。

属性

Nim不需要 get-properties :使用 方法调用语法 调用的普通get-procedure达到相同目的。 但设定值是不同的;为此需要一个特殊的setter语法:

  1. # 模块asocket
  2. type
  3. Socket* = ref object of RootObj
  4. host: int # 无法从模块外部访问
  5.  
  6. proc `host=`*(s: var Socket, value: int) {.inline.} =
  7. ## hostAddr的setter.
  8. ##它访问'host'字段并且不是对 ``host =`` 的递归调用,如果内置的点访问方法可用,则首选点访问:
  9. s.host = value
  10.  
  11. proc host*(s: Socket): int {.inline.} =
  12. ## hostAddr的getter
  13. ##它访问'host'字段并且不是对 ``host`` 的递归调用,如果内置的点访问方法可用,则首选点访问:
  14. s.host
  1. # 模块 B
  2. import asocket
  3. var s: Socket
  4. new s
  5. s.host = 34 # 同`host=`(s, 34)

定义为 f= 的proc(尾随 = )被称为 setter 。

可以通过常见的反引号表示法显式调用setter:

  1. proc `f=`(x: MyObject; value: string) =
  2. discard
  3.  
  4. `f=`(myObject, "value")

f= 可以在模式 xf = value 中隐式调用,当且仅当 x 的类型没有名为 f 的字段或者 f 时在当前模块中不可见。 这些规则确保对象字段和访问者可以具有相同的名称。 在模块 x.f 中总是被解释为字段访问,在模块外部它被解释为访问器proc调用。

命令调用语法

如果调用在语法上是一个语句,则可以在没有 () 的情况下调用例程。 这种限制意味着 echo f 1, f 2 被解析为 echo(f(1), f(2)) 而不是 echo(f(1, f(2)))

在这种情况下,方法调用语法可用于提供一个或多个参数:

  1. proc optarg(x: int, y: int = 0): int = x + y
  2. proc singlearg(x: int): int = 20*x
  3.  
  4. echo optarg 1, " ", singlearg 2 # 打印 "1 40"
  5.  
  6. let fail = optarg 1, optarg 8 # 错误。命令调用的参数太多
  7. let x = optarg(1, optarg 8) # 传统过程调用2个参数
  8. let y = 1.optarg optarg 8 # 与上面相同,没有括号
  9. assert x == y

命令调用语法也不能将复杂表达式作为参数。 例如: (匿名过程), if, casetry 。 没有参数的函数调用仍需要()来区分调用和函数本身作为第一类值。

闭包

过程可以出现在模块的顶层以及其他范围内,在这种情况下,它们称为嵌套过程。 嵌套的proc可以从其封闭的范围访问局部变量,如果它这样做,它就变成了一个闭包。 任何捕获的变量都存储在闭包(它的环境)的隐藏附加参数中,并且它们通过闭包及其封闭范围的引用来访问(即,对它们进行的任何修改在两个地方都是可见的)。

如果编译器确定这是安全的,则可以在堆上或堆栈上分配闭包环境。

在循环中创建闭包

由于闭包通过引用捕获局部变量,因此在循环体内通常不需要行为。 有关如何更改此行为的详细信息,请参阅 closureScope

匿名过程

Procs也可以被视为表达式,在这种情况下,它允许省略proc的名称。

  1. var cities = @["Frankfurt", "Tokyo", "New York", "Kyiv"]
  2.  
  3. cities.sort(proc (x,y: string): int =
  4. cmp(x.len, y.len))

Procs as表达式既可以作为嵌套proc,也可以作为顶级可执行代码。

函数

The func 关键字为 noSideEffect 的过程引入了一个快捷方式。

  1. func binarySearch[T](a: openArray[T]; elem: T): int

是它的简写:

  1. proc binarySearch[T](a: openArray[T]; elem: T): int {.noSideEffect.}

不可重载的内置

由于实现简单,它们不能重载以下内置过程(它们需要专门的语义检查):

declared, defined, definedInScope, compiles, sizeOf, is, shallowCopy, getAst, astToStr, spawn, procCall

因此,它们更像关键词而非普通标识符;然而,与关键字不同,重新定义可能是 shadowsystem 模块中的定义。 从这个列表中不应该用点符号 x.f 写,因为 x 在传递给 f 之前不能进行类型检查:

declared, defined, definedInScope, compiles, getAst, astToStr

Var形参

参数的类型可以使用 var 关键字作为前缀:

  1. proc divmod(a, b: int; res, remainder: var int) =
  2. res = a div b
  3. remainder = a mod b
  4.  
  5. var
  6. x, y: int
  7.  
  8. divmod(8, 5, x, y) # modifies x and y
  9. assert x == 1
  10. assert y == 3

在示例中, resremaindervar parameters 。 可以通过过程修改Var参数,并且调用者可以看到更改。 传递给var参数的参数必须是左值。 Var参数实现为隐藏指针。 上面的例子相当于:

  1. proc divmod(a, b: int; res, remainder: ptr int) =
  2. res[] = a div b
  3. remainder[] = a mod b
  4.  
  5. var
  6. x, y: int
  7. divmod(8, 5, addr(x), addr(y))
  8. assert x == 1
  9. assert y == 3

在示例中,var参数或指针用于提供两个返回值。 这可以通过返回元组以更干净的方式完成:

  1. proc divmod(a, b: int): tuple[res, remainder: int] =
  2. (a div b, a mod b)
  3.  
  4. var t = divmod(8, 5)
  5.  
  6. assert t.res == 1
  7. assert t.remainder == 3

可以使用 元组解包 来访问元组的字段:

  1. var (x, y) = divmod(8, 5) # 元组解包
  2. assert x == 1
  3. assert y == 3

注意: var 参数对于有效的参数传递永远不是必需的。 由于无法修改非var参数,因此如果编译器认为可以加快执行速度,则编译器始终可以通过引用自由传递参数。

Var返回类型

proc,转换器或迭代器可能返回一个 var 类型,这意味着返回的值是一个左值,并且可以由调用者修改:

  1. var g = 0
  2.  
  3. proc writeAccessToG(): var int =
  4. result = g
  5.  
  6. writeAccessToG() = 6
  7. assert g == 6

如果隐式引入的指针可用于访问超出其生命周期的位置,则这是一个静态错误:

  1. proc writeAccessToG(): var int =
  2. var g = 0
  3. result = g # Error!

For iterators, a component of a tuple return type can have a var type too:

  1. iterator mpairs(a: var seq[string]): tuple[key: int, val: var string] =
  2. for i in 0..a.high:
  3. yield (i, a[i])

在标准库中,返回 var 类型的例程的每个名称都以每个约定的前缀 m 开头。 Memory safety for returning by var T is ensured by a simple borrowing rule: If result does not refer to a location pointing to the heap (that is in result = X the X involves a ptr or ref access) then it has to be deviated by the routine's first parameter:

  1. proc forward[T](x: var T): var T =
  2. result = x # ok, deviated from the first parameter.
  3.  
  4. proc p(param: var int): var int =
  5. var x: int
  6. # we know 'forward' provides a view into the location deviated by
  7. # its first argument 'x'.
  8. result = forward(x) # Error: location is derived from ``x``
  9. # which is not p's first parameter and lives
  10. # on the stack.

In other words, the lifetime of what result points to is attached to the lifetime of the first parameter and that is enough knowledge to verify memory safety at the callsite.

未来的方向

Nim的更高版本可以使用如下语法更准确地了解借用规则:

  1. proc foo(other: Y; container: var X): var T from container

这里 var T from container 明确地暴露了该位置不同于第二个形参(在本例中称为'container')。 语法 var T from p 指定一个类型 varTy [T,2] ,它与 varTy [T,1] 不兼容。

下标操作符重载

数组/开放数组/序列的 [] 下标运算符可以重载。