5.2 定义复杂类型

复杂类型专门为元素而准备——只有元素的类型才能是复杂类型。属性的类型只能是简单类型,而元素的类型则既可以是简单类型,也可以是复杂类型。复杂类型的元素有如下几种情况:

alt 元素内容是简单类型值,但元素包含属性——称为包含简单内容的复杂类型。

alt 包含子元素的元素,空元素或混合内容的元素,不管它们是否包含属性,都是复杂类型的——称为包含复杂内容的复杂类型。

简单类型的元素必须满足如下两点:

alt 元素内容是简单类型值。

alt 元素不能包含属性。

定义复杂类型使用<complexType…/>元素,与<simpleType…/>元素类似,定义<complexType…/>元素有两种方式:

alt 定义全局命名复杂类型:将<complexType…/>元素作为<schema…/>、<redefine…/>元素的子元素使用,需要指定name属性,其值就是该复杂类型的类型名。

alt 定义局部匿名复杂类型:将<complexType…/>元素作为<element…/>元素的子元素使用,无须指定name属性,仅用于指定其父元素<element…/>所定义的元素的类型。

复杂类型使用<complexType…/>元素定义,为了具体地定义复杂类型所包含的内容,<complexType…/>元素可以接受如下两个子元素:

alt <simpleContent…/>:如果元素内容是简单类型的值,但元素包含属性,则使用该元素定义复杂类型。该元素用于定义包含简单内容的复杂类型。

alt <complexContent…/>:对于包含子元素的元素,空元素或混合内容的元素,不管它们是否包含属性,其类型都必须使用该元素定义。该元素用于定义包含复杂内容的复杂类型。

图5.1显示了所有元素类型的整体分类,并详细指出了各类型应该使用怎样的元素来定义。

alt

图5.1 元素类型分类概要

5.2.1 定义复杂类型的方式

如果元素的内容是简单数据类型(包括内置类型和用户派生类型),但元素包含属性,则用<simpleContent…/>元素为该元素定义类型。也就是说,<simpleContent…/>元素用来定义包含简单内容的复杂类型。


alt提示

如果元素的内容是简单数据类型(包括内置类型和用户派生类型),且元素不包含属性,则根本没有必要使用复杂类型,直接使用简单类型定义该元素类型即可。


<simpleContent…/>元素有且只能有一个如下的子元素:

alt <restriction…/>:对已有类型进行限制从而派生新的复杂类型。

alt <extension…/>:对已有类型进行扩展从而派生新的复杂类型。

在使用<simpleContent…/>元素定义元素类型时,不管是使用<restriction…/>还是<extension…/>子元素来派生新类型,都必须指定一个基类型,这个基类型既不能是包含子元素的类型,也不能是混合内容类型,也不能是空元素类型。换句话说,这个基类型要么是简单类型,要么是使用<simpleContent…/>元素定义的复杂类型。


alt提示

复杂类型与简单类型不同,系统中没有内置的复杂类型,所有的复杂类型都必须由用户派生出来。派生复杂类型有两种方式:通过限制(restriction)派生或通过扩展(extension)派生。不管是<simpleContent…/>元素,还是<complexContent…/>元素,都可使用这两种方式来派生新的复杂类型。


图5.2显示了各种复杂类型的派生方式:

alt

图5.2 复杂类型派生方式概要

5.2.2 扩展简单类型

使用<extension…/>元素能以扩展简单类型的方式来派生复杂类型,这种扩展包括添加属性或属性组等方式。

如下所示定义了一份Schema,其中定义了一个<book…/>元素,该<book…/>元素的内容是decimal类型的值,该元素具有两个属性:

程序清单:codes\05\5.2\extendSimpleType.xsd

alt

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\extendSimpleType.xml

alt


alt学生提问:既然派生复杂类型的方式有两种,那接下来是不是应该介绍“限制简单类型来派生复杂类型”了?


答:没错!派生复杂类型的方式有两种:通过扩展派生和通过限制派生。前面刚刚介绍了通过扩展简单类型来派生复杂类型,但通过限制简单类型无法派生复杂类型。前面已经介绍过,为简单类型增加限制后派生的新类型依然是简单类型,而不是复杂类型。对于简单类型而言,只能通过扩展来派生复杂类型。

