JSRUN 用代码说话

面向对象编程(OOP)

编辑教程

面向对象编程(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不产生虚拟方法表,但是生成调用树。这样为方法调用和使用内联避免了多余的间接分支。然而,其他的优化像:编译时间评估或者死代码消除对于方法是不起作用的。

JSRUN闪电教程系统是国内最先开创的教程维护系统, 所有工程师都可以参与共同维护的闪电教程,让知识的积累变得统一完整、自成体系。 大家可以一起参与进共编,让零散的知识点帮助更多的人。
X
支付宝
9.99
无法付款,请点击这里
金额: 0
备注:
转账时请填写正确的金额和备注信息,到账由人工处理,可能需要较长时间
如有疑问请联系QQ:565830900
正在生成二维码, 此过程可能需要15秒钟