表达式

优质
小牛编辑
146浏览
2023-12-01

当需要给插值或者指令参数提供值时,可以使用变量或其他复杂的表达式。 例如,我们设x为8,y为5,那么 (x + y)/2 的值就会被处理成数字类型的值6.5。

在我们展开细节之前,先来看一些具体的例子:

  • 当给插值提供值时:插值的使用方式为 ${expression}, 把它放到你想输出文本的位置上,然后给值就可以打印出来了。 即 ${(5 + 8)/2} 会打印出 ''6.5'' 来 (如果输出的语言不是美国英语,也可能打印出''6,5''来)。

  • 当给指令参数提供值时:在入门章节我们已经看到 if 指令的使用了。这个指令的语法是:<#if expression>...</#if>。 这里的表达式计算结果必须是布尔类型的。比如 <#if 2 < 3> 中的 2 <3 (2小于3)是结果为 true 的布尔表达式。

快速浏览(备忘单)

这里给已经了解 FreeMarker 的人或有经验的程序员的提个醒:

请参考: 运算符优先级

直接确定值

通常我们喜欢是使用直接确定的值而不是计算的结果。

字符串

在文本中确定字符串值的方法是看双引号,比如: "some text",或单引号,比如: 'some text'。这两种形式是等同的。 如果文本自身包含用于字符引用的引号 ( "')或反斜杠时, 应该在它们的前面再加一个反斜杠;这就是转义。 转义允许直接在文本中输入任何字符, 也包括换行。例如:

${"It's \"quoted\" and
this is a backslash: \\"}

${'It\'s "quoted" and
this is a backslash: \\'}

将会输出:

It's "quoted" and
this is a backslash: \

It's "quoted" and
this is a backslash: \
Note:

这里当然可以直接在模板中输入文本而不需要 ${...}。 但是我们在这里用它只是为了示例来说明表达式的使用。

下面的表格是FreeMarker支持的所有转义字符。 在字符串使用反斜杠的其他所有情况都是错误的,运行这样的模板都会失败。

转义序列 含义
\" 引号 (u0022)
\' 单引号(又称为撇号) (u0027)
\{ 起始花括号:{
\\ 反斜杠 (u005C)
\n 换行符 (u000A)
\r 回车 (u000D)
\t 水平制表符(又称为tab) (u0009)
\b 退格 (u0008)
\f 换页 (u000C)
\l 小于号:<
\g 大于号:>
\a &符:&
\xCode 字符的16进制 Unicode 码 (UCS 码)

\x 之后的 Code 是1-4位的16进制码。下面这个示例中都是在字符串中放置版权符号: "\xA9 1999-2001""\x0A9 1999-2001""\x00A9 1999-2001"。 如果紧跟16进制码后一位的字符也能解释成16进制码时, 就必须把4位补全,否则FreeMarker就会误解你的意图。

请注意,字符序列 ${ (和 #{) 有特殊的含义,它们被用做插入表达式的数值(典型的应用是变量的值: "Hello ${user}!")。这将在 后续章节中解释。 如果想要打印 ${#{, 就要使用下面所说的原生字符串,或者进行转义。就像 "foo $\{bar}"中的 {

原生字符串是一种特殊的字符串。在原生字符串中, 反斜杠和 ${ 没有特殊含义, 它们被视为普通的字符。为了表明字符串是原生字符串, 在开始的引号或单引号之前放置字母r,例如:

${r"${foo}"}
${r"C:\foo\bar"}

将会输出:

${foo}
C:\foo\bar

数字

输入不带引号的数字就可以直接指定一个数字, 必须使用点作为小数的分隔符而不能是其他的分组分隔符。 可以使用 -+ 来表明符号 (+ 是多余的)。 科学记数法暂不支持使用 (1E3 就是错误的), 而且也不能在小数点之前不写0(.5 也是错误的)。

下面的数字都是合法的:0.08-5.013800811+11

请注意,像 08+88.008 这样的数值是完全等同的,它们都是数字8。 所以, ${08}${+8}${8.00}${8} 的输出都是一样的。

布尔值

直接写 true 或者 false 就表示一个布尔值了,不需使用引号。

序列

指定一个文字的序列,使用逗号来分隔其中的每个 子变量, 然后把整个列表放到方括号中。例如:

<#list ["foo", "bar", "baz"] as x>
${x}
</#list>

将会输出:

foo
bar
baz
 

列表中的项目是表达式,那么也可以这样做: [2 + 2, [1, 2, 3, 4], "foo"]。 其中第一个子变量是数字4,第二个子变量是一个序列, 第三个子变量是字符串"foo"。

值域

值域也是序列,但它们由指定包含的数字范围所创建, 而不需指定序列中每一项。比如: 0..<m,这里假定 m 变量的值是5,那么这个序列就包含 [0, 1, 2, 3, 4]。值域的主要作用有:使用 <#list...> 来迭代一定范围内的数字,序列切分字符串切分

值域表达式的通用形式是( startend 可以是任意的结果为数字表达式):

  • start..end: 包含结尾的值域。比如 1..4 就是 [1, 2, 3, 4], 而 4..1 就是 [4, 3, 2, 1]。当心一点, 包含结尾的值域不会是一个空序列,所以 0..length-1 就是 错误的,因为当长度是 0 时, 序列就成了 [0, -1]

  • start..<endstart..!end: 不包含结尾的值域。比如 1..<4 就是 [1, 2, 3]4..<1 就是 [4, 3, 2], 而 1..<1 表示 []。请注意最后一个示例; 结果可以是空序列,和 ..<..! 没有区别; 最后这种形式在应用程序中使用了 < 字符而引发问题(如HTML编辑器等)。

  • start..*length: 限定长度的值域,比如 10..*4 就是 [10, 11, 12, 13]10..*-4 就是 [10, 9, 8, 7],而 10..*0 表示 []。当这些值域被用来切分时, 如果切分后的序列或者字符串结尾在指定值域长度之前,则切分不会有问题;请参考 序列切分 来获取更多信息。

    Note:

    限定长度的值域是在 FreeMarker 2.3.21版本中引入的。

  • start..: 无右边界值域。这和限制长度的值域很像,只是长度是无限的。 比如 1.. 就是 [1, 2, 3, 4, 5, 6, ... ],直到无穷大。 但是处理(比如列表显示)这种值域时要万分小心,处理所有项时, 会花费很长时间,直到内存溢出应用程序崩溃。 和限定长度的值域一样,当它们被切分时, 遇到切分后的序列或字符串结尾时,切分就结束了。

    Warning!

    无右边界值域在 FreeMarker 2.3.21 版本以前只能用于切分, 若用于其它用途,它就像空序列一样了。要使用新的特性, 使用 FreeMarker 2.3.21 版本是不够的,程序员要设置 incompatible_improvements 至少到2.3.21版本。

值域的进一步注意事项:

  • 值域表达式本身并没有方括号,比如这样编写代码 <#assign myRange = 0..<x>, 而不是 <#assign myRange = [0..<x]>。 后者会创建一个包含值域的序列。方括号是切分语法的一部分,就像 seq[myRange]

  • 可以在 .. 的两侧编写算术表达式而不需要圆括号, 就像 n + 1 ..< m / 2 - 1

  • ....<..!..* 是运算符, 所以它们中间不能有空格。就像 n .. <m 这样是错误的,但是 n ..< m 这样就可以。

  • 无右边界值域的定义大小是2147483647 (如果 incompatible_improvements 低于2.3.21版本,那么就是0), 这是由于技术上的限制(32位)。但当列表显示它们的时候,实际的长度是无穷大。

  • 值域并不存储它们包含的数字,那么对于 0..10..100000000 来说,创建速度都是一样的, 并且占用的内存也是一样的。

哈希表

在模板中指定一个哈希表,就可以遍历用逗号分隔开的"键/值"对, 把列表放到花括号内即可。键和值成对出现并以冒号分隔。比如: { "name": "green mouse", "price": 150 }。 请注意名和值都是表达式,但是用来检索的名称就必须是字符串类型, 而值可以是任意类型。

检索变量

顶层变量

访问顶层的变量,可以简单地使用变量名。例如, 用表达式 user 就可以在根上获取以 "user" 为名存储的变量值。然后打印出存储在里面的内容:

${user}

如果没有顶层变量,那么 FreeMarker 在处理表达式时就会发生错误, 进而终止模板的执行(除非程序员事先配置了 FreeMarker)。

在这种表达式中,变量名只可以包含字母(也可以是非拉丁文), 数字(也可以是非拉丁数字),下划线 (_), 美元符号 ($),at符号 (@)。 此外,第一个字符不可以是ASCII码数字(0-9)。 从 FreeMarker 2.3.22 版本开始,变量名在任何位置也可以包含负号 (-),点(.)和冒号(:), 但这些必须使用前置的反斜杠(\)来转义, 否则它们将被解释成操作符。比如,读取名为"data-id"的变量, 表达式为 data\-id,因为 data-id 将被解释成 "data minus id"。 (请注意,这些转义仅在标识符中起作用,而不是字符串中。)

从哈希表中检索数据

如果有一个表达式的结果是哈希表, 那么我们可以使用点和子变量的名字得到它的值, 假设我们有如下的数据模型:

(root)
 |
 +- book
 |   |
 |   +- title = "Breeding green mouses"
 |   |
 |   +- author
 |       |
 |       +- name = "Julia Smith"
 |       |
 |       +- info = "Biologist, 1923-1985, Canada"
 |
 +- test = "title"

现在,就可以通过book.title 来读取 title,book表达式将返回一个哈希表 (就像上一章中解释的那样)。按这种逻辑进一步来说,我们可以使用表达式 book.author.name 来读取到auther的name。

如果我们想指定同一个表达式的子变量,那么还有另外一种语法格式: book["title"]。在方括号中可以给出任意长度字符串的表达式。 在上面这个数据模型示例中还可以这么来获取title: book[test]。 下面这些示例它们含义都是相等的: book.author.namebook["author"].namebook.author.["name"]book["author"]["name"]

当使用点式语法时,顶层变量名的命名也有相同的限制 (命名时只能使用字母,数字,_$@,但是不能使用 0-9开头, 同时,从2.3.22版本开始,也可以使用 \-\.\:)。当使用方括号语法时,则没有这样的限制, 因为名称可以是任意表达式的结果。(请注意,对于FreeMarker的XML支持来说, 如果子变量名称是 * (星号) 或者 **,那么就不要使用方括号语法。)

对于顶层变量来说,如果尝试访问一个不存在的变量也会引起错误导致解析执行模板中断 (除非程序员事先配置过FreeMarker)。

从序列中检索数据

这和从哈希表中检索是相同的,但是只能使用方括号语法形式来进行, 而且方括号内的表达式最终必须是一个数字而不是字符串。比如,要从 示例数据模型 中获取第一个动物的名字 (记住第一项数字索引是0而不是1),可以这么来写: animals[0].name

特殊变量

特殊变量是由FreeMarker引擎本身定义的。 使用它们,可以按照如下语法形式来进行: .variable_name。.

通常情况下是不需使用特殊变量,而对专业用户来说可能用到。 所有特殊变量的说明可以参见 参考手册

字符串操作

插值 (或连接)

如果要在字符串中插入表达式的值,可以在字符串的文字中使用 ${...} (已经废弃的 #{...})。 ${...} 在字符串中的作用和在 文本 区是相同的 (它遵守相同的 本地化敏感 的数字和日期/时间格式), 而不是 自动转义

示例 (假设user是 ''Big Joe''):

<#assign s = "Hello ${user}!">
${s} <#-- Just to see what the value of s is -->

将会输出:

Hello Big Joe!
Warning!

用户所犯的一个常见错误是将插值放在了不需要/不应该使用的地方。 插值 文本 区 中有效。(比如, <h1>Hello ${name}!</h1>) 还有在字符串值中 (比如, <#include "/footer/${company}.html">)。 典型的 错误 使用是 <#if ${big}>...</#if>, 这会导致语法错误。简单写为 <#if big>...</#if>即可。 而且, <#if "${big}">...</#if> 也是 错误的, 因为它将参数值转换为字符串,但是 if 指令只接受布尔值, 那么这将导致运行时错误。

另外,也可以使用 + 号来达到类似的效果:

<#assign s = "Hello " + user + "!">

这样的效果和使用 ${...} 是一样的。

Warning!

因为 + 和使用 ${...} 的规则相同,附加的字符串受到 localenumber_formatdate_formattime_formatdatetime_formatboolean_format 等等设置的影响, 这是对人来说的,而不是通常机器的解析。默认情况下,这会导致数字出问题, 因为很多地区使用分组(千分位分隔符),那么 "someUrl?id=" + id 就可能会是 "someUrl?id=1 234"。 要预防这种事情的发生,请使用 ?c (对计算机来说)内建函数,那么在 "someUrl?id=" + id?c"someUrl?id=${id?c}"中, 就会得到如 "someUrl?id=1234" 这样的输出, 而不管本地化和格式的设置是什么。

获取字符

在给定索引值时可以获取字符串中的一个字符,这和 序列的子变量是相似的, 比如 user[0]。这个操作执行的结果是一个长度为1的字符串, FTL并没有独立的字符类型。和序列中的子变量一样,这个索引也必须是数字, 范围是从0到字符串的长度,否则模板的执行将会发生错误并终止。

由于序列的子变量语法和字符的getter语法冲突, 那么只能在变量不是序列时使用字符的getter语法(因为FTL支持多类型值,所以它是可能的), 这种情况下使用序列方式就比较多。(为了变通,可以使用 内建函数 string ,比如 user?string[0]。不必担心你不理解这是什么意思, 内建函数将会在后续章节中讨论。)

示例(假设 user 是 "Big Joe"):

${user[0]}
${user[4]}

将会输出(请注意第一个字符的索引是0):

B
J

字符串切分 (子串)

可以按照 切分序列 (请参看)的相同方式来切分字符串,这就是使用字符来代替序列。不同的是:

  • 降序域不允许进行字符串切分。 (因为不像序列那样,很少情况下会想反转字符串。 如果真要这样做了,那就是疏忽。)

  • 如果变量的值既是字符串又是序列(多类型值), 那么切分将会对序列进行,而不是字符串。当处理XML时, 这样的值就是普通的了。此时,可以使用 someXMLnode?string[range]

  • 一个遗留的bug:值域 包含 结尾时, 结尾小于开始索引并且是是非负的(就像在 "abc"[1..0] 中), 会返回空字符串而不是错误。(在降序域中这应该是个错误。) 现在这个bug已经向后兼容,但是不应该使用它,否在就会埋下一个错误。

示例:

<#assign s = "ABCDEF">
${s[2..3]}
${s[2..<4]}
${s[2..*3]}
${s[2..*100]}
${s[2..]}

将会输出:

CD
CD
CDE
CDEF
CDEF
Note:

下面包括了一些使用内建函数来方便字符串切分的典型用例: remove_beginningremove_endingkeep_beforekeep_afterkeep_before_lastkeep_after_last

序列操作

连接

序列的连接可以按照字符串那样使用 + 号来进行,例如:

<#list ["Joe", "Fred"] + ["Julia", "Kate"] as user>
- ${user}
</#list>

将会输出:

- Joe
- Fred
- Julia
- Kate
 

请注意,不要在很多重复连接时使用序列连接操作, 比如在循环中往序列上追加项目,而这样的使用是可以的: <#list users + admins as person>。 尽管序列连接的速度很快,而且速度是和被连接序列的大小相独立的, 但是最终的结果序列的读取却比原先的两个序列慢那么一点。 通过这种方式进行的许多重复连接最终产生的序列读取的速度会慢。

序列切分

使用 seq[range], 这里 range 是一个值域 此处有说明, 就可以得到序列的一个切分。结果序列会包含原序列 (seq)中的项, 而且索引在值域中。例如:

<#assert seq = ["A", "B", "C", "D", "E"]>
<#list seq[1..3] as i>${i}</#list>

将会输出:

BCD 

此外,切分后序列中的项会和值域的顺序相同。 那么上面的示例中,如果值域是 3..1 将会输出 DCB

值域中的数字必须是序列可使用的合法索引, 否则模板的处理将会终止并报错。像上面的示例那样, seq[-1..0] 就会出错, 而 seq[-1] 就是合法的。 seq[1..5] 也不对, 因为 seq[5] 是非法的。 (请注意,尽管100已经越界,但是 seq[100..<100]seq[100..*0] 是合法的,因为那些值域都是空。)

限制长度的值域 (start..*length) 和无右边界值域 (start..) 适用于切分后序列的长度。它们会切分可用项中尽可能多的部分:

<#assign seq = ["A", "B", "C"]>

Slicing with length limited ranges:
- <#list seq[0..*2] as i>${i}</#list>
- <#list seq[1..*2] as i>${i}</#list>
- <#list seq[2..*2] as i>${i}</#list> <#-- Not an error -->
- <#list seq[3..*2] as i>${i}</#list> <#-- Not an error -->

Slicing with right-unlimited ranges:
- <#list seq[0..] as i>${i}</#list>
- <#list seq[1..] as i>${i}</#list>
- <#list seq[2..] as i>${i}</#list>
- <#list seq[3..] as i>${i}</#list>

将会输出:

Slicing with length limited ranges:
- AB
- BC
- C
-

Slicing with right-unlimited ranges:
- ABC
- BC
- C
-

请注意,上面的有限长度切分和无右边界切分都允许开始索引超过最后项 一个 (但不能再多了)。

Note:

要对序列进行给定大小的切分,就应该使用内建函数 chunk

哈希表操作

连接

像连接字符串那样,也可以使用 + 号的方式来连接哈希表。如果两个哈希表含有键相同的项,那么在 + 号右侧的哈希表中的项优先。例如:

<#assign ages = {"Joe":23, "Fred":25} + {"Joe":30, "Julia":18}>
- Joe is ${ages.Joe}
- Fred is ${ages.Fred}
- Julia is ${ages.Julia}

将会输出:

- Joe is 30
- Fred is 25
- Julia is 18

请注意,很多项连接时不要使用哈希表连接, 比如在循环时往哈希表中添加新项。这和序列连接 的情况是一致的。

算数运算

算数运算包含基本的四则运算和求模运算,运算符有:

  • 加法: +
  • 减法: -
  • 乘法: *
  • 除法: /
  • 求模 (求余): %

示例:

${100 - x * x}
${x / 2}
${12 % 10}

假设 x 是 5,将会输出:

75
2.5
2

要保证两个操作数都是结果为数字的表达式。 下面的这个例子在运行时,FreeMarker就会发生错误, 因为是字符串 "5" 而不是数字5:

${3 * "5"} <#-- WRONG! -->

但这种情况也有一个例外,就是 + 号,它是用来 连接字符串的。 如果 + 号的一端是字符串,+ 号的另外一端是数字,那么数字就会自动转换为字符串类型(使用当前页面语言的适当格式), 之后使用 + 号作为字符串连接操作符。示例如下:

${3 + "5"}

将会输出:

35

通常来说,FreeMarker不会自动将字符串转换为数字,反之会自动进行。

有时我们只想获取除法计算(或其它运算)的整数部分, 这可以使用内建函数 int 来解决。(关于内建函数 后续章节会来解释):

${(x/2)?int}
${1.1?int}
${1.999?int}
${-1.1?int}
${-1.999?int}

假设 x 是 5,将会输出:

2
1
1
-1
-1

比较运算

有时我们需要知道两个值是否相等,或者哪个值更大一点。

为了演示具体的例子,我们在这里使用 if 指令。 if 指令的用法是: <#if expression>...</#if>, 其中的表达式的值必须是布尔类型,否则将会出错,模板执行中断。 如果表达式的结果是 true , 那么在开始和结束标记内的内容将会被执行,否则就会被跳过。

测试两个值相等使用 = (或者采用Java和C语言中的 == ;二者是完全等同的。) 测试两个值不等使用 !=。比如, 假设 user 是 ''Big Joe'':

<#if user == "Big Joe">
  It is Big Joe
</#if>
<#if user != "Big Joe">
  It is not Big Joe
</#if>

<#if ...> 中的表达式 user = "Big Joe" 就是布尔值 true,面的代码将会输出 ''It is Big Joe''。

=!= 两边的表达式的结果都必须是标量,而且两个标量都必须是相同类型 (也就是说字符串只能和字符串来比较,数字只能和数字来比较等)否则将会出错, 模板执行中断。例如 <#if 1 = "1"> 就会导致错误。 请注意FreeMarker进行的是精确的比较,所以字符串在比较时要注意大小写和空格: "x""x ""X" 是不同的值。

对数字和日期类型的比较,也可以使用 <<=>=>。不能把它们当作字符串来比较。比如:

<#if x <= 12>
  x is less or equivalent with 12
</#if>

使用 >=> 的时候有一点小问题。FreeMarker解释 > 的时候可以把它当作FTL标签的结束符。为了避免这种问题,可以使用 lt 代替 <lte 代替 <=gt 代替 > 还有 gte 代替 >=, 例如 <#if x gt y>。另外一个技巧是将表达式放到 圆括号 中, 尽管这么写并不优雅,例如 <#if (x > y)>

Note:

FreeMarker 也支持一些其它的选择,但是这些已经废弃了:

  • 在可能出问题的关系标记处使用 &gt;&lt; ,就像: <#if x &gt; y><#if x &gt;= y>。 请注意通常FTL不支持标签中的实体引用(如 &... 这些东西); 做算术比较时就会有异常。

  • \lt\lte\gt\gte 使用他们时, 不带反斜杠的效果一样。

逻辑操作

常用的逻辑操作符:

  • 逻辑 或: ||
  • 逻辑 与: &&
  • 逻辑 非: !

逻辑操作符仅仅在布尔值之间有效,若用在其他类型将会产生错误导致模板执行中止。

例如:

<#if x < 12 && color == "green">
  We have less than 12 things, and they are green.
</#if>
<#if !hot> <#-- here hot must be a boolean -->
  It's not hot.
</#if>

内建函数

内建函数就像FreeMarker在对象中添加的方法一样。 要防止和实际方法和其它子变量的命名冲突,则不能使用点 (.),这里使用问号 (?)来和父对象分隔开。 比如,想要保证 path 有起始的 / ,那么可以这么来写: path?ensure_starts_with('/')path 后的Java对象(通常就是 String) 并没有这样的方法,这是FreeMarker添加的。为了简洁,如果方法没有参数, 那么就可以忽略 (),比如想要获取 path 的长度,就可以写作:path?length而不是 path?length()

内建函数关键性的另外一个原因是常见的(尽管它依赖于配置的设置), FreeMarker不会暴露对象的Java API。那么尽管Java的 String 类有 length() 方法,但在模板中却是不可见的, 就 不得不 使用 path?length 来代替。 这里的优点是模板不依赖下层Java对象的精确类型。(比如某些场景中, path 也可能是 java.nio.Path 类型, 如果程序员配置了FreeMarker去暴露 Path 对象作为FTL字符串类型,那么模板就不会在意了,使用 ?length 也是可以的, 即便 java.nio.Path 没有类似的方法。)

可以找到一些 此处提及的常用内建函数,还有 完整的内建函数参考。 现在,我们只需了解一些重要的内建函数就行了:

比如:

${testString?upper_case}
${testString?html}
${testString?upper_case?html}

${testSequence?size}
${testSequence?join(", ")}

假设 testString 中存储了字符串 ''Tom & Jerry'', 而testSequnce中存储了字符串 "foo", "bar" 和 "baz", 将会输出:

TOM & JERRY
Tom &amp; Jerry
TOM &amp; JERRY

3
foo, bar, baz

请注意:上面的 testString?upper_case?html。因为 test?upper_case 的结果是字符串,那么就可以在它的上面 使用内建函数 html

很自然可以看到,内建函数的左侧可以是任意的表达式,而不仅仅是变量名:

${testSeqence[1]?cap_first}
${"horse"?cap_first}
${(testString + " & Duck")?html}
Bar
Horse
Tom &amp; Jerry &amp; Duck

方法调用

如果有一个方法,那么可以使用方法调用操作。 方法调用操作是使用逗号来分割在括号内的表达式而形成参数列表,这些值就是参数。 方法调用操作将这些值传递给方法,然后返回一个结果。 这个结果就是整个方法调用表达式的值。

假设程序员定义了一个可供调用的方法 repeat。 第一个参数是字符串类型,第二个参数是数字类型。方法的返回值是字符串类型, 而方法要完成的操作是将第一个参数重复显示,显示的次数是第二个参数设定的值。

${repeat("Foo", 3)}

将会输出:

FooFooFoo

这里的 repeat 就是方法变量(根据如何 访问顶层变量), ("What", 3) 就调用了该方法。

这里需要强调方法调用也是普通表达式,和其它都是一样的,所以:

${repeat(repeat("x", 2), 3) + repeat("Foo", 4)?upper_case}

将会输出:

xxxxxxFOOFOOFOOFOO

处理不存在的值

Note:

这些操作符是从 FreeMarker 2.3.7 版本开始引入的(用来代替内建函数 defaultexistsif_exists )。

正如我们前面解释的那样,当试图访问一个不存在的变量时, FreeMarker 将会报错而导致模板执行中断。 通常我们可以使用两个特殊操作符来压制这个错误,控制这种错误情况。 被控制的变量可以是顶层变量,哈希表或序列的子变量。 此外这些操作符还能处理方法调用的返回值不存在的情况 (这点对Java程序员来说: 返回值是 null 或者返回值为 void 类型),通常来说,我们应该使用这些操作符来控制可能不存在的值, 而不仅仅是不存在的变量。

对于知道Java中 null 的人来说,FreeMarker 2.3.x 版本把它们视为不存在的变量。单地说,模板语言中没有 null 这个概念。比如有一个bean,bean中有一个 maidenName 属性, 对于模板而言(假设没有配置FreeMarker来使用一些极端的对象包装), 该属性的值是 null,和不存在这个属性的情况是一致的。 调用方法的返回值如果是 null 的话 FreeMarker 也会把它当作不存在的变量来处理 (假定只使用了普通的对象包装)。可以在 FAQ 中了解更多内容。

Note:

如果想知道为什么 FreeMarker 对不存在的变量如此挑剔, 请阅读 FAQ 部分。

默认值操作符

使用形式: unsafe_expr!default_exprunsafe_expr! or (unsafe_expr)!default_expr(unsafe_expr)!

这个操作符允许你为可能不存在的变量指定一个默认值。

例如,假设下面展示的代码中没有名为 mouse 的变量:

${mouse!"No mouse."}
<#assign mouse="Jerry">
${mouse!"No mouse."}

将会输出:

No mouse.
Jerry

默认值可以是任何类型的表达式,也可以不必是字符串。 也可以这么写:hits!0colors!["red", "green", "blue"]。 默认值表达式的复杂程度没有严格限制,还可以这么来写: cargo.weight!(item.weight * itemCount + 10)

Warning!

如果在 !后面有复合表达式, 如 1 + x通常 使用括号,如 ${x!(1 + y)}${(x!1) + y)},这样就根据你的意图来确定优先级。 由于FreeMarker 2.3.x 版本的源码中的小失误所以必须这么来做。 ! (作为默认值操作) 右侧的优先级非常低。 这就意味着 ${x!1 + y} 会被 FreeMarker 误解为 ${x!(1 + y)},而真实的意义是 ${(x!1) + y}。 这个源码错误在FreeMarker 2.4中会得到修正。 在编程中注意这个错误,要么就使用FreeMarker 2.4!

如果默认值被省略了,那么结果将会是空串,空序列或空哈希表。 (这是 FreeMarker 允许多类型值的体现)请注意,如果想让默认值为 0false,则不能省略它。例如:

(${mouse!})
<#assign mouse = "Jerry">
(${mouse!})

将会输出:

()
(Jerry)
Warning!

因为语法的含糊 <@something a=x! b=y /> 将会解释为 <@something a=x!(b=y) />,那就是说 b=y 将会被视为是比较运算,然后结果作为 x的默认值,而不是想要的参数 b。 为了避免这种情况,如下编写代码即可: <@something a=(x!) b=y />

用于非顶层变量时,默认值操作符可以有两种使用方式:

product.color!"red"

如果是这样的写法,那么在 product 中, 当 color 不存在时(返回 "red" ), 将会被处理,但是如果连 product 都不存在时将不会处理。 也就是说这样写时变量 product 必须存在,否则模板就会报错。

(product.color)!"red"

这时,如果当 product.color 不存在时也会被处理, 那就是说,如果 product 不存在或者 product 存在而 color 不存在,都能显示默认值 "red" 而不会报错。 本例和上例写法的重要区别在于用括号时, 就允许其中表达式的任意部分可以未定义。而没有括号时, 仅允许表达式的最后部分可以不被定义。

当然,默认值操作也可以作用于序列子变量,比如:

<#assign seq = ['a', 'b']>
${seq[0]!'-'}
${seq[1]!'-'}
${seq[2]!'-'}
${seq[3]!'-'}

将会输出:

a
b
-
-

如果序列索引是负数(比如 seq[-1]!'-') 也会发生错误,不能使用该运算符或者其它运算符去压制它。

不存在值检测操作符

使用形式: unsafe_expr??(unsafe_expr)??

这个操作符告诉我们一个值是否存在。基于这种情况, 结果是 truefalse

示例如下,假设并没有名为 mouse 的变量:

<#if mouse??>
  Mouse found
<#else>
  No mouse found
</#if>
Creating mouse...
<#assign mouse = "Jerry">
<#if mouse??>
  Mouse found
<#else>
  No mouse found
</#if>

将会输出:

  No mouse found
Creating mouse...
  Mouse found

访问非顶层变量的使用规则和默认值操作符也是一样的, 也就是说,可以写 product.color??(product.color)??

赋值操作符

这些并不是表达式,只是复制指令语法的一部分,比如 assign, localglobal。 照这样,它们不能任意被使用。

<#assign x += y><#assign x = x + y> 的简写,<#assign x *= y><#assign x = x * y>的简写等等。。。

<#assign x++><#assign x += 1> (或 <#assign x = x + 1>)不同,它只做算术加法运算 (如果变量不是数字的话就会失败),而其它的是进行字符串,序列连接和哈希表连接的重载。 <#assign x--><#assign x -= 1> 的简写。

括号

括号可以用来给任意表达式分组。示例如下:

                               <#-- Output will be: -->
${3 * 2 + 2}                   <#-- 8 -->
${3 * (2 + 2)}                 <#-- 12 -->
${3 * ((2 + 2) * (1 / 2))}     <#-- 6 -->
${"green " + "mouse"?upper_case}    <#-- green MOUSE -->
${("green " + "mouse")?upper_case}  <#-- GREEN MOUSE -->
<#if !(color == "red" || color == "green")>
  The color is nor red nor green
</#if>

请注意,方法调用表达式 使用的括号和给表达式分组的括号含义是完全不同的。

表达式中的空格

FTL 忽略表达式中的多余的 空格。下面的表示是相同的:

${x + ":" + book.title?upper_case}

${x+":"+book.title?upper_case}

${
   x
 + ":"   +  book   .   title
   ?   upper_case
      }

操作符的优先级

下面的表格显示了已定义操作符的优先级。 表格中的运算符按照优先程度降序排列:上面的操作符优先级高于它下面的。 高优先级的运算符执行要先于优先级比它低的。表格同一行上的两个操作符优先级相同。 当有相同优先级的二元运算符(运算符有两个''参数'',比如 +-)挨着出现时,它们按照从左到右的原则运算。

运算符组 运算符
最高优先级运算符 [subvarName] [subStringRange] . ? (methodParams) expr! expr??
一元前缀运算符 +expr -expr !expr
乘除法,求模运算符 * / %
加减法运算符 + -
数字值域 .. ..< ..! ..*
关系运算符 < > <= >= (and equivalents: gt, lt, etc.)
相等,不等运算符 == != (and equivalents: =)
逻辑 "与" 运算符 &&
逻辑 "或" 运算符 ||

如果你熟悉C语言,Java语言或JavaScript语言, 请注意 FreeMarker 中的优先级规则和它们是相同的, 除了那些只有FTL本身含有的操作符。

因为编程的失误,默认值操作符 (exp!exp) 不在上面的表格中,按照向后兼容的原则,在 FreeMarker 2.4 版本中将会修正它。 而且它将是最高优先级的运算符,但是在 FreeMarker 2.3.x 版本中它右边的优先级由于失误就非常低。 所以在默认值操作符的右边中使用复杂表达式时可以使用括号, 可以是 x!(y + 1) 或者是 (x!y) + 1。 而不能是 x!y + 1