alt


5.2.3 包含属性的两种方式

前面已经指出:Schema中定义元素和定义属性是完全统一的,只是定义属性使用<attribute…/>元素,关键是如何建立元素和属性的关联,也就是如何指定一个属性属于哪个元素。

Schema支持如下两种语法来指定属性属于某个元素:

alt 局部属性:直接把属性放在<complexType…/>内定义,该局部属性只能属于当前复杂类型。

alt 全局属性:把属性作为<schema…/>的子元素定义,该全局属性可属于多个复杂类型。通过为<attribute…/>元素指定ref属性建立全局属性和复杂类型之间的关联。

使用<attribute…/>元素定义属性时可以指定如下两个常用属性:

alt fixed:为该属性指定一个固定值。

alt default:为该属性指定一个默认值。


alt注意

因为fixed是指定固定值,而default是指定默认值,因此不能在同一个<attribute…/>元素中同时使用这两个属性。


下面先看局部属性的例子:

程序清单:codes\05\5.2\localAttribute.xsd

alt

上面的Schema中定义了2个局部属性,这两个局部属性都是在book_Type类型内定义的,因此都属于该类型。

除此之外,Schema也允许为<attribute…/>元素添加ref属性,该属性值引用一个已有的全局属性,这样就可以建立元素和全局属性之间的关联,上面的Schema可改为如下形式:

程序清单:codes\05\5.2\globalAttribute.xsd

alt

上面的Schema中定义了两个全局属性:isbn和name,然后在①、②两行代码处分别通过ref引用这两个全局属性,建立了全局属性和元素之间的关联关系。

值得指出的是,在为<attribute…/>元素增加ref属性时,该属性总是引自一个已有的全局属性定义,因此该<attribute…/>元素的ref属性不能和name、type两个属性(它们用于定义属性)同时出现。

从上面的Schema代码中可以看到,我们还在<attribute…/>元素中使用了use属性。只有在如下两种情况下才能为<attribute…/>元素指定use属性:

alt <attribute…/>元素里指定了ref属性。

alt 采用<attribute…/>元素定义局部属性时。

use属性用于指定该复杂类型对该属性的要求,支持如下几个属性值:

alt optional:指定该属性是可选的,即它是一个可有可无的属性。这是该属性的默认值。

alt prohibited:指定该属性是被禁止的,表明该属性不能出现。

alt required:指定该属性是必需的。


alt注意

可能有读者对prohibited感到奇怪,既然我们声明了引用该属性,为何又要指定禁止使用该属性呢?其实这个属性值主要在由已有复杂类型派生新的复杂类型时使用,用于删除原复杂类型中的某个属性。还有一点,若定义某个属性时通过default指定了默认的属性值,则引用该属性时的use属性只能是optional。


5.2.4 扩展包含简单内容的复杂类型

图5.2已经指出,扩展包含简单内容的复杂类型依然需要使用<simpleContent…/>元素,因此扩展后的类型依然只能是包含简单内容的复杂类型。简单地说,这种扩展方式只能是添加属性。

如下Schema中采用这种方式派生了一个新的复杂类型:

程序清单:codes\05\5.2\extendSimpleContent.xsd

alt

上面的Schema中先定义了一个book_Type类型,这是一个包含简单内容的复杂类型。接下来以book_Type类型为基础派生出了extended_book_Type类型——是以扩展的方式派生的,因此只是增加了一个price属性。上面的Schema中指定了<book…/>元素的类型是extended_book_Type。对于上面的Schema文档,如下XML文档是有效的:

程序清单:codes\05\5.2\extendSimpleContent.xml

alt

由上面的代码可看到:<book…/>元素里同时指定了isbn、name和price 3个属性,这表明扩展出来的extended_book_Type类型确实包含这3个属性。

5.2.5 使用派生类型的另一种方式


alt学生提问:在上面的Schema中先定义了一个book_Type类型,然后扩展该类型增加了一个price属性,为何不在定义book_Type类型时一次性添加isbn、name和price 3个属性,而非要一次一次地添加呢?这样做有实际意义吗?


