5 Programmer-Defined Datatypes
(part ("(lib scribblings/reference/reference.scrbl)" "structures")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) also documents structure types.
新的数据类型通常使用 struct 形式创建,这是本章的主题。基于类的对象系统 (我们推迟到 Classes and Objects 讨论)提供了创建新数据类型的另一种机制, 但即使是类和对象也是基于结构类型来实现的。
5.1 Simple Structure Types: struct
(part ("(lib scribblings/reference/reference.scrbl)" "define-struct")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) also documents struct.
粗略地看,struct 的语法是
(struct struct-id (field-id ...))
(struct posn (x y))
struct 形式绑定 struct-id 以及从 struct-id 和 field-id 构建的多个标识符:
struct-id : 一个 constructor 函数, 接受与 field-id 数量相同的参数, 并返回结构类型的一个实例。
Example:> (posn 1 2) #<posn>
struct-id? : 一个 predicate 函数,接受单个参数,如果它是结构类型的实例则返回 #t, 否则返回 #f。
Examples:> (posn? 3) #f
> (posn? (posn 1 2)) #t
struct-id-field-id : 对于 每个 field-id,一个 accessor,用于从结构类型的实例中 提取对应字段的值。
Examples:> (posn-x (posn 1 2)) 1
> (posn-y (posn 1 2)) 2
struct:struct-id : 一个 structure type descriptor,它是一个将结构类型表示为一等值的值 (通过 #:super,稍后在 More Structure Type Options 中讨论)。
struct 形式对结构类型实例中字段可以出现的值的种类不施加任何约束。 例如,(posn "apple" #f) 会产生 posn 的一个实例, 即使 "apple" 和 #f 对于 posn 实例的明显用途来说 不是有效的坐标。对字段值实施约束(例如要求它们是数字)通常是 contract 的工作, 如稍后在 Contracts 中讨论的那样。
5.2 Copying and Update
struct-copy 形式克隆一个结构并可选地更新克隆中的指定字段。 这个过程有时称为 functional update,因为结果是一个具有更新字段值的结构, 但原始结构不会被修改。
(struct-copy struct-id struct-expr [field-id expr] ...)
出现在 struct-copy 之后的 struct-id 必须是由 struct 绑定的结构类型名称。 struct-expr 必须产生结构类型的一个实例。结果是结构类型的一个新实例, 与旧实例类似,只是每个 field-id 指示的字段获得对应 expr 的值。
> (define p1 (posn 1 2)) > (define p2 (struct-copy posn p1 [x 3])) > (list (posn-x p2) (posn-y p2)) '(3 2)
> (list (posn-x p1) (posn-y p1)) '(1 2)
5.3 Structure Subtypes
struct 的扩展形式可用于定义 structure subtype, 即扩展现有结构类型的结构类型:
(struct struct-id super-id (field-id ...))
super-id 必须是由 struct 绑定的结构类型名称(即不能直接用作表达式的名称)。
(struct posn (x y)) (struct 3d-posn posn (z))
结构子类型继承其父类型的字段,子类型的 constructor 在父类型字段的值之后接受子类型字段的值。 结构子类型的实例可以与父类型的 predicate 和 accessor 一起使用。
> (define p (3d-posn 1 2 3)) > p #<3d-posn>
> (posn? p) #t
> (3d-posn-z p) 3
; a 3d-posn has an x field, but there is no 3d-posn-x selector: > (3d-posn-x p) 3d-posn-x: undefined;
cannot reference an identifier before its definition
in module: top-level
; use the supertype's posn-x selector to access the x field: > (posn-x p) 1
5.4 Opaque versus Transparent Structure Types
使用如下结构类型定义
(struct posn (x y))
结构类型的实例以不显示任何字段值信息的方式打印。也就是说,结构类型默认是 opaque。 如果结构类型的 accessor 和 mutator 对模块保持私有,则其他模块无法依赖该类型实例的表示。
要使结构类型 transparent,请在字段名序列之后使用 #:transparent 关键字:
(struct posn (x y) #:transparent)
> (posn 1 2) (posn 1 2)
transparent 结构类型的实例打印得像对 constructor 的调用,因此它显示结构的字段值。 transparent 结构类型还允许对其实例使用反射操作,例如 struct? 和 struct-info (参见 Reflection and Dynamic Evaluation)。
结构类型默认是 opaque 的,因为 opaque 的结构实例提供更多的封装保证。 也就是说,库可以使用 opaque 结构来封装数据,库的客户端不能按照库允许之外的方式操作结构中的数据。
5.5 Structure Comparisons
通用的 equal? 比较会自动递归到 transparent 结构类型的字段上, 但对于 opaque 结构类型,equal? 默认仅检查实例的同一性:
(struct glass (width height) #:transparent)
> (equal? (glass 1 2) (glass 1 2)) #t
(struct lead (width height))
> (define slab (lead 1 2)) > (equal? slab slab) #t
> (equal? slab (lead 1 2)) #f
要支持通过 equal? 进行实例比较而不使结构类型变为 transparent, 你可以使用 #:methods 关键字、gen:equal+hash,并实现三个方法:
(struct lead (width height) #:methods gen:equal+hash [(define (equal-proc a b equal?-recur) ; compare a and b (and (equal?-recur (lead-width a) (lead-width b)) (equal?-recur (lead-height a) (lead-height b)))) (define (hash-proc a hash-recur) ; compute primary hash code of a (+ (hash-recur (lead-width a)) (* 3 (hash-recur (lead-height a))))) (define (hash2-proc a hash2-recur) ; compute secondary hash code of a (+ (hash2-recur (lead-width a)) (hash2-recur (lead-height a))))])
> (equal? (lead 1 2) (lead 1 2)) #t
列表中的第一个函数对两个 lead 实现 equal? 测试; 函数的第三个参数用于替代 equal? 进行递归相等性测试, 以便正确处理数据循环。另外两个函数计算主 hash 码和 副 hash 码,用于 hash tables:
> (define h (make-hash)) > (hash-set! h (lead 1 2) 3) > (hash-ref h (lead 1 2)) 3
> (hash-ref h (lead 2 1)) hash-ref: no value found for key
key: #<lead>
gen:equal+hash 提供的第一个函数不要求 递归比较结构的字段。例如,表示集合的结构类型可能通过 检查集合成员是否相同来实现相等性,而不依赖于内部表示中 元素的顺序。只需注意 hash 函数对任何两个 应被视为等价的结构类型产生相同的值。
5.6 Structure Type Generativity
每当 struct 形式被求值时,它都会生成一个 与所有现有结构类型不同的结构类型,即使某个其他结构类型 具有相同的名称和字段。
这种生成性对于强制抽象和实现诸如解释器之类的程序很有用, 但要注意不要将 struct 形式放在会被多次 求值的位置。
(define (add-bigger-fish lst) (struct fish (size) #:transparent) ; new every time (cond [(null? lst) (list (fish 1))] [else (cons (fish (* 2 (fish-size (car lst)))) lst)])) > (add-bigger-fish null) (list (fish 1))
> (add-bigger-fish (add-bigger-fish null)) fish-size: contract violation
expected: fish?
given: (fish 1)
(struct fish (size) #:transparent) (define (add-bigger-fish lst) (cond [(null? lst) (list (fish 1))] [else (cons (fish (* 2 (fish-size (car lst)))) lst)]))
> (add-bigger-fish (add-bigger-fish null)) (list (fish 2) (fish 1))
5.7 Prefab Structure Types
虽然 transparent 结构类型以显示其内容的方式打印, 但结构的打印形式不能像数字、字符串、symbol 或列表的打印形式那样 在表达式中使用来取回结构。
prefab("previously fabricated",预先制造的)结构类型是一种 内建类型,Racket 打印器和表达式读取器都认识它。存在无限多个这样的类型, 它们按名称、字段数量、父类型和其他此类细节进行索引。prefab 结构的打印形式 类似于 vector,但它以 #s 而不是 # 开头, 并且打印形式中的第一个元素是 prefab 结构类型的名称。
以下示例显示了具有一个字段的 sprout prefab 结构类型的实例。第一个实例的字段值为 'bean,第二个实例的字段值为 'alfalfa:
> '#s(sprout bean) '#s(sprout bean)
> '#s(sprout alfalfa) '#s(sprout alfalfa)
像数字和字符串一样,prefab 结构是"自引用"的, 所以上面的引号是可选的:
> #s(sprout bean) '#s(sprout bean)
当您将 #:prefab 关键字与 struct 一起使用时,您获得的绑定可以与 现有的 prefab 结构类型配合工作,而不是生成新的结构类型:
> (define lunch '#s(sprout bean)) > (struct sprout (kind) #:prefab) > (sprout? lunch) #t
> (sprout-kind lunch) 'bean
> (sprout 'garlic) '#s(sprout garlic)
上面的字段名 kind 对于查找 prefab 结构类型并不重要; 只有名称 sprout 和字段数量才重要。同时, 具有三个字段的 prefab 结构类型 sprout 与 只有一个字段的结构类型是不同的:
> (sprout? #s(sprout bean #f 17)) #f
> (struct sprout (kind yummy? count) #:prefab) ; redefine > (sprout? #s(sprout bean #f 17)) #t
> (sprout? lunch) #f
prefab 结构类型可以有另一个 prefab 结构类型作为其父类型, 它可以有可变字段,也可以有自动字段。 这些维度上的任何变化都对应于不同的 prefab 结构类型, 结构类型名称的打印形式编码了所有相关细节。
> (struct building (rooms [location #:mutable]) #:prefab)
> (struct house building ([occupied #:auto]) #:prefab #:auto-value 'no) > (house 5 'factory) '#s((house (1 no) building 2 #(1)) 5 factory no)
每个 prefab 结构类型都是 transparent 的——但比 transparent 类型更不抽象,因为实例可以在不访问 特定结构类型声明或现有示例的情况下创建。总体而言, 结构类型的不同选项提供了从更抽象到更方便的一系列可能性:
Opaque(默认):实例无法在不访问结构类型声明的情况下 被检查或伪造。如下一节所讨论的,constructor guards 和 properties 可以附加到结构类型上,以进一步保护或 特化其实例的行为。
Transparent:任何人都可以在不访问结构类型声明的情况下 检查或创建实例,这意味着值打印器可以显示实例的内容。然而, 所有实例创建都通过 constructor guard,以便控制实例的内容, 并且可以通过 properties 特化实例的行为。 由于结构类型是由其定义生成的,实例不能仅通过结构类型的名称 来制造,因此不能由表达式读取器自动生成。
Prefab:任何人都可以在任何时候检查或创建实例, 无需事先访问结构类型声明或示例实例。因此, 表达式读取器可以直接制造实例。实例不能有 constructor guard 或 properties。
由于表达式读取器可以生成 prefab 实例,当方便的 serialization 比抽象更重要时它们很有用。不过, Opaque 和 transparent 结构也可以被序列化, 如果它们是用 serializable-struct 定义的, 如 Datatypes and Serialization 中所述。
5.8 More Structure Type Options
struct 的完整语法支持许多选项, 既包括结构类型级别的选项,也包括单个字段级别的选项:
(struct struct-id maybe-super (field ...) struct-option ...)
maybe-super =
| super-id field = field-id | [field-id field-option ...]
struct-option 总是以关键字开头:
#:mutable 使结构的所有字段变为可变的,并为每个 field-id 引入一个 mutator set-struct-id-field-id! 用于设置结构类型实例中对应字段的值。
Examples:
> (struct dot (x y) #:mutable)
(define d (dot 1 2)) > (dot-x d) 1
> (set-dot-x! d 10) > (dot-x d) 10
#:mutable 选项也可以用作 field-option,此时它使单个字段变为可变的。
Examples:
> (struct person (name [age #:mutable]))
(define friend (person "Barney" 5)) > (set-person-age! friend 6) > (set-person-name! friend "Mary") set-person-name!: undefined;
cannot reference an identifier before its definition
in module: top-level
#:transparent 控制对结构实例的反射访问,如前一节 Opaque versus Transparent Structure Types 中所讨论的。
#:inspector inspector-expr 泛化 #:transparent 以支持对反射操作的更受控访问。
#:prefab 访问内建结构类型,如前一节 Prefab Structure Types 中所讨论的。
#:auto-value auto-expr 指定用于结构类型中所有自动字段的值,其中自动字段由 #:auto 字段选项指示。constructor 过程不 接受自动字段的参数。自动字段隐式可变(通过反射操作), 但只有在同时指定了 #:mutable 时才会绑定 mutator 函数。
Examples:
> (struct posn (x y [z #:auto]) #:transparent #:auto-value 0) > (posn 1 2) (posn 1 2 0)
#:guard guard-expr 指定一个 constructor guard 过程,每当创建结构类型的实例时都会调用它。 该 guard 接受与结构类型中非自动字段数量相同的参数, 再加上一个用于被实例化类型名称的参数(以防实例化子类型, 此时最好使用子类型的名称报告错误)。guard 应返回与给定值 相同数量的值(减去名称参数)。如果给定参数中有不可接受的, guard 可以引发异常,或者它可以转换参数。
Examples:
> (struct thing (name) #:transparent #:guard (lambda (name type-name) (cond [(string? name) name] [(symbol? name) (symbol->string name)] [else (error type-name "bad name: ~e" name)]))) > (thing "apple") (thing "apple")
> (thing 'apple) (thing "apple")
> (thing 1/2) thing: bad name: 1/2
即使创建子类型的实例时也会调用 guard。在这种情况下, 只有 constructor 接受的字段会提供给 guard (但子类型的 guard 会同时获得原始字段和子类型添加的字段)。
Examples:
> (struct person thing (age) #:transparent #:guard (lambda (name age type-name) (if (negative? age) (error type-name "bad age: ~e" age) (values name age)))) > (person "John" 10) (person "John" 10)
> (person "Mary" -1) person: bad age: -1
> (person 10 10) person: bad name: 10
#:methods interface-expr [body ...] 为结构类型关联与 generic interface(泛型接口)对应的方法定义。 例如,为 gen:dict 实现方法允许结构类型的实例 用作字典。为 gen:custom-write 实现方法 允许自定义结构类型实例的 display 方式。
Examples:
> (struct cake (candles) #:methods gen:custom-write [(define (write-proc cake port mode) (define n (cake-candles cake)) (show " ~a ~n" n #\. port) (show " .-~a-. ~n" n #\| port) (show " | ~a | ~n" n #\space port) (show "---~a---~n" n #\- port)) (define (show fmt n ch port) (fprintf port fmt (make-string n ch)))]) > (display (cake 5))
.....
.-|||||-.
| |
-----------
#:property prop-expr val-expr 将 property 和值与结构类型关联。 例如,prop:procedure property 允许结构实例 用作函数;property 值决定了将结构用作函数时 调用是如何实现的。
Examples:
> (struct greeter (name) #:property prop:procedure (lambda (self other) (string-append "Hi " other ", I'm " (greeter-name self))))
(define joe-greet (greeter "Joe")) > (greeter-name joe-greet) "Joe"
> (joe-greet "Mary") "Hi Mary, I'm Joe"
> (joe-greet "John") "Hi John, I'm Joe"
#:super super-expr 在 struct-id 旁边提供 super-id 的替代方式。 super-expr 应该产生一个 structure type descriptor 值, 而不是结构类型的名称(结构类型名称不是表达式)。#:super 的一个 优势是结构类型描述符是值,因此可以传递给过程。
Examples:
(define (raven-constructor super-type) (struct raven () #:super super-type #:transparent #:property prop:procedure (lambda (self) 'nevermore)) raven)
> (let ([r ((raven-constructor struct:posn) 1 2)]) (list r (r))) (list (raven 1 2) 'nevermore)
> (let ([r ((raven-constructor struct:thing) "apple")]) (list r (r))) (list (raven "apple") 'nevermore)
(part ("(lib scribblings/reference/reference.scrbl)" "structures")) in (part ("(lib scribblings/reference/reference.scrbl)" "top")) provides more on 结构类型.