Distinct类型
distinct 类型是源于 base type “基类”的新类型,一个重要的特性是,它和其基类型之间 不 是父子类型关系。 但允许显式将 distinct 类型转换到基类型,反之亦然。请参阅 distinctBase 以获得反向操作的相关信息。
如果 distinct 类型的基类型是序数类型,则 distinct 类型也为序数类型。
模拟货币
distinct 类型可用于模拟不同的物理 units “单位”,例如,数字基本类型。以下为模拟货币的示例。
在货币计算中不应混用不同的货币。distinct 类型是一个模拟不同货币的理想工具:
type
Dollar = distinct int
Euro = distinct int
var
d: Dollar
e: Euro
echo d + 12
# 错误: 数字不可以与 `Dollar` 直接相加
可惜, 不允许 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类型的基类型过程相同的实现,因此不会生成任何代码。
但是,Euro 货币似乎需要重复这些样式的代码,这个可以用模板来解决。
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)
borrow 编译指示也可用于 distinct 类型注解,以提升某些内置操作:
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 injection attack “SQL注入攻击” :
import std/strutils
proc query(db: DbHandle, statement: string) = ...
var
username: string
db.query("SELECT FROM users WHERE name = '$1'" % username)
# 糟糕的安全漏洞,但是编译器不关心
这可以通过区分包含 SQL 的字符串和不包含 SQL 的字符串来避免。distinct 类型提供了一种引入与 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 std/[strutils, sequtils]
proc properQuote(s: string): SQL =
# 正确地为 SQL 语句引用字符串
return SQL(s)
proc `%` (frmt: SQL, values: openarray[string]): SQL =
# 引用每个参数:
let v = values.mapIt(properQuote(it))
# 需要一个临时类型用到类型转换 :-(
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 等类似,已经作为 SqlQuery type 实际存在与库中。