答:这样做是有实际意义的。实际开发中可能无法一次确定某个元素需要哪些属性,因此可能第一次定义<book…/>元素时指定它的类型为book_Type。随着项目的开展,可能出现一些新的<book…/>元素,这些<book…/>元素的类型有了改变,此时就可以通过原有的book_Type类型派生新类型,这样就可以复用原有的book_Type类型。

alt


当原有的book_Type类型不足以满足实际需要时,Schema允许通过原有的book_Type类型派生新的类型,此处的Schema中就出现了2个类型(book_Type和extended_book_Type),如果我们只是使用extended_book_Type来定义元素,那这种派生就没有必要,还不如直接修改原有的book_Type好了。

实际上派生类型有一种额外的功能:在开始定义<book…/>元素时指定它的类型是book_Type,后面在为book_Type派生了新类型之后,无须改变<book…/>元素的类型,但<book…/>元素将变成2个“虚拟”的元素:一个<book…/>元素的类型是book_Type,另一个<book…/>元素的类型则是book_Type的派生类型。

下面将extendSimpleContent.xsd中的<book…/>元素的类型改为book_Type,也就是使用如下代码来定义<book…/>元素(Schema其他部分不变):

alt

引入该Schema的XML文档中将拥有两个版本的<book…/>元素,一个<book…/>元素的类型是book_Type,另一个<book…/>元素的类型是book_Type的派生类型,可在<book…/>元素中增加xsi:type属性来指定该元素实际的类型。

如下XML文档是有效的:

程序清单:codes\05\5.2\useDerived1.xml

alt

由上面的代码可以看出,如果使用<book…/>元素时没有指定xsi:type属性,则该元素将采用它的默认类型。


alt提示

xsi:type中的xsi是http://www.w3.org/2001/XMLSchema-instance命名空间对应的前缀,


因此使用该属性时需要增加xsi前缀。该命名空间里定义的type属性可应用于任何元素。

如下XML文档也是有效的:

程序清单:codes\05\5.2\useDerived2.xml

alt

5.2.6 限制包含简单内容的复杂类型

限制包含简单内容的复杂类型主要可从3个方面入手:

alt 为元素内容增加进一步的约束。

alt 为元素的属性类型增加进一步的约束。

alt 删除某些属性。


alt注意

前面已经指出,不要试图限制简单类型来派生复杂类型,为简单类型增加限制只能派生出新的简单类型。


如下Schema中示范了从3个方面来限制包含简单内容的复杂类型:

程序清单:codes\05\5.2\restrictSimpleContent.xsd

alt

上面的Schema中先定义了一个book_Type类型,然后以该类型派生了一个restricted_book_Type类型,派生的restricted_book_Type类型删除了isbn属性,对元素内容和name属性都增加了进一步的约束。对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\restrictSimpleContent.xml

alt

5.2.7 限制anyType派生新类型

前面介绍过,anyType是所有类型的基类型,因此通过限制anyType来派生新的类型应使用<complexContent…/>子元素。也就是说,它派生出来的是包含复杂内容的复杂类型,这就可定义包含子元素的元素类型了。

前面介绍DTD时已经知道:子元素之间是有先后顺序的,而且子元素之间还可存在互斥关系。类似地,Schema也提供了这种支持。Schema为定义子元素提供了如下3个元素:

alt <sequence…/>元素:该元素包含的所有子元素必须按定义的顺序出现。

alt <choice…/> 元素:该元素包含的所有子元素只能出现其中之一。

alt <all…/> 元素:该元素包含的子元素能以任意顺序出现。

如下Schema中通过限制anyType类型派生了几个复杂类型:

程序清单:codes\05\5.2\restrictAnyType.xsd

alt

上面的Schema中定义的<favorite-list…/>元素是favorite-list_Type类型,即该元素里可以出现<book…/>或<game…/>元素中的任意一个。

<book…/>元素是book_Type类型,该元素里可依次出现<name…/>和<price…/>两个子元素。

<game…/>元素是game_Type类型,该元素里可依次出现<name…/>和<type…/>两个子元素。

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\restrictAnyType1.xml

alt

如下XML文档也是有效的:

