强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Nim 完全指南 / 10 面向对象编程

第 10 章:面向对象编程

10.1 对象(Object)

Nim 的对象是值类型,存储在栈上(除非用 ref):

type
  Point = object
    x, y: float

  Person = object
    name: string
    age: int
    email: string

# 创建对象
var p1 = Point(x: 1.0, y: 2.0)
var p2 = Point(x: 3.0, y: 4.0)

echo p1.x, ", ", p1.y  # 1.0, 2.0

# 对象是值类型
var p3 = p1
p3.x = 10.0
echo p1.x  # 1.0(不受影响)

# 创建 Person
var person = Person(name: "张三", age: 28, email: "zhangsan@example.com")
echo person.name  # 张三

10.1.1 对象构造

type
  Config = object
    host: string
    port: int
    debug: bool

# 逐字段构造
var cfg1 = Config(host: "localhost", port: 8080, debug: true)

# 部分构造(其余字段为默认值)
var cfg2 = Config(host: "0.0.0.0", port: 3000)
echo cfg2.debug  # false

# 默认值
type
  ServerConfig = object
    host {.defaultValue: "localhost".}: string
    port {.defaultValue: 8080.}: int

# 使用 proc 作为构造函数
proc newConfig(host = "localhost", port = 8080, debug = false): Config =
  Config(host: host, port: port, debug: debug)

var cfg = newConfig(port = 3000, debug = true)
echo cfg

10.1.2 对象字段

type
  User = object
    id: int          # 私有字段(不导出)
    name*: string    # 公开字段(导出)
    age*: int

  PublicUser = object
    id*: int         # 公开
    name*: string
    age*: int

# 私有字段只能在同一模块中访问
var u = User(id: 1, name: "Alice", age: 30)
echo u.name  # ✅ 可访问
# echo u.id  # ❌ 其他模块无法访问

10.2 引用对象(Ref Object)

ref object 是引用类型,存储在堆上,适合大对象和共享数据:

type
  Node = ref object
    value: int
    next: Node

  TreeNode = ref object
    value: int
    left, right: TreeNode

# 创建引用对象
var root = TreeNode(
  value: 1,
  left: TreeNode(value: 2),
  right: TreeNode(value: 3)
)

# 引用语义——共享同一对象
var a = root
a.value = 100
echo root.value  # 100(同一对象)

# nil 检查
var n: Node = nil
echo n.isNil   # true

n = Node(value: 42, next: nil)
echo n.isNil   # false

10.3 继承

Nim 支持单一继承,使用 ref object of 语法:

type
  # 基类
  Shape = ref object of RootObj
    color: string

  # 子类
  Circle = ref object of Shape
    radius: float

  Rectangle = ref object of Shape
    width, height: float

  Square = ref object of Rectangle  # 多层继承

# 构造
proc newCircle(radius: float, color = "red"): Circle =
  Circle(radius: radius, color: color)

proc newRectangle(w, h: float, color = "blue"): Rectangle =
  Rectangle(width: w, height: h, color: color)

proc newSquare(side: float, color = "green"): Square =
  Square(width: side, height: side, color: color)

# 使用
let c = newCircle(5.0)
let r = newRectangle(4.0, 6.0)
let s = newSquare(3.0)

echo c.color     # red
echo r.width     # 4.0
echo s.width     # 3.0
echo s.height    # 3.0

10.4 方法与多态

10.4.1 方法(method)

method 支持动态分发(运行时多态):

type
  Animal = ref object of RootObj
    name: string
  
  Dog = ref object of Animal
  Cat = ref object of Animal
  Bird = ref object of Animal

# 基类方法(必须标注 {.base.})
method speak(a: Animal): string {.base.} =
  raise newException(CatchableError, "Not implemented")

method speak(d: Dog): string =
  "汪汪!"

method speak(c: Cat): string =
  "喵喵!"

method speak(b: Bird): string =
  "叽叽!"

# 多态调用
let animals: seq[Animal] = @[
  Dog(name: "旺财"),
  Cat(name: "咪咪"),
  Bird(name: "小黄"),
]

for a in animals:
  echo &"{a.name}: {a.speak()}"
# 旺财: 汪汪!
# 咪咪: 喵喵!
# 小黄: 叽叽!

10.4.2 类型转换

type
  Vehicle = ref object of RootObj
    speed: float
  
  Car = ref object of Vehicle
    doors: int
  
  Truck = ref object of Vehicle
    payload: float

proc newCar(speed: float, doors: int): Car =
  Car(speed: speed, doors: doors)

proc newTruck(speed, payload: float): Truck =
  Truck(speed: speed, payload: payload)

# 向上转换(隐式)
let v: Vehicle = newCar(120.0, 4)  # Car → Vehicle

# 向下转换(显式,运行时检查)
if v of Car:
  let car = Car(v)  # 或 v.Car
  echo &"Car with {car.doors} doors"

# of 检查
proc describe(v: Vehicle) =
  if v of Car:
    echo "Car: ", Car(v).doors, " doors"
  elif v of Truck:
    echo "Truck: payload ", Truck(v).payload
  else:
    echo "Unknown vehicle"

10.4.3 method vs proc

type
  Base = ref object of RootObj
  Derived = ref object of Base

method testMethod(b: Base) {.base.} =
  echo "Base method"

proc testProc(b: Base) =
  echo "Base proc"

method testMethod(d: Derived) =
  echo "Derived method"

proc testProc(d: Derived) =
  echo "Derived proc"

