我试着理解静态定义的数据结构和动态定义的数据结构之间的方案有什么不同。
我知道静态定义的数据结构是在编译时创建的,并且是在运行时动态定义的数据结构,但这在实践中意味着什么?
谢谢!
答案 0 :(得分:4)
你的问题有点含糊不清。
回答你的问题需要一个明确的概念,即静态定义的数据结构"与#34;动态定义的数据结构。"毕竟,在实践中,大多数数据结构实例在运行时创建(无论它们是否已分配静态结构类型或是动态定义记录的混合)。因此,这似乎与您对何时创建此类结构的理解相矛盾,至少根据您问题中的最后一个陈述。
在试图推断出你的意思之后,我决定对Scheme和某些其他知名语言进行比较,因为这似乎是我们可能会找到对静态内容的共同理解的最有可能的领域。定义"应该是指。
Scheme(如在R5RS中)并不特别重要,至少在与其他语言(如Pascal,C和C ++)进行比较时。 (作为对词法范围和static name resolution的严格遵守,它在许多方面比其他语言(如Perl或Python)更多静态。)
在Pascal或C等语言中,通常会在隔离的声明中写出显式结构或类定义。这样的显式定义静态(即,在编译时)定义:
分配了多少字节的内存来表示结构的每个实例,
从实例中提取时,如何解释结构的每个实例的成员/字段。
因此,在C中,声明如下:
typedef struct coordinate coord_t;
struct coordinate {
intptr_t x;
intptr_t y;
coord_t *next;
};
是一种告诉编译器您希望类型struct coordinate
表示占用3个字的内存块的方法:x
和y
中每个字的一个机器字,以及第三个机器字,用于存放指向另一个坐标的指针(我们假设我们将在列表中将这些记录链接在一起)。该声明还表明x
和y
表示有符号整数(在字大小允许的范围内),而next
表示指向坐标的指针。
(一个指针,即内存地址,与任意整数完全不同,尽管某些语言的语法和实现在一些上下文中混淆了指针和整数。)
另一个重要细节是,每个标识符都是manifestly constrained到允许保存的值的类型。对于结构字段(上面的x
,y
和next
)以及参数和局部变量都是如此,如下面的函数定义sum_coords
所示
将每个坐标解释为二维平面中的矢量,可以在坐标列表中添加所有坐标,如下所示:
coord_t sum_coords(coord_t *coords)
{
coord_t c;
c.x = 0;
c.y = 0;
c.next = NULL;
while (coords != NULL) {
c.x += coords->x;
c.y += coords->y;
coords = coords->next;
}
return c;
}
请注意,coords
明显限制为只保留coord_t*
,而c
明显受限于只保留coord_t
又名struct coordinate
。如果我在没有明确使用类型转换的情况下尝试违反该约束,编译器可能会拒绝编译我的程序,如下所示:
coord_t sum_coords(coord_t *coords)
{
coord_t c;
c = coords; // <-- compiler error: incompatible types in assignment
...
}
到目前为止,这么好。但当然,你的问题是关于Scheme。
典型的R5RS方案代码不像上面那样工作。在R5RS Scheme中,没有编写单独的声明来由方案编译器/解释器解释,描述了与数据类型的每个实例关联的内存量,或者应该如何解释实例的内存块中的字节。 / p>
在R5RS Scheme中,不是使用如上所述的结构定义来创建复合数据,而是分配对,向量,字符串或过程对象。 (Scheme中的列表由对组成。)在许多Scheme实现中,每对占用恰好两个单词,而向量和字符串各自具有在分配它们时提供的单个大小。 (过程对象实例所需的内存高度依赖于Scheme运行时以及它如何表示闭包和词汇环境。)关键是,你通常不会认为关于数据结构实例使用的机器字数;你首先关注的是解决手头的问题,并且在你确定需要这样的努力之前,不要担心字节数。
无论如何,重点是,在R5RS Scheme中,不需要告诉编译器/解释器如何表示坐标结构;至少,不是在为此目的添加的独立运行时解析声明中。
相反,人们可能会这样写:
;; A Coordinate is a (list Number Number)
;; A CoordinateList is one of:
;; - '()
;; - (cons c cl), where c is a Coordinate and cl is a CoordinateList
;; sum-coords: CoordinateList -> Coordinate
(define (sum-coords l)
(cond ((null? l) (list 0 0))
(else (let ((c (car l))
(sum-of-rest (sum-coords (cdr l))))
(list (+ (list-ref c 0) (list-ref sum-of-rest 0))
(+ (list-ref c 1) (list-ref sum-of-rest 1)))))))
;; Below is in the REPL:
> (sum-coords '((1 4) (2 3) (3 2) (4 1)))
(10 10)
一些差异:
此处,数据如何布局的描述在Scheme comment 中,而不是代码。这样的注释是好的Scheme风格,因为即使方案编译器/解释器不需要这样的注释,其他人类几乎可以肯定 需要它们。 (有关此主题的更多信息,请参阅文本How to Design Programs。)
不需要明确声明代表Coordinate
的列表元素是数字; Scheme运行时将在必要时携带该信息(以便您可以使用number?
之类的谓词来查看所携带的值是否为数字。
在必要时,Scheme的类型安全实现将检查像+
这样的操作,提供的参数是数字(而不是对对的引用,或对向量的引用等);但是,R5RS报告本身表示&#34;对于一个操作,如果一个参数没有被指定处理&#34 ;;这是一个错误。根据报告的惯例,未必检测到此类错误,并且未指定后续行为。
同样,变量c
不是明显约束为仅包含两个元素列表。也就是说,此过程并不固有地规定c
表示的值必须始终看起来像匹配我们的数据定义的坐标。只有通过编程约定,我们才会尝试确保这样的约束成立;编程语言与它无关。
在R5RS Scheme中,不会将类型与对或向量的子组件相关联; 任何值都可以由一对的car
或cdr
引用,也可以由向量的任何元素引用。将其与C代码进行比较,我们只能将intptr_t
值放入x
的{{1}}和y
字段中,并且只能放struct coordinate
(即struct coordinate*
字段中的坐标指针。
最后一点是区分动态和静态的一个关键区别:在上面的Scheme程序中,使用列表(对)和向量时可以获得很大的自由度,因为你可以将任何类型的值放入其中。这种自由可以提供很大的力量和灵活性。但是你也放弃了一些东西:当你将错误的类型的值放入一个对或向量中时,你会失去编译器的帮助,从而打破你所依赖的其他代码的假设。
考虑例如next
的这种变体:
sum-coords
我故意混淆了这个版本。有一个bug;当我运行它时,我收到一个错误:
(define (sum-coords l)
(cond ((null? l) (list 0 0))
(else (let* ((c (car l))
(s (sum-coords (cdr c))))
(list (+ (car (car l)) (car c))
(+ (car (cdr c)) (car (cdr s))))))))
该错误有点难以找到(尽管如果有人跟随design recipe,那么就永远不会编写上面的代码)。但是有人可以声称这个错误很容易在Scheme中生成,因为语言鼓励使用列表和对来表示所有内容,因此并不总是保护你阻止你传递一个坐标(一个一种列表)到一个预期CoordinateList(另一种列表)的地方。 (Scheme的某些方言,例如Typed Racket,为用户提供了静态定义静态数据类型的方法,以便编译器可以再次提供此类帮助。)
当然,在像C这样的语言中,类型系统主要是为了告诉编译器如何在内存中布局对象。在C中使用类型转换非常简单,可以在预期> (sum-coords '((1 4) (2 3) (3 2) (4 1)))
Error: cdr: 4 is not a pair.
的地方传递struct coordinate*
,并且您不太可能得到与
intptr_t
所以你应该采取这种所谓的'#34;区别&#34;两者之间有大粒盐。有一个big difference between "Type Safety" and "Statically Typed"。 (简而言之,R5RS Scheme和C都不是类型安全的,尽管R5RS Scheme 的几个实现类型是安全的,或者尝试是。)
另一个可以区分动态和静态的地方:我可能选择像这样编写数据定义:
> (sum-coords '((1 4) (2 3) (3 2) (4 1)))
Error: +: (4 1) is not a number.
;; A Coordinate is a (list Number Number Any ...)
现在,sum-coords的原始实现仍然是&#34;工作&#34;有这个定义。用户可以随意将任何额外信息(可能是颜色或点上的标签)作为列表中的额外元素,我们仍然可以总结X和Y组件。这与上述观点有关,;; interpretation: A Coordinate (list x y payload ...) is a point at location <x,y> on a 2D plane, with a metadata payload also associated with the point.
并未明显限制为仅包含两个数字元素列表。
(当然,通过子类化可以在C ++这样的语言中实现相同的目标,并且通过在前三个单词中定义与布局c
具有相同布局的另一个结构,然后在C中完成相同的目标,然后键入在构建链表时进行转换。此时,在这些语言的任何中,您实际上正在回归更加动态的数据结构定义;这只是一个问题继续极端之间的频谱。)
即使不需要单独的结构声明,也可以很好地定义一小组用于处理数据类型的过程,并将它们用作所有其他操作都要使用的抽象接口。 。在这种情况下,我们可能会尝试这样做:
struct coordinate
这不是完美的(它是一个漏洞的抽象),但它是一个开始;当然,还需要修改;; coord : Number Number -> Coordinate
(define (coord x y) (list x y))
;; coord-x : Coordinate -> Number
(define (coord-x c) (list-ref c 0))
;; coord-y : Coordinate -> Number
(define (coord-y c) (list-ref c 1))
以使用这些程序,而不是直接访问sum-coords
的列表表示。
请注意,Scheme代码不意味着是C代码的精确逐字模拟。
例如,C代码中的每个坐标由三个机器字组成;坐标结构中的第三个字带有下一个元素的链接字段。在Scheme中更直接地写入的一种方法是a 数据定义如
Coordinate
这将导致上面写的C代码更加忠实的逐字音译,但不会忠实于Scheme的精神,在这种情况下,解耦坐标结构和链接结构更为惯用。 / p>
尽管R5RS Scheme没有用于声明结构的特殊形式,但R5RS确实定义了一个宏系统,其中可以定义这样的record syntax。
此外,一些Scheme方言(如Racket和Chez)提供了语言扩展,用于定义称为记录的结构化数据。即使在后两种情况下,记录仍然是一个潜在的动态实体:一个可以动态创建新的记录类型,而不必在编译时预先提供所有需要的记录类型。但仅仅因为某些事情可以动态完成并不意味着应该如此,正如Chez手册中所解释的那样:
程序界面比语法界面更灵活,但这种灵活性 导致程序可读性降低,并影响编译器生成的能力 高效的代码。程序员只要足够就应该使用语法接口。
如何定义C语言中的数据结构与Scheme之类的语言之间存在许多差异。在C中,您需要预先说明数据结构中出现的条目数以及它们的类型;同样适用于参数和局部变量。在Scheme中,您可以在评论中尽可能多地说出,也可以相应地编写代码。