Distinct类型
distinct 类型是从 基类型 派生的新类型与它的基类型不兼容。 特别是,它是一种不同类型的基本属性,它 并不 意味着它和基本类型之间的子类型关系。 允许从不同类型到其基本类型的显式类型转换,反之亦然。另请参阅 distinctBase 以获得逆操作。
如果基类型是序数类型,则不同类型是序数类型。
模拟货币
可以使用不同的类型来模拟不同的物理 单位 比如具有数字基类型。 以下示例模拟货币。
货币计算中不应混合不同的货币。 不同类型是模拟不同货币的完美工具:
- type
- Dollar = distinct int
- Euro = distinct int
- var
- d: Dollar
- e: Euro
- echo d + 12
- # 错误:无法添加没有单位的数字和 ``美元``
d + 12.Dollar 也不允许,因为 + 为 int (在其它类型之中)定义, 而没有为 Dollar 定义。 因此需要定义美元的 + :
- proc `+` (x, y: Dollar): Dollar =
- result = Dollar(int(x) + int(y))
将一美元乘以一美元是没有意义的,但可以不带单位相乘;对除法同样成立:
- proc `*` (x: Dollar, y: int): Dollar =
- result = Dollar(int(x) * y)
- proc `*` (x: int, y: Dollar): Dollar =
- result = Dollar(x * int(y))
- proc `div` ...
这很快变得乏味。 实现是琐碎的,编译器不应该生成所有这些代码只是为了以后优化它 - 毕竟美元 + 应该生成与整型 + 相同的二进制代码。 编译指示 borrow 旨在解决这个问题;原则上它会生成以上的琐碎实现:
- proc `*` (x: Dollar, y: int): Dollar {.borrow.}
- proc `*` (x: int, y: Dollar): Dollar {.borrow.}
- proc `div` (x: Dollar, y: int): Dollar {.borrow.}
borrow 编译指示使编译器使用与处理distinct类型的基类型的proc相同的实现,因此不会生成任何代码。
但似乎所有这些样板代码都需要为 欧元 货币重复。这可以通过 模板 解决。
- template additive(typ: typedesc) =
- proc `+` *(x, y: typ): typ {.borrow.}
- proc `-` *(x, y: typ): typ {.borrow.}
- # 一元运算符:
- proc `+` *(x: typ): typ {.borrow.}
- proc `-` *(x: typ): typ {.borrow.}
- template multiplicative(typ, base: typedesc) =
- proc `*` *(x: typ, y: base): typ {.borrow.}
- proc `*` *(x: base, y: typ): typ {.borrow.}
- proc `div` *(x: typ, y: base): typ {.borrow.}
- proc `mod` *(x: typ, y: base): typ {.borrow.}
- template comparable(typ: typedesc) =
- proc `<` * (x, y: typ): bool {.borrow.}
- proc `<=` * (x, y: typ): bool {.borrow.}
- proc `==` * (x, y: typ): bool {.borrow.}
- template defineCurrency(typ, base: untyped) =
- type
- typ* = distinct base
- additive(typ)
- multiplicative(typ, base)
- comparable(typ)
- defineCurrency(Dollar, int)
- defineCurrency(Euro, int)
借用编译指示还可用于注释不同类型以允许某些内置操作被提升:
- type
- Foo = object
- a, b: int
- s: string
- Bar {.borrow: `.`.} = distinct Foo
- var bb: ref Bar
- new bb
- # 字段访问有效
- bb.a = 90
- bb.s = "abc"
目前只有点访问符可以用这种方式借用。
避免SQL注入攻击
从Nim传递到SQL数据库的SQL语句可能被模拟为字符串。 但是,使用字符串模板并填充值很容易受到 SQL注入攻击:
- import strutils
- proc query(db: DbHandle, statement: string) = ...
- var
- username: string
- db.query("SELECT FROM users WHERE name = '$1'" % username)
- # 可怕的安全漏洞,但编译没有问题
通过将包含SQL的字符串与不包含SQL的字符串区分开来可以避免这种情况。 不同类型提供了一种引入与 string 不兼容的新字符串类型 SQL 的方法:
- type
- SQL = distinct string
- proc query(db: DbHandle, statement: SQL) = ...
- var
- username: string
- db.query("SELECT FROM users WHERE name = '$1'" % username)
- # 静态错误:`query` 需要一个SQL字符串。
它是抽象类型的基本属性,它们 并不 意味着抽象类型与其基类型之间的子类型关系。 允许从 string 到 SQL 的显式类型转换:
- import strutils, sequtils
- proc properQuote(s: string): SQL =
- # 为SQL语句正确引用字符串
- return SQL(s)
- proc `%` (frmt: SQL, values: openarray[string]): SQL =
- # 引用每个论点:
- let v = values.mapIt(SQL, properQuote(it))
- # we need a temporary type for the type conversion :-(
- type StrSeq = seq[string]
- # 调用 strutils.`%`:
- result = SQL(string(frmt) % StrSeq(v))
- db.query("SELECT FROM users WHERE name = '$1'".SQL % [username])
现在我们有针对SQL注入攻击的编译时检查。 因为 "".SQL 转换为 SQL("") 不需要新的语法来获得漂亮的 SQL 字符串字面值。 假设的 SQL 类型实际上存在于库中,作为 db_sqlite 等模块的 TSqlQuery类型 。