面向对象编程(OOP)
nim支持面向对象编程(OOP)是极保守行动,可以使用功能强大的面向对象技术。面向对象的程序设计是设计一个程序的一种方式,并不是唯一的方法。通常一个程序的方法将产生更简单和高效的代码。特别地,相比继承,组合往往是更好的设计。
对象
就像元组,对象是一种手段以一种结构化的方式将不同的值包装在一起。对象提供了很多元组没有的功能。对象提供继承和信息隐藏。由于对象封装数据,T()对象构造器应该只用于内部,程序应该提供一个过程用于初始化对象(这被叫做构造器)。
对象在运行时访问他们的类型。of操作符,可以用来检查对象的类型:
type
Person = ref object of RootObj
name*: string # *意味着'name'从其他的模块使可以访问到的
age: int # 没有*意味着对于其他模块该域是隐藏的
Student = ref object of Person # Student从Person继承,有一个id域
id: int # with an id field
var
student: Student
person: Person
assert(student of Student) # is true
# object construction: 对象构造
student = Student(name: "Anton", age: 5, id: 2)
echo student[]
从外部特定的模块可以访问到的对象域必须用*标记。相比之下,元组的不同的对象类型从来是不等价的。新的对象类型只能在type部分定义。
继承是处理对象的语法。现在还不支持多继承。如果一个对象类型没有合适的祖先,RootObj可以作为它的祖先,但这只是一个约定。没有祖先的对象是隐藏的final。你可以用inheritable编译指示来产生一个除了来自system.RotObj之外的的根对象。(例如:这被用在GTK包)。
每当使用继承时应使用ref对象。它不是绝对必要的,但是用non-ref对象赋值,如:let person: Person = Student(id: 123)将截断子类域。
注意:组合(has-a 关系)往往优于继承(is-a 关系)为了简单的代码重用。由于在nim中对象是一种值类型,组合和继承一样有效。注:(引用类型(重量级对象)和值类型(轻量级对象))
相互递归类型
对象,元组和引用可以塑造相当复杂的数据结构相互依赖彼此;它们是相互递归。在nim中这些类型只能在一个单一的类型部分声明。(其他任何需要任意前端符号会减慢编辑。)
Example:
type
Node = ref NodeObj # a traced reference to a NodeObj
NodeObj = object
le, ri: Node # left and right subtrees
sym: ref Sym # leaves contain a reference to a Sym
Sym = object # a symbol
name: string # the symbol's name
line: int # 符号声明的行
code: PNode # 符号的抽象语法树
类型转换
nim区分显示的类型转换和隐式的类型。显示的类型转换用casts操作符并且强制编译器解释一种位模式成为另一种类型。
隐式的类型转换是一个更礼貌的方式将一个类型型转换为另一个:他们保存摘要值,不一定是位模式。如果一个类型转换是不可能的,编译器控诉或者抛出一个异常。
类型转换语法是:destination_type(expression_to_convert)目的类型(要转换的表达式)(像一个普通的调用)
proc getID(x: Person): int =
Student(x).id
如果x不是一个Student类型,会抛出InvalidObjectConversionError异常。
对象变形
通常一个对象层次结构在特定的情况下是不必要的,需要简单的变体类型。
一个例子:
# 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 “kind”域是鉴别器
of nkInt: intVal: int
of nkFloat: floatVal: float
of nkString: strVal: string
of nkAdd, nkSub:
leftOp, rightOp: PNode
of nkIf:
condition, thenPart, elsePart: PNode
var n = PNode(kind: nkFloat, floatVal: 1.0)
# the following statement raises an `FieldError` exception, because
# n.kind's value does not fit: 下面的语句引发一个"FieldError"异常,因为n.kind's的值不匹配:
n.strVal = ""
可以从这个例子中看到,一个对象层次结构的一个优点是,不需要不同的对象类型之间的转换。然而,访问无效的对象域会引发一个异常。
方法
在普遍的面向对象程序设计语言中,过程(也叫做方法)被绑定到一个类。这种做法有缺点:
- 程序员无法控制添加一个方法到一个类中是不可能的或者需要丑陋的解决方法。
- 很多情况下方法应该属于哪里是不清楚的:是加入一个字符串方法还是一个数组方法?
- nim通过不分配方法到一个类中避免了这样的问题。所有的方法在nim中都是多方法。后面我们将看到,多方法区别与过程只为了动态绑定目的。
方法调用语法
对于调用例程有一个语法糖:可以用语法obj.method(args)而不是method(obj,args).如果没有剩余的参数,圆括号可以省略:obj.len(而不是len(obj))。
这个方法调用语法是不受对象限制的,它可以被用于任何类型。
echo("abc".len) # is the same as echo(len("abc")) 类似于echo(len("abc"))
echo("abc".toUpper())
echo({'a', 'b', 'c'}.card)
stdout.writeln("Hallo") # the same as writeln(stdout, "Hallo") 类似于writeln(stdout, "Hallo")
(另一种方式来看待方法调用语法是它提供了缺失的后缀表示法.)
所以纯面向对象代码是容易写的:
import strutils
stdout.writeln("Give a list of numbers (separated by spaces): ")
stdout.write(stdin.readLine.split.map(parseInt).max.`$`)
stdout.writeln(" is the maximum!")
特性
如上面的例子所示,nim没必要get-properities:通常get-procedures被称为方法调用语法实现相同的功能。但是设定的值是不一样的;对于这需要一个特殊的setter语法:
type
Socket* = ref object of RootObj
FHost: int # cannot be accessed from the outside of the module
# the `F` prefix is a convention to avoid clashes since
# the accessors are named `host`
proc `host=`*(s: var Socket, value: int) {.inline.} =
## setter of hostAddr
s.FHost = value
proc host*(s: Socket): int {.inline.} =
## getter of hostAddr
s.FHost
var s: Socket
new s
s.host = 34 # same as `host=`(s, 34)
(这个程序也展示了inline程序)
[]数组访问运算符可以重载以提供数组属性:
type
Vector* = object
x, y, z: float
proc `[]=`* (v: var Vector, i: int, value: float) =
# setter
case i
of 0: v.x = value
of 1: v.y = value
of 2: v.z = value
else: assert(false)
proc `[]`* (v: Vector, i: int): float =
# getter
case i
of 0: result = v.x
of 1: result = v.y
of 2: result = v.z
else: assert(false)
这个例子是愚蠢的,因为一个vector通过一个元组可以更好的模拟,元组已经提供v[]访问。
动态调度
程序总是使用静态调度。对于动态调度使用method代替proc关键词:
type
PExpr = ref object of RootObj ## abstract base class for an expression 一个表达式的抽象基类
PLiteral = ref object of PExpr
x: int
PPlusExpr = ref object of PExpr
a, b: PExpr
# watch out: 'eval' relies on dynamic binding 当心:‘eval’依赖于动态绑定
method eval(e: PExpr): int =
# override this base method 重写这个基础的方法
quit "to override!"
method eval(e: PLiteral): int = e.x
method eval(e: PPlusExpr): int = eval(e.a) + eval(e.b)
proc newLit(x: int): PLiteral = PLiteral(x: x)
proc newPlus(a, b: PExpr): PPlusExpr = PPlusExpr(a: a, b: b)
echo eval(newPlus(newPlus(newLit(1), newLit(2)), newLit(4)))
注意:在例子中,构造器newLit和newPlus是过程,因为对于它们使用静态绑定更有意义,但是eval是一个方法因为它需要动态绑定。
在一个多方法的所有参数中有一个对象类型用于调度:
type
Thing = ref object of RootObj
Unit = ref object of Thing
x: int
method collide(a, b: Thing) {.inline.} =
quit "to override!"
method collide(a: Thing, b: Unit) {.inline.} =
echo "1"
method collide(a: Unit, b: Thing) {.inline.} =
echo "2"
var a, b: Unit
new a
new b
collide(a, b) # output: 2
如上面那个例子所示,调用一个多方法不能是模棱两可的。相比collide 1,collide 2是首选,因为决议是从左到右工作的。因此,Unit, Thing优于Thing, Unit。
注意:nim不产生虚拟方法表,但是生成调用树。这样为方法调用和使用内联避免了多余的间接分支。然而,其他的优化像:编译时间评估或者死代码消除对于方法是不起作用的。