在XSLT 2.0及之后的版本里,xsl:function指令提供了在样式表内部定义具名函数的能力,这类函数可以像内置XPath函数一样参与表达式计算。要使用它,必须为样式表声明一个自定义命名空间,因为XSLT函数必须要有一个命名空间前缀,直接写未加前缀的函数名会被认为是系统内置函数,找不到时处理器会报错。函数通过name属性指定全名,返回值使用xsl:sequence或xsl:value-of输出,同时可以用as属性声明返回类型,让错误在早期暴露出来。

一、基础定义:命名空间与函数签名
先创建一个完整的样式表文件。根元素xsl:stylesheet上需要声明一个非XSLT命名空间,这个命名空间就是自定义函数的归属空间。常见的做法是使用公司域名或项目路径作为URI,但这里为了避免域名冲突,示例统一使用http://ipipp.com/my-functions。exclude-result-prefixes属性用于阻止该前缀出现在输出结果中,避免污染最终文档。
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="http://ipipp.com/my-functions"
exclude-result-prefixes="my">
<xsl:function name="my:add" as="xs:integer">
<xsl:param name="a" as="xs:integer"/>
<xsl:param name="b" as="xs:integer"/>
<xsl:sequence select="$a + $b"/>
</xsl:function>
</xsl:stylesheet>
这里的my:add是函数全名,my对应前面声明的命名空间前缀,add是本地名称。as属性写在xsl:function上表示返回值类型,xs:integer说明这个函数返回一个整数。函数体里面用xsl:param声明了两个入参,也都通过as限定为整数类型。函数最后用xsl:sequence返回表达式计算结果。这里的xsl:sequence与xsl:value-of不同,它保留原子值类型而不会自动转成文本节点,因此更适合在函数内部返回计算值。
调用时,可以在任何允许XPath表达式的地方使用my:add(3, 5)。比如在模板输出中写<xsl:value-of select="my:add(3, 5)"/>,结果会输出8。如果传入字符串参数,Saxon等处理器会在编译期或运行期抛出类型错误,因为参数类型已经限定为xs:integer。
二、函数返回规则与空序列处理
函数返回值的类型并不仅限于原子类型。as属性支持任意序列类型,例如xs:string、xs:date、element()、node()*或item()*。如果不写as属性,默认返回item()*,也就是可以返回任意项目序列。返回节点时,即使节点来自输入文档,函数也会按值返回该节点引用,不会复制节点。只有通过xsl:document或xsl:result-document创建新的结果树节点时,节点才属于新的文档。
空序列是一个容易忽略的情况。如果函数体在某些条件下没有匹配到任何xsl:sequence输出,返回值就是空序列。如果声明了as="xs:string"而实际返回空序列,XSLT 2.0会判定类型不匹配,因为xs:string不允许空序列。这时候需要将返回类型改成xs:string?来表示零个或一个字符串。问号后缀在XPath类型系统中表示可选,星号表示零个或多个,加号表示一个或多个。示例:
<xsl:function name="my:first-word" as="xs:string?">
<xsl:param name="text" as="xs:string"/>
<xsl:variable name="parts" select="tokenize($text, '\s+')"/>
<xsl:sequence select="if (exists($parts)) then $parts[1] else ()"/>
</xsl:function>
这个函数把传入字符串按空白拆分,返回第一个单词。如果输入为空串,tokenize返回空序列,函数通过if判断exists($parts)来决定返回第一个元素还是空序列。因为返回类型写的是xs:string?,空序列被允许。这种处理方式在数据清洗、日志解析等场景中很常见。
还有一个技巧:函数内部可以创建局部变量保存中间结果,避免重复计算。XSLT 2.0的变量是不可变的,一旦绑定就不能修改,这一点在递归函数里尤其重要,因为它会迫使你把状态变化转化为递归参数传递。
三、递归函数:终止条件与尾调用
xsl:function支持递归调用,这是处理不定长度结构或数学序列时的有效手段。递归函数必须在某个分支上返回固定值,否则会无限循环直至栈溢出。编写递归时通常把终止条件放在前面,再写递推分支。
下面看一个计算字符串长度的函数,这里不使用内置string-length,而是用递归逐字符累加,用来演示递归写法。
<xsl:function name="my:str-len" as="xs:integer">
<xsl:param name="s" as="xs:string"/>
<xsl:choose>
<xsl:when test="$s = ''">
<xsl:sequence select="0"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="1 + my:str-len(substring($s, 2))"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
这个例子里,空字符串返回0,否则去掉第一个字符继续递归,并把结果加一。每次调用都会缩小字符串规模,因此一定能够到达终止条件。不过实际项目中不建议重复造内置函数的轮子,这里只是为了展示递归模式。类似地,阶乘、斐波那契数列、目录树遍历、嵌套元素深度计算都可以用xsl:function递归实现。
性能方面,Saxon企业版对尾调用有优化,但并不是所有递归都能被优化成循环。如果递归调用出现在表达式中间,比如1 + my:str-len(...),它就属于非尾调用,每次递归都需要保留调用栈。处理大规模数据时,应优先考虑使用XPath 3.0/3.1提供的fold-left、fold-right等函数式折叠操作,或者把递归改写成迭代式模板。
四、函数库的模块化与复用
当多个样式表都要使用同一批自定义函数时,可以把函数集中放在一个独立的XSLT模块中,然后通过xsl:include或xsl:import引入。函数库文件通常以xsl:stylesheet作为根元素,只包含xsl:function定义,不包含匹配模板。主样式表引用后,所有使用该命名空间前缀的函数调用都能正常解析。
下面是一个独立函数库的简化示例:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:str="http://ipipp.com/string-utils"
exclude-result-prefixes="str">
<xsl:function name="str:left" as="xs:string">
<xsl:param name="s" as="xs:string"/>
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="substring($s, 1, $n)"/>
</xsl:function>
<xsl:function name="str:right" as="xs:string">
<xsl:param name="s" as="xs:string"/>
<xsl:param name="n" as="xs:integer"/>
<xsl:sequence select="substring($s, string-length($s) - $n + 1)"/>
</xsl:function>
</xsl:stylesheet>
主样式表中使用<xsl:include href="string-utils.xsl"/>引入即可。使用xsl:include时,被包含模块的函数定义与主样式表处于同一优先级;使用xsl:import时,主样式表中的同名函数定义会覆盖导入模块中的定义。这一点和模板规则类似,但函数名解析时更严格,同一个命名空间和本地名不能出现冲突,否则编译阶段就会报重复函数定义错误。
调试自定义函数时,可以在函数体内临时加入xsl:message,输出参数值与中间变量。把消息标记为terminate="no",处理器会把信息发送到标准错误输出,而不会中断转换。Saxon的命令行参数explain或trace也能帮助查看函数调用序列,适合定位递归逻辑中的错误。
五、与命名模板的对比及选型建议
从表面看,xsl:function和带name属性的xsl:template都能封装复用逻辑,但二者在设计目标和调用方式上有明显区别。xsl:function属于XPath函数体系,只能返回序列,不能直接向结果树写入元素或属性。而命名模板的执行上下文仍然在结果树构造流程中,模板可以通过xsl:element、xsl:attribute、xsl:text等指令输出节点。
如果业务逻辑是生成一段固定格式的XML片段,比如拼装一个配置节点、格式化输出列表项,使用命名模板更直观。如果业务逻辑是计算某个数值、提取字符串、判断日期范围,这些计算本质上是纯函数,使用xsl:function更合适,因为函数返回值可以直接参与XPath运算,不需要像模板那样通过变量中转。
以字符串处理为例,假设需要在一段文本中提取第一个数字并转换为整数。若用模板,需要先得到字符串结果,再利用XPath转换;若用函数,可以写成一条表达式:my:parse-int(my:first-number(text))。这种嵌套调用让逻辑更紧凑,也方便单元测试。XSLT 2.0及以上版本中,函数还能与path表达式、谓词组合使用,例如item[my:score(.) > 60],让数据筛选逻辑更简洁。
选择处理器时也要注意XSLT版本支持。Saxon HE/PE/EE从9.x起支持XSLT 2.0,Saxon 9.8起支持XSLT 3.0,XmlPrime和Altova RaptorXML也都支持xsl:function。处理器的版本差异会影响某些高级类型和序列处理的可用性,因此生产环境最好先确认版本兼容性。
XSLTxsl:function自定义函数修改时间:2026-09-30 04:53:10