Distinct类型

distinct 类型是源于 base type “基类”的新类型,一个重要的特性是,它和其基类型之间 是父子类型关系。 但允许显式将 distinct 类型转换到基类型,反之亦然。请参阅 distinctBase 以获得反向操作的相关信息。

如果 distinct 类型的基类型是序数类型,则 distinct 类型也为序数类型。

模拟货币

distinct 类型可用于模拟不同的物理 units “单位”,例如,数字基本类型。以下为模拟货币的示例。

在货币计算中不应混用不同的货币。distinct 类型是一个模拟不同货币的理想工具:

  1. type
  2. Dollar = distinct int
  3. Euro = distinct int
  4. var
  5. d: Dollar
  6. e: Euro
  7. echo d + 12
  8. # 错误: 数字不可以与 `Dollar` 直接相加

可惜, 不允许 d + 12.Dollar ,因为 + 已被 int (以及其他)所定义。 所以用于 Dollar 的 + 需要进行这样的定义:

  1. proc `+` (x, y: Dollar): Dollar =
  2. result = Dollar(int(x) + int(y))

将一美元乘以一美元是没有意义的,但是可以乘以或除法一个无符号数:

  1. proc `*` (x: Dollar, y: int): Dollar =
  2. result = Dollar(int(x) * y)
  3. proc `*` (x: int, y: Dollar): Dollar =
  4. result = Dollar(x * int(y))
  5. proc `div` ...

这很快就会变得乏味。这些实现很细微而作用不明显,生成所有这些代码,而可能稍后又优化掉了 —— 美元的 + 应该产生与整数的 + 相同的二进制代码。编译指示 borrow “借用”旨在解决这个问题,理论上,能够简单实现上述所生成内容:

  1. proc `*` (x: Dollar, y: int): Dollar {.borrow.}
  2. proc `*` (x: int, y: Dollar): Dollar {.borrow.}
  3. proc `div` (x: Dollar, y: int): Dollar {.borrow.}

borrow 编译指示会让编译器使用,与处理distinct类型的基类型过程相同的实现,因此不会生成任何代码。

但是,Euro 货币似乎需要重复这些样式的代码,这个可以用模板来解决。

  1. template additive(typ: typedesc) =
  2. proc `+` * (x, y: typ): typ {.borrow.}
  3. proc `-` * (x, y: typ): typ {.borrow.}
  4. # 一元操作符:
  5. proc `+` * (x: typ): typ {.borrow.}
  6. proc `-` * (x: typ): typ {.borrow.}
  7. template multiplicative(typ, base: typedesc) =
  8. proc `*` * (x: typ, y: base): typ {.borrow.}
  9. proc `*` * (x: base, y: typ): typ {.borrow.}
  10. proc `div` * (x: typ, y: base): typ {.borrow.}
  11. proc `mod` * (x: typ, y: base): typ {.borrow.}
  12. template comparable(typ: typedesc) =
  13. proc `<` * (x, y: typ): bool {.borrow.}
  14. proc `<=` * (x, y: typ): bool {.borrow.}
  15. proc `==` * (x, y: typ): bool {.borrow.}
  16. template defineCurrency(typ, base: untyped) =
  17. type
  18. typ* = distinct base
  19. additive(typ)
  20. multiplicative(typ, base)
  21. comparable(typ)
  22. defineCurrency(Dollar, int)
  23. defineCurrency(Euro, int)

borrow 编译指示也可用于 distinct 类型注解,以提升某些内置操作:

  1. type
  2. Foo = object
  3. a, b: int
  4. s: string
  5. Bar {.borrow: `.`.} = distinct Foo
  6. var bb: ref Bar
  7. new bb
  8. # 字段访问有效
  9. bb.a = 90
  10. bb.s = "abc"

目前仅点访问器可以通过这个方式借用。

避免SQL注入攻击

从 Nim 传递到 SQL 数据库的 SQL 语句可能转化为字符串。 但是,使用字符串模板并填写值很容易受到著名的 SQL injection attack “SQL注入攻击” :

  1. import std/strutils
  2. proc query(db: DbHandle, statement: string) = ...
  3. var
  4. username: string
  5. db.query("SELECT FROM users WHERE name = '$1'" % username)
  6. # 糟糕的安全漏洞,但是编译器不关心

这可以通过区分包含 SQL 的字符串和不包含 SQL 的字符串来避免。distinct 类型提供了一种引入与 string 不兼容的新字符串类型 SQL 的方法:

  1. type
  2. SQL = distinct string
  3. proc query(db: DbHandle, statement: SQL) = ...
  4. var
  5. username: string
  6. db.query("SELECT FROM users WHERE name = '$1'" % username)
  7. # 静态错误: `query` 期望一个 SQL 字符串

抽象类型一个重要的属性是,抽象类型与它们的子类型之间没有父子关系。允许显示将 string 类型转换到 SQL :

  1. import std/[strutils, sequtils]
  2. proc properQuote(s: string): SQL =
  3. # 正确地为 SQL 语句引用字符串
  4. return SQL(s)
  5. proc `%` (frmt: SQL, values: openarray[string]): SQL =
  6. # 引用每个参数:
  7. let v = values.mapIt(properQuote(it))
  8. # 需要一个临时类型用到类型转换 :-(
  9. type StrSeq = seq[string]
  10. # 调用 strutils.`%`:
  11. result = SQL(string(frmt) % StrSeq(v))
  12. db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])

现在我们有了针对 SQL 注入攻击的编译期检查。 由于 “”.SQL 被转换为 SQL(“”) ,所以不需要新的语法来实现简洁的 SQL 字符串字面值。 假设 SQL 类型与 db_sqlite 等类似,已经作为 SqlQuery type 实际存在与库中。