程序清单:codes\05\5.2\restrictAnyType2.xml

alt


alt学生提问:为什么没有通过扩展anyType来派生新类型呢?


答:想想anyType这个类型吧,它是一个没有任何限制的类型,既然如此,还能怎么扩展呢?所以不要考虑扩展anyType类型。

alt


Schema约定:如果某个复杂类型是由限制anyType派生出来的,那么在定义该复杂类型时可以省略<complexContent…/>和<restriction…/>元素,而直接在<complexType…/>元素内使用<sequence…/>、<choice…/>、<all…/>和<attribute…/>来定义元素和属性。因此restrictAnyType.xsd和如下Schema的效果完全一样(省略了<complexContent…/>和<restriction…/>元素):

程序清单:codes\05\5.2\restrictAnyType2.xsd

alt

从表面上看,<all…/>元素内包含的子元素能以任意顺序出现,似乎可用于定义多个无序子元素,但使用<all…/>元素包含子元素有如下几个限制(使用<sequence…/>和<choice…/>元素则不存在这样的限制):

alt <all…/>元素内包含的子元素不能重复出现,最多只能出现一次。

alt <all…/>元素不能与<sequence…/>和<choice…/>元素同时出现,而且不能放在<sequence…/>和<choice…/>元素内部使用,通常只能作为<complexContent…/>或< complexType…/>元素的顶级子元素使用。

下面先看如下Schema定义:

程序清单:codes\05\5.2\choice_in_sequence.xsd

alt

上面的Schema在<sequence…/>元素里放置了两个<choice…/>子元素,表明这两个<choice…/>组必须按顺序出现,第1组可以是<name…/>或<short-name…/>元素中的任意一个,第2组则可以是<price…/>或<discount-price…/>元素中的任意一个。如下XML文档是有效的:

程序清单:codes\05\5.2\choice_in_sequence1.xml

alt

如下XML文档也是有效的:

程序清单:codes\05\5.2\choice_in_sequence2.xml

alt

从上面的Schema可以看出:<choice…/>和<sequence…/>两个元素可以相互嵌套使用,但<all…/>元素就不能这样使用。而且<choice…/>和<sequence…/>元素包含的<element…/>元素可指定maxOccurs属性,用于指定该元素允许出现的次数,<all…/>元素也可指定maxOccurs属性,但其属性值只能是0或1。下面是使用<all…/>元素的示例:

程序清单:codes\05\5.2\all.xsd

alt

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\all.xml

alt

5.2.8 包含子元素的两种方式

正如在前面的示例中所看到的那样,在Schema中定义元素有两种方式:

alt 全局元素:将<element…/>元素放在<schema…/>元素的根元素下定义。

alt 局部元素:将<element…/>元素放在<sequence…/>、<choice…/>或<all…/>等3个元素里定义。

使用<element…/>定义元素时可以指定如下3个常用属性:

alt fixed:为该元素指定一个固定值。

alt default:为该元素指定一个默认值。

alt nillable:该属性值只能是true或false。用于指定是否可以将显式的零值分配给该元素,该属性只对元素内容有效,而对元素属性无效。默认值为false。

如果定义元素类型时指定了nillable属性为true,则在XML文档中使用该元素时允许将nil属性设置为true,用于将显式的零值分配给该元素。

如下Schema中定义了一个nillable属性为true的元素:

程序清单:codes\05\5.2\nillable.xsd

alt

接下来在XML文档中就可以将显式的零值赋给该元素了,如以下XML文档所示:

程序清单:codes\05\5.2\nillable.xml

alt


alt注意

因为fixed是指定固定值,而default是指定默认值,因此不能在同一个<element…/>元素中同时使用这两个属性。


对于局部元素而言,它本身就是放在<sequence…/>、<choice…/>或<all…/>元素里定义的,因此自然也属于当前类型。前面all.xsd中定义<book…/>、<game…/>和<sport…/>元素时使用的都是局部元素的语法。

除此之外,Schema还允许为<element…/>元素添加ref属性,以引用一个已有的全局元素,即可将被引用的全局元素变成当前元素的子元素,因此将前面的all.xsd改为如下形式效果不变:

