Distinct类型

distinct 类型是从 基类型 派生的新类型与它的基类型不兼容。 特别是,它是一种不同类型的基本属性,它 并不 意味着它和基本类型之间的子类型关系。 允许从不同类型到其基本类型的显式类型转换,反之亦然。另请参阅 distinctBase 以获得逆操作。

如果基类型是序数类型,则不同类型是序数类型。

模拟货币

可以使用不同的类型来模拟不同的物理 单位 比如具有数字基类型。 以下示例模拟货币。

货币计算中不应混合不同的货币。 不同类型是模拟不同货币的完美工具:

  1. type
  2. Dollar = distinct int
  3. Euro = distinct int
  4.  
  5. var
  6. d: Dollar
  7. e: Euro
  8.  
  9. echo d + 12
  10. # 错误:无法添加没有单位的数字和 ``美元``

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.  
  4. proc `*` (x: int, y: Dollar): Dollar =
  5. result = Dollar(x * int(y))
  6.  
  7. 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类型的基类型的proc相同的实现,因此不会生成任何代码。

但似乎所有这些样板代码都需要为 欧元 货币重复。这可以通过 模板 解决。

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

借用编译指示还可用于注释不同类型以允许某些内置操作被提升:

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

目前只有点访问符可以用这种方式借用。

避免SQL注入攻击

从Nim传递到SQL数据库的SQL语句可能被模拟为字符串。 但是,使用字符串模板并填充值很容易受到 SQL注入攻击:

  1. import strutils
  2.  
  3. proc query(db: DbHandle, statement: string) = ...
  4.  
  5. var
  6. username: string
  7.  
  8. db.query("SELECT FROM users WHERE name = '$1'" % username)
  9. # 可怕的安全漏洞,但编译没有问题

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

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

它是抽象类型的基本属性,它们 并不 意味着抽象类型与其基类型之间的子类型关系。 允许从 stringSQL 的显式类型转换:

  1. import strutils, sequtils
  2.  
  3. proc properQuote(s: string): SQL =
  4. # 为SQL语句正确引用字符串
  5. return SQL(s)
  6.  
  7. proc `%` (frmt: SQL, values: openarray[string]): SQL =
  8. # 引用每个论点:
  9. let v = values.mapIt(SQL, properQuote(it))
  10. # we need a temporary type for the type conversion :-(
  11. type StrSeq = seq[string]
  12. # 调用 strutils.`%`:
  13. result = SQL(string(frmt) % StrSeq(v))
  14.  
  15. db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])

现在我们有针对SQL注入攻击的编译时检查。 因为 "".SQL 转换为 SQL("") 不需要新的语法来获得漂亮的 SQL 字符串字面值。 假设的 SQL 类型实际上存在于库中,作为 db_sqlite 等模块的 TSqlQuery类型