let d: Base = Derived()
d.testMethod()  # "Derived method"(动态分发)
d.testProc()    # "Base proc"(静态分发)

⚠️ 注意:优先使用 proc,只有需要运行时多态时才用 method

10.5 运算符重载

type Vec3 = object
  x, y, z: float

proc vec3(x, y, z: float): Vec3 =
  Vec3(x: x, y: y, z: z)

proc `+`(a, b: Vec3): Vec3 =
  vec3(a.x+b.x, a.y+b.y, a.z+b.z)

proc `-`(a, b: Vec3): Vec3 =
  vec3(a.x-b.x, a.y-b.y, a.z-b.z)

proc `*`(v: Vec3, s: float): Vec3 =
  vec3(v.x*s, v.y*s, v.z*s)

proc `==`(a, b: Vec3): bool =
  a.x == b.x and a.y == b.y and a.z == b.z

proc `$`(v: Vec3): string =
  &"({v.x}, {v.y}, {v.z})"

let a = vec3(1, 2, 3)
let b = vec3(4, 5, 6)
echo a + b       # (5.0, 7.0, 9.0)
echo a * 2.0     # (2.0, 4.0, 6.0)

10.6 封装模式

10.6.1 属性模式

type
  BankAccount = ref object
    balance: float
    owner: string

proc newBankAccount(owner: string, initialBalance = 0.0): BankAccount =
  BankAccount(balance: initialBalance, owner: owner)

# 通过 proc 封装访问
proc getBalance(account: BankAccount): float =
  account.balance

proc deposit(account: BankAccount, amount: float) =
  if amount <= 0:
    raise newException(ValueError, "Amount must be positive")
  account.balance += amount

proc withdraw(account: BankAccount, amount: float) =
  if amount > account.balance:
    raise newException(ValueError, "Insufficient funds")
  account.balance -= amount

proc `$`(account: BankAccount): string =
  &"{account.owner}: ${account.balance:.2f}"

var acc = newBankAccount("张三", 1000)
acc.deposit(500)
acc.withdraw(200)
echo acc  # 张三: $1300.00

10.6.2 工厂模式

type
  Connection = ref object of RootObj
    host: string
    port: int
  
  MySQLConn = ref object of Connection
    database: string
  
  PostgresConn = ref object of Connection
    schema: string

proc newConnection(dbType, host: string, port: int): Connection =
  case dbType
  of "mysql":
    MySQLConn(host: host, port: port, database: "default")
  of "postgres":
    PostgresConn(host: host, port: port, schema: "public")
  else:
    raise newException(ValueError, "Unknown DB type: " & dbType)

let conn = newConnection("mysql", "localhost", 3306)
echo conn of MySQLConn  # true

10.7 接口模拟

Nim 没有原生接口,但可以用 concept 或过程类型模拟:

# 使用 concept 模拟接口
type
  Drawable = concept d
    d.draw() is string
    d.area() is float

type
  Circle = object
    radius: float
  
  Rectangle = object
    width, height: float

proc draw(c: Circle): string =
  &"Drawing circle r={c.radius}"

proc area(c: Circle): float =
  PI * c.radius * c.radius

proc draw(r: Rectangle): string =
  &"Drawing rect {r.width}x{r.height}"

proc area(r: Rectangle): float =
  r.width * r.height

# 接受任何满足 Drawable concept 的类型
proc render[T: Drawable](shape: T) =
  echo shape.draw()
  echo &"Area: {shape.area():.2f}"

render(Circle(radius: 5.0))
render(Rectangle(width: 4.0, height: 6.0))

10.8 实战示例

🏢 场景:插件系统

type
  Plugin = ref object of RootObj
    name*: string
    version*: string
  
  LoggerPlugin = ref object of Plugin
    logFile: string
  
  CachePlugin = ref object of Plugin
    maxSize: int
    cache: seq[string]

method init(p: Plugin) {.base.} =
  echo &"Initializing plugin: {p.name}"

method init(lp: LoggerPlugin) =
  echo &"Logger initialized, file: {lp.logFile}"

method init(cp: CachePlugin) =
  echo &"Cache initialized, max: {cp.maxSize}"

method shutdown(p: Plugin) {.base.} =
  echo &"Shutting down: {p.name}"

type PluginManager = object
  plugins: seq[Plugin]

proc newPluginManager(): PluginManager =
  PluginManager(plugins: @[])

proc register(pm: var PluginManager, plugin: Plugin) =
  pm.plugins.add(plugin)

proc initAll(pm: PluginManager) =
  for p in pm.plugins:
    p.init()

proc shutdownAll(pm: PluginManager) =
  for p in pm.plugins:
    p.shutdown()

var pm = newPluginManager()
pm.register(LoggerPlugin(name: "Logger", version: "1.0", logFile: "app.log"))
pm.register(CachePlugin(name: "Cache", version: "2.0", maxSize: 1000))
pm.initAll()
pm.shutdownAll()

本章小结

概念语法说明
值对象type X = object栈分配,值语义
引用对象type X = ref object堆分配,引用语义
继承ref object of Base单一继承
方法method动态分发
过程proc静态分发
基类方法{.base.}标记基类方法
类型检查x of T运行时类型检查
类型转换T(x)x.T向下转换

练习

  1. 实现一个简单的图形类层次结构(Shape → Circle/Rectangle/Triangle)
  2. 实现一个带继承的事件系统
  3. 使用 ref object 实现一个链表
  4. 使用 concept 创建一个可序列化接口

扩展阅读


上一章:字符串处理 | 下一章:泛型编程