Program Structure 笔记15 (数据类型的表示及处理)
(作者:colinboy Email:cybbh@163.com) 2008.6.6
不同数据的表示以及处理
对于一个有理数(表示成分数形式)或者一个复数,都可以用如下对结构表示:
对于上述表示,可以看成是有理数3/4或者复数3+4i,一般除非我们说明它是表示的什么数据,否则我们没法判断它具体表示什么.
为了决定一个数据的具体类型,一般程序设计语言采取如下2种形式:
1. 类型定义(例如c/c++/java)
2. 数据标志
scheme中采用带标志的数据表示各种不同类型,例如对于上述的对结构,可以增加一个标志单元,表示为:
这种表示通过在数据本身前面增加一个类型说明来表示数据的类型.
实现:
对于上述例子,对带标志数据的实现如下:
;增加标志
(define attach-tag cons)
;获取类型
(define type-tag car)
;获取内容
(define contents cdr)
创建上述的有理数和复数的方法如下:
(define r1 (attach-tag 'rational (cons 3 4))) --> (rational 3 . 4)
(define c1 (attach-tag 'complex (cons 3 4))) --> (complex 3 . 4)
对于带标志数据的处理有3种常见形式:
1. 传统设计风格
2. 数据导向
3. 消息传递
例子
假设我们设计一个几何图形处理程序,有很多图形,每个图形数据前面都会加一个标志来表示这是哪种图形,例如正方形标志为square,圆形为circle,我们对每种图形都能求它的面积和周长,我们把能处理的图形和能对图形进行的操作画成一个二维表格,这个表格的行对应着每个图形能进行的操作,列对应着每个操作能作用于哪些图形,表格如下:
如下例子我们只实现square(正方形)和circle(圆形)两种图形.
1. 传统设计风格
首先来看看传统设计风格如何实现,最先想到的处理这些图形的方法也许就是创建2个过程area和perimeter,每个过程能作用于很多图形.如表格上红色所示,也就是对每列编写一个过程,
(define (make-square side)
(attach-tag 'square side))
(define (make-circle radius)
(attach-tag 'circle radius))
(define pi 3.14)
;area过程的定义
(define (area shape)
(cond ((eq? (type-tag shape) 'square) (* (contents shape) (contents shape)))
((eq? (type-tag shape) 'circle) (* pi (contents shape) (contents shape)))
(else (error "some error occur"))))
;perimeter过程的定义
(define (perimeter shape)
(cond ((eq? (type-tag shape) 'square) (* (contents shape) 4))
((eq? (type-tag shape) 'circle) (* pi 2 (contents shape)))
(else (error "some error occur"))))
;测试
(define s (make-square 5))
(define c (make-circle 5))
(area s) --> 25
(area c) --> 78.5
(perimeter s) --> 20
(perimeter c) --> 31.4
传统设计风格直观明了,但是当我们要添加和删除各种图形时要修改很多函数,维护不方便.
2. 数据导向
其实我们要对某种图形进行操作,对于前面的表格,只要知道操作的图形以及需要进行的操作,查表就可以知道这个操作如何进行,根据这个原理,我们可以把各种图形能进行的操作存储在表格中,当执行的时候根据图形的类型以及要进行的操作查找表格找出操作的过程然后执行.
为此我们需要2个过程,一个是往表格中存放数据,另一个是从表格中取出数据,假设我们已经实现了这2个过程,名称为put和get.
put和get的使用方法如下:
(put 'lastname 'yuan 'cao)
(get 'lastname 'yuan) --> cao
(get 'bank-idpassword 'yuan) --> #f
put用于存储数据到表格中,第一个参数为表格的行标志,第二个参数为表格的列标志,第三个为存入的数据.
get用于取出数据, 第一个参数为表格的行标志,第二个参数为表格的列标志,如果输入的行或列不存在,就返回#f.
数据导向的实现为:
;存放过程到表格
(put 'square 'area (lambda (s) (* s s)))
(put 'circle 'area (lambda (r) (* pi r r)))
(put 'square 'perimeter (lambda (s) (* s 4)))
(put 'circle 'perimeter (lambda (r) (* 2 pi r)))
;通用操作
(define (operate op obj)
(let ((proc (get (type-tag obj) op)))
(if proc
(proc (contents obj))
(error "some error occur"))))
(define (area shape)
(operate 'area shape))
(define (perimeter shape)
(operate 'perimeter shape))
;测试
(define s (make-square 5))
(area s) --> 25
(perimeter s) --> 20
对于make-square和make-circle函数都不需要修改.
数据导向的设计可以很容易的添加和删除新的图形类型,添加和删除类型都不需要修改现有的过程,只需要更新和删除表格中的内容即可.
3. 消息传递
消息传递的设计是对表格中每行编写一个过程,实现如下:
(define (operate op obj)
(obj op))
(define (make-square s)
(lambda (message)
(cond ((eq? message 'area) (* s s))
((eq? message 'perimeter) (* 4 s))
(else (error "some error occur")))))
(define (make-circle s)
(lambda (message)
(cond ((eq? message 'area) (* pi r r))
((eq? message 'perimeter) (* 2 pi r))
(else (error "some error occur")))))
对于消息传递的实现,只修改了make系列函数和operate函数,这种设计通过对数据本身发送消息而进行数据处理.
把数据能进行的各种操作都封装到了数据本身.
其实以前我们就已经实现过消息处理的设计,就是cons的手工实现kons.