原文链接:http://nim-lang.org/docs/manual.html#types
作者:Andreas Rumpf, Zahary Karadjov
版本:0.11.2
类型
所有的表达式都有一个在编译时已知的类型。nim是静态类型。你可以定义新类型,本质上是定义一个标识符,它可以用于表示自定义类型。
这些是主要的类型类:
序数类型(包括,整型,布尔型,字符型,枚举(它的子类型)类型)
浮点类型
字符串类型
结构类型
引用(指针)类型
程序类型
泛型
序数类型
序数类型有以下特点:
1 序数类型是可数的和有序的。这个特性允许函数操作,比如:在序数类型上定义了inc, ord, dec操作。
2 序数值有一个最小可能值。试图比最小值进一步减小将给出运行时检查或者静态错误。
3 序数值有一个最大可能值。试图比最大值进一步增加将给出运行时检查或者静态错误。
整型,布尔型以及枚举类型(和这些类型的子界类型)属于序数类型。为简单实施的原因,类型unit和uint64不是序数类型。
预先定义的整数类型
这些整数类型是预先定义的:
int
int是一般的有符号整数类型;它的大小依赖于平台并且与一个指针有相同的大小。这种类型可用于一般情况。一个没有类型后缀的整型数是这种类型。
intXX
额外的有符号整数类型的XX位使用此命名方案(例如:int16 是一个16比特的整数)。目前的实现支持int8,int16,int32,int64。这些类型的字面值有后缀'iXX。
uint
uint是一般的无符号整数类型;它的大小依赖于平台并且与一个指针有相同的大小。一个带有类型后缀'u的整型数是这种类型。
uintXX
额外的无符号整型的XX位使用此命名方案(例如:uint16 是一个16比特的无符号整型数)。目前的实现支持uint8,uint16,uint32,uint64。这些类型的字面值有后缀'uXX。所有关于无符号数的操作;它们不能导致上溢,或下溢错误。
对于有符号和无符号整型除了通用的算数操作符(+ - *等),也有作用在有符号整型数的操作符但将它们的参数作为无符号数:它们大多提供与缺少无符号整形类型的旧版本语言的向后兼容性。
对于有符号整型数的无符号操作使用%后缀作为公约:
operation meaning
a +% b unsigned integer addition 无符号加
a -% b unsigned integer subtraction 无符号减
a *% b unsigned integer multiplication 无符号乘
a /% b unsigned integer division 无符号除
a %% b unsigned integer modulo operation 无符号模操作
a <% b treat a and b as unsigned and compare 把a和b作为无符号比较
a <=% b treat a and b as unsigned and compare
ze(a) extends the bits of a with zeros until it has the width of the int type 使用0扩展a的位直到它具有int类型的大小
toU8(a) treats a as unsigned and converts it to an unsigned integer of 8 bits (but still the int8 type)
把a作为无符号数,并且将它转化为一个8位的无符号整型数(但它仍然是int8类型)
toU16(a) treats a as unsigned and converts it to an unsigned integer of 16 bits (but still the int16 type)
把a作为无符号数,并且将它转化为一个16位的无符号整型数(但它仍然是int16类型)
toU32(a) treats a as unsigned and converts it to an unsigned integer of 32 bits (but still the int32 type)
把a作为无符号数,并且将它转化为一个32位的无符号整型数(但它仍然是int32类型)
在表达式中使用不类型的整型会执行自动类型转换:较小的类型转化为较大的类型。
缩小的类型转换将一个较大的类型转换成一个较小的类型(例如:int32->int16).一个拓宽的类型转换将一个较小的类型转化为一个较大的类型(例如:int16->int32)。在nim中仅拓宽的类型转换是隐藏的:
var myInt16 = 5i16
var myInt: int
myInt16 + 34 # of type ``int16``
myInt16 + myInt # of type ``int``
myInt16 + 2i32 # of type ``int32``
然而,int字面值可隐式的转换为一个更小的整数类型如果字面值符合这种较小的类型,以及这样一个转换比其他隐式转换代价要小,所以myInt16 + 34产生一个int16结果。
对于更多的细节,看Convertible relation。
子类型
一个子类型是一个来自一个序数类型的值的范围(基本类型)。为了定义一个子类型,必须明确它的级限值,最低和最高类型值。
type
Subrange = range[0..5]
Subrange是一个整型的子类型仅仅可以保存0到5的值.给一个Subrange类型变量赋其他任何的值是一个检查运行时错误(或者静态错误如果它可以静态确定)。从基类型给它的一个子类型赋值是允许的(反之亦然)。
一个子类型与它的基类型有相同的尺寸(如例子中的int)。
nim需要子界类型的区间算法涵盖一组涉及常数的内置操作符:x %% 3是类型range[0..2].下列对于整型数的内置操作符都受这种规则的影响:-, +, *, min, max, succ, pred, mod, div, %%, and (bitwise and 按位与).
按位与只产生一个range如果它的一个操作数是一个常数x以至于(x+1)是两个数。(按位与然后一个%%操作.)
这意味着下面的代码是可以接受的:
case (x and 3) + 7
of 7: echo "A"
of 8: echo "B"
of 9: echo "C"
of 10: echo "D"
# note: no ``else`` required as (x and 3) + 7 has the type: range[7..10]
预定义的浮点类型
下面的浮点类型是预先定义的:
float
float是通用浮点类型;它的大小依赖于平台(编译器选择处理器最快的浮点类型)。一般情况下应使用这种类型。
floatXX
一个实现可以定义额外的XX位的浮点类型使用这个命名方案(例如:float64是一个64位的浮点数)。当前的实现支持float32和float64。这种类型的字面值带有'fXX后缀。
在表达式中采用不同类型的浮点类型会执行自动类型转换:详情请见Convertible relation。对浮点类型进行运算遵循IEEE标准。整数类型不能自动转换为浮点类型,反之亦然。
IEEE标准定义了五种浮点异常类型:
1 无效:数学无效操作数的运算,例如:0.0/0.0,sqrt(-1.0),以及log(-37.8)。
2 除0:除数是0并且被除数是一个有限的非零数,例如:1.0/0.0
3 上溢:操作产生一个超出数值大小范围的结果,例如:MAXDOUBLE+0.0000000000001e308。
4 下溢:操作产生一个太小而不能表示为一个正常数的结果,例如:MINDOUBLE * MINDOUBLE。
5 精度:操作产生一个结果不能表示无限的精度,例如:2.0 / 3.0, log(1.1)和输入0.1。
IEEE异常或者在运行时被忽视或者映射到nim异常:FloatInvalidOpError,FloatDivByZeroError,FloatOverflowError,FloatUnderflowError,FloatInexactError。从FloatingPointError基类继承这些异常。
nim提供编译指示NaNChecks和InfChecks用于控制是否IEEE异常被忽视或者陷入一个nim异常:
{.NanChecks: on, InfChecks: on.}
var a = 1.0
var b = 0.0
echo b / b # raises FloatInvalidOpError
echo a / b # raises FloatOverflowError
在当前的实现中FloatDivByZeroError和FloatInexactError永远不会抛出。抛出了FloatOverflowError而不是FloatDivByZeroError。这也有一个floatChecks编译指示,它是结合NaNChecks和InfChecks编译指示的一个简写。floatChecks默认关闭。
受floatChecks编译指示影响的操作仅仅是对于浮点类型的+, -, *, /操作符。
执行时应始终使用最大的精度来评估在编译时的浮点指针值;这意味着像这样的表达式0.09'f32 + 0.01'f32 == 0.09'f64 + 0.01'f64是真值。
布尔类型
布尔类型在nim中命名为bool,可以是两个预定义值true和false的其中之一。 while,if,elif,when语句中的条件需要是布尔类型。
这个条件成立:
ord(false) == 0 and ord(true) == 1
为bool类型定义了操作符not,and,or,xor,<, <=, >, >=, !=, ==。and和or操作符执行简化判断。例如:
while p != nil and p.name != "xyz":
# p.name is not evaluated if p == nil 如果p==nil,就不再对p.name进行判断,短路算法
p = p.next
布尔类型的大小是一个字节。
字符类型
字符类型在nim中命名为char。它的大小是一个字节。因此它不能表示一个UTF-8字符,但是它的一部分。这样做的原因是效率:在使用的绝大多数情况下,产生的程序仍将正确处理UTF-8,因为UTF-8是专门为这设计的。另一个原因是nim可以有效的支持array[char,int]或者set[char],许多算法依赖于这个特性。Rune类型使用Unicode字符,它可以代表任何Unicode字符。Rune在unicode模块中声明。
枚举类型
枚举类型定义一个新的类型,其值由指定的值组成。值是有序的。例如:
type
Direction = enum
north, east, south, west
下面的是正确的:
ord(north) == 0
ord(east) == 1
ord(south) == 2
ord(west) == 3
因此,north < east < south < west。比较运算符可以用于枚举类型。
为了更好的与其他编程语言的接口,枚举类型的域可以指定一个明确的序数值。然而,序数值必须是升序排列。一个域的序数值没有明确给出会被赋值为前一个域序数值+1。
一个明确的有序枚举可以不连续:
type
TokenType = enum
a = 2, b = 4, c = 89 # holes are valid
然而,它们不再是一个序数类型,因此不可能使用这些枚举作为一个数组的索引类型。过程inc,dec,succ和pred对于它们是不可用的。
对于枚举编译器支持内置的字符串解析操作(注:stringify()用于从一个对象解析出字符串)。stringify()解析的结果可以通过显示的给出字符串值使用控制:
type
MyEnum = enum
valueA = (0, "my value A"),
valueB = "value B",
valueC = 2,
valueD = (3, "abc")
从例子中可以看出,这是可能的通过一个元组同时指定一个域的序数值以及它的字符串值。也可以仅指定两者中的其中一个。
一个枚举可以标有pure编译指示,以至于它的域不被添加到当前的作用域中,所以它们总是需要通过MyEnum.value访问:
type
MyEnum {.pure.} = enum
valueA, valueB, valueC, valueD
echo valueA # error: Unknown identifier
echo MyEnum.valueA # works
字符串类型
所有字符串字面值都是string类型。在nim中一个字符串类似于一个字符序列。然而,在nim中字符串以0终结并且有一个长度域。你可以用内置的len过程取得长度;长度不计数终结符0。字符串的赋值操作复制整个字符串。&操作符链接字符串。字符串通过字典序比较。所有的比较操作符都是可用的。字符串可以像数组一样被索引(下界是0)。与数组不同的是,它们可以用在case语句中:
case paramStr(i)
of "-v": incl(options, optVerbose)
of "-h", "-?": incl(options, optHelp)
else: write(stdout, "invalid command line option!\n")
按照约定,所有的字符串都是UTF-8字符串,但是这不是强制的。例如,当从二进制文件读取字符串时,他们仅是一个字节序列。下标操作s[i]意味着s的第i个字符,不是第i个unichar。来自unicoe模块的runes迭代器可以用于迭代所有的Unicode字符。
cstring类型
cstring类型代表一个指针指向一个以0为终结符的字符数组,在Ansi C中兼容char*类型。其主要目的在于容易与C接口链接。下标操作s[i]意味着s的第i个字符;然而没有对cstring执行边界检查使得执行索引操作不安全。
为方便起见,一个nim字符串隐式的转化为cstring。如果一个nim字符串被传递给一个c风格的变长过程,它也被隐式的转化为cstring:
proc printf(formatstr: cstring) {.importc: "printf", varargs,
header: "<stdio.h>".}
printf("This works %s", "as expected")
即使转换是隐式的,但它是不安全的:垃圾收集器不考虑一个cstring是一个根,可能回收底层内存。然而在实践中这几乎不会发生因为GC谨慎的考虑栈根。你可以使用内置的过程GC_ref和GC_unref来保持字符串数据存活对于极少数在它不工作的情况下。
为cstrings定义的$过程返回一个字符串。因此可从一个cstring得到一个nim string:
var str: string = "Hello!"
var cstr: cstring = str
var newstr: string = $cstr
结构类型
一个结构化类型的变量可以同时容纳多个值。结构化类型可以嵌套到无限的级别。数组,序列,元组,对象和集合都属于结构类型。
数组和序列类型
数组是一个同性质的类型,意味着数组中的每一个元素有相同的类型。数组通常有一个固定的长度,它在编译时确定(除了开放数组)。它们可以通过任何序数类型索引。一个参数A可能是一个开放数组,在这种情况下它通过从0到len(A)-1的整数索引。可以通过数组构造器[]构造一个数组表达式。
序列类似于数组但是它有在运行时可能会改变(像字符串)的动态长度。序列被实现为动态增长的数组,分配内存片作为它的项目添加。一个序列S总是通过从0到len(S)-1的整数值索引,并且它有边界检查。可以通过数组构造器[]与数组到序列操作符@结合来构造序列。另一种为一个序列分配空间的方式是调用内置的newSeq过程。
一个序列可以被传递给一个open array类型的参数。
例子:
type
IntArray = array[0..5, int] # an array that is indexed with 0..5
IntSeq = seq[int] # a sequence of integers
var
x: IntArray
y: IntSeq
x = [1, 2, 3, 4, 5, 6] # [] is the array constructor
y = @[1, 2, 3, 4, 5, 6] # the @ turns the array into a sequence
一个数组或者序列的下界可以通过内置的low()过程取得,通过high()取得上界。长度可以通过len()取得。low()对于一个序列或者一个开放数组总是返回0,因为这是第一个有效的下标值。你可以使用add()过程或者&操作符实现给一个序列添加元素,使用pop()过程移除(以及得到)一个序列的最后一个元素。
符号x[i]可以用于得到x的第i个元素。
数组总是进行边界检查(在编译时或者运行时)。这些检查可以通过编译指示或者调用编译器的boundChecks:off命令行参数开关禁用。
开放数组
通常固定大小的数组被证明太不灵活;过程应该能够处理不同大小的数组。openarray类型允许这样;它仅能用于参数。Openarrays总是有一个开始位置为0的整型索引。len,low,high操作同样可用于open array。任何一个带有兼容基类型的数组都可以传递给一个openarray参数,索引类型并不重要。除此之外,数组序列也可以传递给一个open array参数。
openarray类型不能嵌套:多维openarrays是不支持的因为这很少需要并且不能有效运行。
可变参数(variable number of arguments)
一个数量可变的参数是一个openarray参数,此外,允许给一个过程传递个数可变的参数。编译器隐式的将参数列表转换为一个数组:
proc myWriteln(f: File, a: varargs[string]) =
for s in items(a):
write(f, s)
write(f, "\n")
myWriteln(stdout, "abc", "def", "xyz")
# is transformed to:
myWriteln(stdout, ["abc", "def", "xyz"])
如果可变参数在过程头中是最后一个参数这种转换才会发生。在这样的背景下也有可能发生类型转换:
proc myWriteln(f: File, a: varargs[string, `$`]) =
for s in items(a):
write(f, s)
write(f, "\n")
myWriteln(stdout, 123, "abc", 4.0)
# is transformed to:
myWriteln(stdout, [$123, $"def", $4.0])
注意一个显示的数组构造器传递给一个可变参数是不包含在另一个隐含的数组结构中:
proc takeV[T](a: varargs[T]) = discard
takeV([123, 2, 1]) # takeV's T is "int", not "array of int"
varargs[expr]被特别处理:它匹配一个任意类型的可变参数列表但是常常构造一个隐式的数组。这是必须的以便内置的echo过程所做的工作是预期的:
proc echo*(x: varargs[expr, `$`]) {...}
echo(@[1, 2, 3])
# prints "@[1, 2, 3]" and not "123"
元组和对象类型
一个元组或者一个对象类型的一个变量是一个异构的存储容器。一个元组或者对象定义了一个类型的各种命名域。一个元组也定义了一个域的顺序。元组意味着没有开销的异构存储类型以及一些抽象的可能性。构造器()可用于构造元组。构造器中的域的顺序必须匹配元组定义中域的顺序。不同的元组类型如果它们特定的相同类型的相同域有相同的顺序则它们是等价的。域的名字也必须相同。
对于元组的赋值操作复制每一个元素。对于对象的默认赋值操作复制每一个元素。为对象重载赋值运算符是不可能的,但是这将在未来的编译器版本中改变。
type
Person = tuple[name: string, age: int] # type representing a person:
# a person consists of a name
# and an age
var
person: Person
person = (name: "Peter", age: 30)
# the same, but less readable:
person = ("Peter", 30)
为了更好的访问性能实行字段对齐.对齐方式与c编译器的处理方式兼容.
为了与object对象声明的一致性,在一个type部分可以使用缩进而不是[]定义元组:
type
Person = tuple # type representing a person
name: string # a person consists of a name
age: natural # and an age
对象提供了很多元组没有的特性。对象提供了继承和信息隐藏。对象可以在运行时访问它们的类型,以便of操作符可用于确定对象的类型。
type
Person {.inheritable.} = object
name*: string # the * means that `name` is accessible from other modules *意味着从其他模块可以访问到`name`
age: int # no * means that the field is hidden 没有*意味着该域是隐藏的
Student = ref object of Person # a student is a person
id: int # with an id field
var
student: Student
person: Person
assert(student of Student) # is true
可以从外部定义的模块可见的对象域必须用*标记。与元组不同的是,不同的对象类型是绝不等价的。没有祖先的对象是隐藏的,所以没有隐藏的类型域。你可以使用inheritable编译指示引入除了system.RootObj之外新的根对象。
对象构造
对象也可以使用一个对象构造表达式来创建,对象构造表达式有这样的语法T(fieldA: valueA, fieldB: valueB, ...) ,T是一个object类型或者是一个ref object类型:
var student = Student(name: "Anton", age: 5, id: 3)
对于一个ref object类型隐式的调用 system.new 。
对象变形
通常在某些特定的情况下一个对象的层次结构是不必要的需要简单的变形类型.
一个例子:
# This is an example how an abstract syntax tree could be modelled in Nim
# 这是一个例子在Nim中怎样建模一个抽象语法树
type
NodeKind = enum # the different node types
nkInt, # a leaf with an integer value
nkFloat, # a leaf with a float value
nkString, # a leaf with a string value
nkAdd, # an addition
nkSub, # a subtraction
nkIf # an if statement
Node = ref NodeObj
NodeObj = object
case kind: NodeKind # the ``kind`` field is the discriminator
of nkInt: intVal: int
of nkFloat: floatVal: float
of nkString: strVal: string
of nkAdd, nkSub:
leftOp, rightOp: Node
of nkIf:
condition, thenPart, elsePart: Node
# create a new case object:
var n = Node(kind: nkIf, condition: nil)
# accessing n.thenPart is valid because the ``nkIf`` branch is active:
n.thenPart = Node(kind: nkFloat, floatVal: 2.0)
# the following statement raises an `FieldError` exception, because
# n.kind's value does not fit and the ``nkString`` branch is not active:
n.strVal = ""
# invalid: would change the active object branch:
n.kind = nkInt
var x = Node(kind: nkAdd, leftOp: Node(kind: nkInt, intVal: 4),
rightOp: Node(kind: nkInt, intVal: 2))
# valid: does not change the active object branch:
x.kind = nkSub
可以从例子中看到,一个对象结构层次的一个优点是不同的对象类型之间没有转换的需要。然而,访问无效的对象域会引发一个异常。
在对象声明中的case语法与case语句的语法联系紧密:在一个case部分的分支也可以缩进。
在例子中kind域叫做鉴别器:为了安全起见,不能取得它的地址并且给它赋值是受限制的:新的值一定不能导致活动对象分支的改变。对于一个对象分支,必须使用开关system.reset。
集合类型
集合类型是一个集合的数学概念模型。集合的基类型只能是一个序数类型。原因是集合是高性能的位向量的实现。
通过集合构造器构造集合:{}是空集合。空集合类型兼容于任何具体的集合类型。构造器也可以用于包含元素(以及元素范围):
type
CharSet = set[char]
var
x: CharSet
x = {'a'..'z', '0'..'9'} # This constructs a set that contains the
# letters from 'a' to 'z' and the digits
# from '0' to '9'
operation meaning
A + B union of two sets 两个集合的并集
A * B intersection of two sets 两个集合的交集
A - B difference of two sets (A without B's elements) 两个集合的差集(补集)
A == B set equality 集合相等
A <= B subset relation (A is subset of B or equal to B) 子集关系(A是B的子集或与B相等)
A < B strong subset relation (A is a real subset of B) 强子集关系(A是B的一个真正的子集)
e in A set membership (A contains element e) 集合元素(A包含元素e)
e notin A A does not contain element e A不包含元素e
contains(A, e) A contains element e A包含元素e
card(A) the cardinality of A (number of elements in A) 集合A的基数(A中的元素个数)
incl(A, elem) same as A = A + {elem} 向A中添加元素elem
excl(A, elem) same as A = A - {elem} A中去除元素elem
集合经常用于定义一个程序的flags类型。相比仅仅定义整数内容这是一个非常清晰(以及类型安全)的解决方法应该与or一起使用。
引用和指针类型
引用(类似于其他编程语言中的指针)是一种介绍多对一关系的方法。这意味着不同的引用可以指向或者修改内存中的相同位置(也称为混淆现象).
nim区分追踪引用和非追踪引用。非追踪引用也称作指针。追踪引用指向一个垃圾收集堆对象,非追踪引用指向手动分配的对象或者内存中其他地方的对象。因此非追踪引用是不安全的。然而对于特定的底层操作(访问硬件)非追踪引用是必不可少的。
追踪引用用ref关键字声明,非追踪引用用ptr关键字声明。
一个空下标[]符号可以用于解引用,addr过程返回一个项目的地址。一个地址通常是一个非追踪引用。因此使用addr是一个不安全的特征。
.(访问一个元组或对象域操作符)以及[](数组或字符串或序列索引操作符)操作符对于引用类型执行隐式的解引用操作。
type
Node = ref NodeObj
NodeObj = object
le, ri: Node
data: int
var
n: Node
new(n)
n.data = 9
# no need to write n[].data; in fact n[].data is highly discouraged!
对于程序调用的第一个参数也执行自动解引用。但是这个特征目前仅能通过{.experimental.}编译指示激活。
{.experimental.}
proc depth(x: NodeObj): int = ...
var
n: Node
new(n)
echo n.depth
# no need to write n[].depth either
为了简化结构类型检查,递归的元组是无效的:
# invalid recursion
type MyTuple = tuple[a: ref MyTuple]
同样的T = ref T也是一个无效的类型。
作为一个语法拓展,如果在一个类型部分通过ref object或者or object符号声明对象,对象类型可以是匿名的。这个特征是有用的如果一个对象只能获得引用语义:
type
Node = ref object
le, ri: Node
data: int
为了分配一个新的追踪对象,必须使用内置的new过程。为了处理非追踪内存,可以使用alloc,dealloc和realloc过程。系统模块的文档包含更多的信息
如果一个引用指向为空,它有空值nil。
要特别小心,如果一个非追踪对象包含追踪对象例如:追踪引用,字符串或者序列:为了适当的释放,在手动释放非追踪内存之前必须调用内置的GCunref过程:
type
Data = tuple[x, y: int, s: string]
# allocate memory for Data on the heap:
var d = cast[ptr Data](alloc0(sizeof(Data)))
# create a new string on the garbage collected heap:
d.s = "abc"
# tell the GC that the string is not needed anymore:
GCunref(d.s)
# free the memory:
dealloc(d)
没有调用GCunref过程,分配给d.s字符串的内存将永远不会被释放。这个例子还显示了底层编程的两个重要特性:sizeof过程以字节为单位返回一个类型或者一个值的大小。cast操作符可以规避系统类型:编译器强制处理alloc0的调用结果(它返回一个无类型指针)如果它想有ptr Data类型。cast在不可避免的情况下才会执行:它打破了类型安全和漏洞可能导致莫名崩溃。
注意:由于内存初始化为0这个例子才工作(alloc0完成了这个功能而不是alloc):因此d.s初始化为nil,则字符串赋值可以处理。你需要知道底层细节例如这何时将垃圾收集的数据与非托管内存混合。
Not nil注释
所有类型的nil都是一个有效值,可以通过not nil标记声明排除nil作为一个有效值:
type
PObject = ref TObj not nil
TProc = (proc (x, y: int)) not nil
proc p(x: PObject) =
echo "not nil"
# compiler catches this:
p(nil)
# and also this:
var x: PObject
p(x)
编译器确保每一个代码路径初始化变量包含not nil指针.这一分析的细节仍然是在这里指定.
内存区
ref类型和ptr类型可以得到一个可选择的内存区标注。一个区域必须是一个对象类型。
在OS内核开发中区域对于区别用户空间与内核内存非常有用。
type
Kernel = object
Userspace = object
var a: Kernel ptr Stat
var b: Userspace ptr Stat
# the following does not compile as the pointer types are incompatible:
a = b
如上例所示ptr也可以作为一个二目运算符,region ptr T是ptr[region, T]的一个简称。
为了使通用的代码更容易写,对于任何的R,ptr T是ptr[R, T]的一个子类型。
此外,区域对象类型的子类型关系提升到指针类型:如果 A <: B,因此ptr[A, T] <: ptr[B, T]。这可以用来塑造分区内存。作为一个特殊的类型规则ptr[R, T]是不兼容的指针为了阻止以下编译:
# from system
proc dealloc(p: pointer)
# wrap some scripting language
type
PythonsHeap = object
PyObjectHeader = object
rc: int
typ: pointer
PyObject = ptr[PythonsHeap, PyObjectHeader]
proc createPyObject(): PyObject {.importc: "...".}
proc destroyPyObject(x: PyObject) {.importc: "...".}
var foo = createPyObject()
# type error here, how convenient:
dealloc(foo)
未来的方向:
1 内存区域可能也可用于字符串和序列。
2 内置的区域例如,private, global和local将被证明对将到来的OpenCL目标非常有用。
3 内置的“regions”可以模仿lent和unique指针。
4 一个赋值操作符可以附属于一个区域以便产生适当的写屏障。这意味着GC完全可以在用户区域实现。
程序类型
程序类型是内部指向程序的指针。对于一个过程类型变量nil是一个允许的值。nim使用过程类型实现函数式编程技术。
例子:
proc printItem(x: int) = ...
proc forEach(c: proc (x: int) {.cdecl.}) =
...
forEach(printItem) # this will NOT compile because calling conventions differ
type
OnMouseMove = proc (x, y: int) {.closure.}
proc onMouseMove(mouseX, mouseY: int) =
# has default calling convention
echo "x: ", mouseX, " y: ", mouseY
proc setOnMouseMove(mouseMoveEvent: OnMouseMove) = discard
# ok, 'onMouseMove' has the default calling convention, which is compatible
# to 'closure':
setOnMouseMove(onMouseMove)
关于程序类型一个微妙的问题是,程序类型的调用约定影响类型的兼容性:如果程序类型有相同的调用约定它们才是兼容的。作为一个特殊的拓展,一个带有调用约定nimcall的程序可以传递给一个要求调用约定是closure的一个过程的参数.
nim支持这些调用约定:
nimcall
对于nim proc,nimcall是默认公约。它与fastcall相同,但是只有C编译器支持fastcall。
closure
是一个程序类型的默认调用公约,缺少任何编译注释。它表明程序有一个隐藏的隐式参数(一个环境)。有closure调用规则的程序变量占去两个机器词:一个是过程指针,另一个指向隐式的环境指针.
stdcall
stdcall公约是由微软指定的。生成的c程序用stdcall关键字声明。
cdecl
cdecl公约意味着一个程序应使用与C编译器相同的公约。在windows系统下,生成的c程序用cdecl关键字声明.
safecall
safecall公约是由微软指定的。生成的c程序用safecall关键字声明。安全这个词指的是所有的硬件寄存器将被推到硬件栈上。
inline
inline公约意味着调用者不应该调用程序,而是直接内联代码。注意,nim不内联,但是将这留给c编译器处理;它生成inline程序。这仅仅是给编译器的一个小提示:编译器完全可以忽视它并且它可能内联没有标记inline的程序。
fastcall
对于不同的c编译器fastcall意味着不同的事情。C __fastcall意味着什么得到什么。
syscall
syscall约定与c中的syscall相同。它用于中断。
noconv
生成的c代码将不会有任何显示的调用约定,因此使用c编译器的默认调用约定。这是需要的,因为为了提高速度nim默认的程序调用约定是fastcall。
大部分调用约定仅存在于windows32位平台上。
只有下列条件之一成立,才允许赋值或传递一个过程给一个过程变量:
1 访问驻留在当前模块中的程序.
2 标记有procvar编译指示的程序(查看procvar pragma)
3 有一个区别于nimcall调用约定的程序。
4 程序是匿名的。
该规则的目的是防止这样的情况:使用默认的参数拓展一个非procvar程序中断客户端代码。
默认的调用约定是nimcall,除非它是一个内部的过程(一个过程在一个过程里面)。对于一个内部的过程执行一个分析是否访问它的环境。如果它访问环境,它有调用约定closure,否则它有调用约定nimcall。
不同类型
distinct类型是从基本类型派生的新类型,它与它的基类型是不兼容的。特别的,这是不同类型的一个本质属性:并不意味着在不同类型与它的基类型之间是子类型关系。从一个不同类型到它的基类型显示的类型转换是允许的,反之亦然。
模拟货币
不同类型可用于模拟有一个基础值类型的不同物理单元,例如。下面的例子模拟货币。
不同货币在货币计算中不应该被混合。不同的类型是一个非常好的工具模拟不同的货币:
type
Dollar = distinct int
Euro = distinct int
var
d: Dollar
e: Euro
echo d + 12
# Error: cannot add a number with no unit and a ``Dollar`` 不能让一个没有单位的数与另一个有"Dollar"单位的数相加
不幸的是,d + 12.Dollar也是不允许的,因为 + 是为int定义的(包括其他类型),没有为Dollor定义。所以需要为dollars定义一个 +:
proc `+` (x, y: Dollar): Dollar =
result = Dollar(int(x) + int(y))
一美元与一美元相乘是没有意义的,除非有一个没有单位的数;对于除法也是一样:
proc `*` (x: Dollar, y: int): Dollar =
result = Dollar(int(x) * y)
proc `*` (x: int, y: Dollar): Dollar =
result = Dollar(x * int(y))
proc `div` ...
这很快就变得乏味。这样的实现是琐碎的,并且编译器不会生成所有的代码,仅仅做了后面的优化-毕竟对于dollars + 应该与ints + 生成相同的二进制代码。编译指示borrow已被设计来解决这个问题;原则上它生成上述琐碎的实现:
proc `*` (x: Dollar, y: int): Dollar {.borrow.}
proc `*` (x: int, y: Dollar): Dollar {.borrow.}
proc `div` (x: Dollar, y: int): Dollar {.borrow.}
borrow编译指示使编译器使用相同的实现过程处理基本类型的不同类型,所以没有代码生成。
但是,似乎这所有的样板代码都需要对Euro货币重置。这可以使用用模版解决:
template additive(typ: typedesc): stmt =
proc `+` *(x, y: typ): typ {.borrow.}
proc `-` *(x, y: typ): typ {.borrow.}
# unary operators:
proc `+` *(x: typ): typ {.borrow.}
proc `-` *(x: typ): typ {.borrow.}
template multiplicative(typ, base: typedesc): stmt =
proc `*` *(x: typ, y: base): typ {.borrow.}
proc `*` *(x: base, y: typ): typ {.borrow.}
proc `div` *(x: typ, y: base): typ {.borrow.}
proc `mod` *(x: typ, y: base): typ {.borrow.}
template comparable(typ: typedesc): stmt =
proc `<` * (x, y: typ): bool {.borrow.}
proc `<=` * (x, y: typ): bool {.borrow.}
proc `==` * (x, y: typ): bool {.borrow.}
template defineCurrency(typ, base: expr): stmt =
type
typ* = distinct base
additive(typ)
multiplicative(typ, base)
comparable(typ)
defineCurrency(Dollar, int)
defineCurrency(Euro, int)
type
Foo = object
a, b: int
s: string
Bar {.borrow: `.`.} = distinct Foo
var bb: ref Bar
new bb
# field access now valid
bb.a = 90
bb.s = "abc"
目前,这种方式中只有点访问符可以借用。
避免SQL注入攻击
一个SQL语句是从nim传递到一个SQL数据库的,可能建模为一个字符串。然而,使用字符串模版和填充值很容易受到著名的SQL注入攻击。
import strutils
proc query(db: DbHandle, statement: string) = ...
var
username: string
db.query("SELECT FROM users WHERE name = '$1'" % username)
# Horrible security hole, but the compiler does not mind!
这可以通过区分字符串中是否包含SQL来避免。不同的类型提供了一种方法引入了一个新的SQL字符串类型,它与字符串类型不兼容。
type
SQL = distinct string
proc query(db: DbHandle, statement: SQL) = ...
var
username: string
db.query("SELECT FROM users WHERE name = '$1'" % username)
# Error at compile time: `query` expects an SQL string!
这是抽象类型的本质属性,它不意味着在抽象类型与它的基本类型之间是一种子类型的关系。从字符串到SQL显示的类型转换是允许的:
import strutils, sequtils
proc properQuote(s: string): SQL =
# quotes a string properly for an SQL statement
return SQL(s)
proc `%` (frmt: SQL, values: openarray[string]): SQL =
# quote each argument:
let v = values.mapIt(SQL, properQuote(it))
# we need a temporary type for the type conversion :-(
type StrSeq = seq[string]
# call strutils.`%`:
result = SQL(string(frmt) % StrSeq(v))
db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])
现在我们有编译时检查来反抗SQL注入攻击。自从"".SQL转换为SQL(""),没有新的语法是好的SQL字符串需要的。假设SQL类型实际存在于库中,如db_sqlite模块中的TSqlQuery类型。
void类型
void类型表示没有任何类型(空类型)。void类型的参数都被视为是不存在的,void作为一个返回类型是指该程序没有返回值:
proc nothing(x, y: void): void =
echo "ha"
nothing() # writes "ha" to stdout
proc callProc[T](p: proc (x: T), x: T) =
when T is void:
p()
else:
p(x)
proc intProc(x: int) = discard
proc emptyProc() = discard
callProc[int](intProc, 12)
callProc[void](emptyProc)
然而,在泛型代码中无法推断出void类型:
callProc(emptyProc)
# Error: type mismatch: got (proc ())
# but expected one of:
# callProc(p: proc (T), x: T)
void类型仅对参数和返回类型有效,其他符号不能是void类型。