5 程序员定义的数据类型
在《Racket参考》中的“(structures)”部分也有关于数据结构类型的文档。
新的数据类型通常用struct表来创造,这是本章的主题。基于类的对象系统,我们参照《类和对象》,为创建新的数据类型提供了一种替换机制,但即使是类和对象也是根据结构类型实现。
在《Racket参考》中的“(define-struct)”部分也有关于struct的文档。
作为一个最接近的,struct的语法是
(struct struct-id (field-id ...))
(struct posn (x y))
struct表绑定struct-id和一个标识符的数值,它构建于struct-id和field-id:
-
struct-id:一个构造器(constructor)函数,带有和acket[_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,在《更多的结构类型选项》中作为后续讨论)。
一个struct表对值的种类不设置约束条件,它可表现为这个结构类型的一个实例中的字段。例如,(posn "apple" #f)过程产生一个posn实例,即使"apple"和#f对posn实例的显性使用是无效的配套。执行字段值上的约束,比如要求它们是数值,通常是一个合约的工作,在《合约》中作为后续讨论。
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-x p2)) '(1 3)
struct的一个扩展表可以用来定义一个结构子类型(structure subtype),它是一种扩展一个现有结构类型的结构类型:
(struct struct-id super-id (field-id ...))
这个super-id必须是一个由struct绑定的结构类型名称(即名称不能被作为一个表达式直接使用)。
(struct posn (x y)) (struct 3d-posn posn (z))
一个结构子类型继承其超类型的字段,并且子类型构造器接受在超类型字段的值之后的子类型字段的值。一个结构子类型的一个实例可以被用作这个超类型的判断和访问器。
> (define p (3d-posn 1 2 3)) > p #<3d-posn>
> (posn? p) #t
> (3d-posn-z p) 3
; 3d-posn有一个x字段,但是这里却没有3d-posn-x选择器: > (3d-posn-x p) 3d-posn-x: undefined;
cannot reference undefined identifier
; 使用基类型的posn-x选择器去访问x字段: > (posn-x p) 1
用一个结构类型定义如下:
(struct posn (x y))
结构类型的一个实例以不显示关于字段值的任何信息的方式打印。也就是说,默认的结构类型是不透明的(opaque)。如果一个结构类型的访问器和修改器对一个模块保持私有,那么没有其它的模块可以依赖这个类型实例的表示。
让一个结构类型透明(transparent),在字段名序列后面使用#:transparent关键字:
(struct posn (x y) #:transparent)
> (posn 1 2) (posn 1 2)
一个透明结构类型的一个实例像一个对构造器的调用一样打印,因此它显示这个结构字段值。一个透明结构类型也允许反射操作,比如struct?和struct-info,在其实例中被使用(参见《反射和动态求值》)。
默认情况下,结构类型是不透明的,因为不透明的结构实例提供了更多的封装保证。也就是说,一个库可以使用不透明的结构来封装数据,而库中的客户机除了在库中被允许之外,也不能操纵结构中的数据。
一个通用的equal?比较自动出现在一个透明的结构类型的字段上,但是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?支持实例比较而不需要使结构型透明,你可以使用#:methods关键字、gen:equal+hash并执行三个方法:
(struct lead (width height) #:methods gen:equal+hash [(define (equal-proc a b equal?-recur) ; 比较a和b (and (equal?-recur (lead-width a) (lead-width b)) (equal?-recur (lead-height a) (lead-height b)))) (define (hash-proc a hash-recur) ; 计算首要的a哈希代码。 (+ (hash-recur (lead-width a)) (* 3 (hash-recur (lead-height a))))) (define (hash2-proc a hash2-recur) ; 计算次重要的a哈希代码。 (+ (hash2-recur (lead-width a)) (hash2-recur (lead-height a))))])
> (equal? (lead 1 2) (lead 1 2)) #t
列表中的第一个函数实现对两个lead的equal?测试;函数的第三个参数是用来代替equal?实现递归的相等测试,以便这个数据循环可以被正确处理。其它两个函数计算以哈希表(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的第一个函数不需要递归比较结构的字段。例如,表示一个集合的一个结构类型可以通过检查这个集合的成员是相同的来执行相等,独立于内部表示的元素顺序。只是要注意哈希函数对任何两个假定相等的结构类型产生相同的值。
每次对一个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;
given value instantiates a different structure type with
the same name
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))
虽然一个透明结构类型以显示内容的方式打印,但不像一个数值、字符串、符号或列表的打印表,结构的打印表不能用在一个表达式中以找回结构。
一个预制(prefab)(“被预先制造”)结构类型是一个内置的类型,它是已知的Racket打印机和表达式阅读器。有无限多这样的类型存在,并且它们通过名字、字段计数、超类型以及其它细节来索引。一个预制结构的打印表类似于一个向量,但它以#s开始而不是仅以#开始,而且打印表的第一个元素是预制结构类型的名称。
下面的示例显示具有一个字段的sprout预置结构类型的实例。第一个实例具有一个字段值'bean,以及第二个具有字段值'alfalfa:
> '#s(sprout bean) '#s(sprout bean)
> '#s(sprout alfalfa) '#s(sprout alfalfa)
像数字和字符串一样,预置结构是“自引用”,所以上面的引号是可选的:
> #s(sprout bean) '#s(sprout bean)
当你用struct使用#:prefab关键字,而不是生成一个新的结构类型,你获得与现有的预制结构类型的绑定:
> (define lunch '#s(sprout bean)) > (struct sprout (kind) #:prefab) > (sprout? lunch) #t
> (sprout-kind lunch) 'bean
> (sprout 'garlic) '#s(sprout garlic)
上面的字段名kind对查找预置结构类型无关紧要,仅名称sprout和字段数量是紧要的。同时,具有三个字段的预制结构类型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
一个预制结构类型可以有另一种预制结构类型作为它的超类型,它具有可变的字段,并它可以有自动字段。这些维度中的任何变化都对应于不同的预置结构类型,而且结构类型名称的打印表编码所有的相关细节。
> (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)
每个预制结构类型都是透明的——但甚至比一个透明类型更抽象,因为可以创建实例而不必访问一个特定的结构类型声明或现有示例。总体而言,结构类型的不同选项提供了从更抽象到更方便的一连串可能性:
-
不透明的(Opaque)(默认):没有访问结构类型声明,就不能检查或创造实例。正如下一节所讨论的,构造器看守(constructor guards)和属性(properties)可以附加到结构类型上以进一步保护或专门化其实例的行为。
-
透明的(Transparent):任何人都可以检查或创建一个没有访问结构类型声明的实例,这意味着这个值打印机可以显示一个实例的内容。然而,所有实例创建都经过一个构造器看守,这样可以控制一个实例的内容,并且实例的行为可以通过属性(properties)进行特例化。由于结构类型由其定义生成,实例不能简单地通过结构类型的名称来制造,因此不能由表达式读取器自动生成。
-
预制的(Prefab):任何人都可以在任何时候检查或创建一个实例,而不必事先访问一个结构类型声明或一个实例。因此,表达式读取器可以直接制造实例。实例不能具有一个构造器看守或属性。
由于表达式读取器可以生成预制实例,所以在便利的序列化(serialization)比抽象更重要时它们是有用的。然而,如果他们如《数据类型和序列化》所描述那样被用serializable-struct定义,不透明和透明的结构也可以被序列化。}]
无论是在结构类型级还是在个别字段级上,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产生一个设置方式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 undefined identifier
#:transparent 对结构实例的控制反射访问,如前面一节《不透明结构类型与透明结构类型对比》所讨论的那样。
#:inspector inspector-expr 推广#:transparent以支持更多的控制访问或反射操作。
#:prefab 访问内置结构类型,如前一节《预制结构类型》所讨论的。
#:auto-value auto-expr 指定一个被用于所有在结构类型里的自动字段值,这里一个自动字段被#:auto字段被标示。这个构造函数不接受给自动字段的参数。自动字段无疑是可变的(通过反射操作),但设置函数仅在#:mutable也被指定的时候被绑定。
Examples:
> (struct posn (x y [z #:auto]) #:transparent #:auto-value 0) > (posn 1 2) (posn 1 2 0)
#:guard guard-expr 每当一个结构类型的实例被创建,都指定一个构造器看守(constructor 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
即使子类型实例被创建,这个看守也会被调用。在这种情况下,只有被构造器接受的字段被提供给看守(但是子类型的看守同时获得子类型添加的原始字段和现有字段)。
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属性允许一个结构实例用作一个函数;属性值决定当使用这个结构作为一个函数时一个调用如何被执行。
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)
在《Racket参考》中的“(structures)”里提供有更多关于数据结构类型的内容。