程序清单:codes\05\5.2\all2.xsd

alt

很明显,使用局部元素的形式定义子元素具有更好的内聚性,因为局部子元素只在当前类型内有效;而使用全局元素的形式定义子元素具有更好的可复用性,因为可以在多个类型定义里通过ref重复引用同一个子元素。

与<attribute…/>元素类似的是,在为<element…/>元素增加ref属性时,该属性总是引自一个已有的全局元素定义,因此该<element…/>元素的ref属性不能和name、type两个属性(它们用于定义元素)同时出现。

就笔者的使用经验来看,如果子元素定义比较简单,而且该子元素确实只需要在指定类型内有效,则可以使用局部元素的语法来定义子元素;反之应该考虑使用全局元素语法。

使用<element…/>元素时还可使用minOccurs和maxOccurs两个属性,但只有在如下两种情况下才能为<element…/>元素指定minOccurs和maxOccurs属性:

alt <element…/>元素里指定了ref属性。

alt 采用<element…/>元素定义局部元素时。

minOccurs和maxOccurs属性值分别用于指定该元素允许出现的最小次数和最大次数,它们可以是任何非零整数或者unbounded(不限制最大次数),但minOccurs属性值不能大于maxOccurs属性值。如果没有指定这两个属性,则其默认值都是1,表明该元素必须出现,而且至少出现1次。

如下Schema中使用minOccurs和maxOccurs两个属性增加了对元素出现次数的控制:

程序清单:codes\05\5.2\frequency.xsd

alt

对于上面的Schema文档,如下XML文档是有效的:

程序清单:codes\05\5.2\frequency.xml

alt

除此之外,还可以为<sequence…/>、<choice…/>或<all…/>等元素指定这两个属性,用以表示整个元素组允许出现的次数。特别是当需要指定无序子元素时(<all…/>元素的功能太有限,它所包含的每个无序子元素最多只能出现一次),此时可以通过为<choice…/>元素增加maxOccurs属性来实现。例如希望在<favorite-list…/>元素下包含3个无序子元素,而且3个无序子元素都可出现多次,则可定义如下Schema:

程序清单:codes\05\5.2\noSequence.xsd

alt

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\noSequence.xml

alt

5.2.9 空元素类型

空元素类型用于定义元素内容为空或空字符串的元素,但该元素可以接受属性。

定义空元素类型有两种方式:

alt 扩展长度为0的字符串:如果该元素不需要包含属性,那么直接使用长度为0的字符串类型定义该元素即可。

alt 限制anyType:限制anyType时不定义任何子元素,只定义所需属性即可。

如下Schema采用第1种方式定义了一个空元素类型:

程序清单:codes\05\5.2\empty1.xsd

alt

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\empty1.xml

alt

大部分情况下,Schema更推荐采用第2种方式来定义空元素类型,如下Schema采用第2种方式定义了一个空元素类型:

程序清单:codes\05\5.2\empty2.xsd

alt

很明显,这种方式定义的空元素类型要简单得多,因此实际应用中大多采用这种方式来定义空元素类型(如果该空元素类型不包含属性,则<complexType…/>元素内连<attribute…/>子元素都不需要)。

5.2.10 混合内容类型

Schema中定义混合内容类型非常简单,只需为<complexType…/>元素增加mixed="true"即可。当然,该元素包含的子元素还需要使用嵌套定义。下面的Schema中定义了一个混合内容的元素:

程序清单:codes\05\5.2\mixed.xsd

alt

对于上面的Schema,如下XML文档是有效的:

程序清单:codes\05\5.2\mixed.xml

alt

与DTD中的混合内容类型不同的是:Schema中的混合内容类型约束更严谨。由于在<complexType…/>元素内使用了<sequence…/>子元素来规定各子元素必须按指定顺序出现,因此<book…/>元素内允许出现字符串内容,而且字符串可以出现在任意位置,但<name…/>元素必须出现在<price…/>元素之前。


alt注意

DTD中的混合内容类型里各子元素之间总是无序的,DTD无法为子元素关系提供更细致的约束。但Schema中的混合内容类型里各元素间依然可以提供有序、互斥和无序等进一步的语义约束。