表达式¶
表达式求值为一组值,并具有类型。
例如,表达式 1 + 2
求值为整数 3
,表达式 "QL"
求值为字符串 "QL"
。 1 + 2
的 类型 为 int
,而 "QL"
的类型为 string
。
以下部分描述了 QL 中可用的表达式。
变量引用¶
变量引用是已声明 变量 的名称。这种表达式的类型与它引用的变量相同。
例如,如果你已 声明 变量 int i
和 LocalScopeVariable lsv
,那么表达式 i
和 lsv
的类型分别为 int
和 LocalScopeVariable
。
你还可以引用变量 this
和 result
。这些用于 谓词 定义,并且与其他变量引用以相同的方式起作用。
字面量¶
你可以在 QL 中直接表示某些值,例如数字、布尔值和字符串。
布尔值 字面量:它们的值为
true
和false
。整数 字面量:它们是十进制数字(
0
到9
)的序列,可能以减号(-
)开头。例如0 42 -2048
浮点数 字面量:它们是十进制数字的序列,由点(
.
)分隔,可能以减号(-
)开头。例如2.0 123.456 -100.5
字符串 字面量:它们是 16 位字符的有限字符串。你可以通过将字符括在引号(
"..."
)中来定义字符串字面量。大多数字符表示它们自己,但有一些字符需要使用反斜杠进行“转义”。以下是字符串字面量的示例"hello" "They said, \"Please escape quotation marks!\""
有关详细信息,请参阅 QL 语言规范中的 字符串字面量。
注意:QL 中没有“日期字面量”。相反,要指定 日期,应使用
toDate()
谓词将字符串转换为它表示的日期。例如,"2016-04-03".toDate()
是 2016 年 4 月 3 日的日期,"2000-01-01 00:00:01".toDate()
是 2000 年新年后一秒的时间点。- 以下字符串格式被识别为日期
- ISO 日期,例如
"2016-04-03 17:00:24"
。秒部分是可选的(如果缺少,则假定为"00"
),整个时间部分也可以省略(在这种情况下,假定为"00:00:00"
)。 - 简写 ISO 日期,例如
"20160403"
。 - 英国式日期,例如
"03/04/2016"
。 - 详细日期,例如
"03 April 2016"
。
- ISO 日期,例如
圆括号表达式¶
圆括号表达式是由圆括号 (
和 )
包围的表达式。该表达式的类型和值与原始表达式完全相同。圆括号用于将表达式组合在一起以消除歧义并提高可读性。
范围¶
范围表达式表示在两个表达式之间排序的值范围。它由两个用 ..
分隔的表达式组成,并用方括号([
和 ]
)括起来。例如,[3 .. 7]
是一个有效的范围表达式。它的值是 3
和 7
之间的任何整数(包括 3
和 7
本身)。
在有效的范围内,开始和结束表达式是整数、浮点数或日期。如果其中一个是日期,则两者都必须是日期。如果其中一个是整数,另一个是浮点数,则两者都被视为浮点数。
集合字面量表达式¶
集合字面量表达式允许显式列出对几个值的选项。它由用方括号([
和 ]
)括起来的用逗号分隔的表达式集合组成。例如,[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
是一个有效的集合字面量表达式。它的值是前十个素数。
为了使集合字面量表达式有效,包含的表达式的值需要是 兼容类型。此外,集合元素中至少有一个必须是所有其他包含表达式类型的超类型。
超级表达式¶
QL 中的超级表达式类似于其他编程语言(如 Java)中的超级表达式。当你想使用来自超类型的谓词定义时,你可以在谓词调用中使用它们。在实践中,当谓词从其超类型继承两个定义时,这很有用。在这种情况下,谓词必须 覆盖 这些定义以避免歧义。但是,如果你想使用来自特定超类型的定义而不是编写新的定义,你可以使用超级表达式。
在以下示例中,类 C
继承了谓词 getANumber()
的两个定义——一个来自 A
,一个来自 B
。它没有覆盖这两个定义,而是使用了来自 B
的定义。
class A extends int {
A() { this = 1 }
int getANumber() { result = 2 }
}
class B extends int {
B() { this = 1 }
int getANumber() { result = 3 }
}
class C extends A, B {
// Need to define `int getANumber()`; otherwise it would be ambiguous
override int getANumber() {
result = B.super.getANumber()
}
}
from C c
select c, c.getANumber()
此查询的结果为 1, 3
。
对谓词的调用(带结果)¶
对 带结果的谓词 的调用本身就是表达式,与对 不带结果的谓词 的调用不同,后者是公式。有关详细信息,请参阅“对谓词的调用。”
对带结果谓词的调用求值为调用谓词的 result
变量的值。
例如 a.getAChild()
是对变量 a
上的谓词 getAChild()
的调用。此调用求值为 a
的子集。
聚合¶
聚合是一种映射,它从由公式指定的输入值集中计算结果值。
通用语法为
<aggregate>(<variable declarations> | <formula> | <expression>)
在 <variable declarations>
中 声明 的变量称为 **聚合变量**。
排序的聚合(即 min
、max
、rank
、concat
和 strictconcat
)默认按它们的 <expression>
值排序。排序要么是数字的(对于整数和浮点数),要么是词典的(对于字符串)。词典排序基于每个字符的 Unicode 值。
要指定其他排序,请在 <expression>
后面加上关键字 order by
,然后是指定排序的一个或多个用逗号分隔的表达式,并且可选地在每个表达式后面加上关键字 asc
或 desc
(以确定是按升序还是降序对表达式排序)。如果你没有指定排序,它默认为 asc
。例如,order by o.getName() asc, o.getSize() desc
可用于按名称对某些对象排序,并按降序大小来解决平局。
QL 中提供以下聚合
count
: 此聚合函数用于确定每个可能的聚合变量分配下,<expression>
的不同值的数量。例如,以下聚合返回具有超过
500
行的文件数量。count(File f | f.getTotalNumberOfLines() > 500 | f)
如果没有可能的聚合变量分配满足公式,例如
count(int i | i = 1 and i = 2 | i)
,则count
默认值为0
。
min
和max
: 这些聚合函数用于确定所有可能的聚合变量分配中,<expression>
的最小值 (min
) 或最大值 (max
)。<expression>
必须是数值类型或字符串类型,或者使用order by
定义显式排序。使用order by
时,如果出现并列情况,可能存在多个结果。例如,以下聚合使用代码行数来解决并列情况,返回具有最大代码行数的
.js
文件(或文件)的名称。max(File f | f.getExtension() = "js" | f.getBaseName() order by f.getTotalNumberOfLines(), f.getNumberOfLinesOfCode())
以下聚合返回下面提到的三个字符串中,最小的字符串
s
,即在所有可能的s
值的字典顺序中排在最前的字符串。(在本例中,它返回"De Morgan"
。)min(string s | s = "Tarski" or s = "Dedekind" or s = "De Morgan" | s)
avg
: 此聚合函数用于确定所有可能的聚合变量分配下,<expression>
的平均值。<expression>
的类型必须是数值类型。如果没有可能的聚合变量分配满足公式,则聚合失败,不返回任何值。换句话说,它将评估为空集。例如,以下聚合返回整数
0
、1
、2
和3
的平均值。avg(int i | i = [0 .. 3] | i)
sum
: 此聚合函数用于确定所有可能的聚合变量分配下,<expression>
的值的总和。<expression>
的类型必须是数值类型。如果没有可能的聚合变量分配满足公式,则总和为0
。例如,以下聚合返回
i * j
的总和,其中i
和j
可以取所有可能的取值。sum(int i, int j | i = [0 .. 2] and j = [3 .. 5] | i * j)
concat
: 此聚合函数用于将所有可能的聚合变量分配下,<expression>
的值连接在一起。请注意,<expression>
必须是字符串类型。如果没有可能的聚合变量分配满足公式,则concat
默认为空字符串。例如,以下聚合返回字符串
"3210"
,即字符串"0"
、"1"
、"2"
和"3"
按降序连接的结果。concat(int i | i = [0 .. 3] | i.toString() order by i desc)
concat
聚合函数还可以接收第二个表达式,用逗号与第一个表达式隔开。此第二个表达式将作为每个连接值之间的分隔符插入。例如,以下聚合返回
"0|1|2|3"
。concat(int i | i = [0 .. 3] | i.toString(), "|")
rank
: 此聚合函数用于对<expression>
的所有可能取值进行排名。<expression>
必须是数值类型或字符串类型,或者使用order by
定义显式排序。聚合函数返回排名在 **排名表达式** 指定位置的值。您必须在关键字rank
后使用方括号包含此排名表达式。使用order by
时,如果出现并列情况,可能存在多个结果。例如,以下聚合返回在所有可能的取值中排名第 4 的值。在本例中,
8
是从5
到15
的范围内,排名第 4 的整数。rank[4](int i | i = [5 .. 15] | i)
注意
- 排名索引从
1
开始,因此rank[0](...)
没有结果。 rank[1](...)
等同于min(...)
。
- 排名索引从
strictconcat
、strictcount
和strictsum
: 这些聚合函数分别与concat
、count
和sum
功能类似,但它们是 **严格的**。也就是说,如果没有可能的聚合变量分配满足公式,则整个聚合将失败并评估为空集(而不是默认为0
或空字符串)。这在您只对聚合主体非平凡的结果感兴趣时非常有用。
unique
: 此聚合函数取决于所有可能的聚合变量分配下,<expression>
的值。如果在聚合变量上,<expression>
存在唯一值,则聚合函数将评估为该值。否则,聚合函数将没有值。例如,以下查询返回正整数
1
、2
、3
、4
、5
。对于负整数x
,表达式x
和x.abs()
的值不同,因此聚合表达式中y
的值无法唯一确定。from int x where x in [-5 .. 5] and x != 0 select unique(int y | y = x or y = x.abs() | y)
聚合函数的评估¶
一般来说,聚合函数评估涉及以下步骤:
- 确定输入变量:这些变量是
<variable declarations>
中声明的聚合变量,以及在聚合函数外部声明并在聚合函数的某个组件中使用的变量。 - 生成所有可能的输入变量值的不同元组(组合),使得
<formula>
成立。请注意,聚合变量的相同值可能出现在多个不同的元组中。在处理元组时,相同值的相同出现将被视为不同的出现。 - 将
<expression>
应用于每个元组,并收集生成的(不同)值。将<expression>
应用于元组可能会生成多个值。 - 将聚合函数应用于步骤 3 中生成的价值,计算最终结果。
让我们将这些步骤应用于以下查询中的 sum
聚合函数。
select sum(int i, int j |
exists(string s | s = "hello".charAt(i)) and exists(string s | s = "world!".charAt(j)) | i)
输入变量:
i
、j
。所有可能的满足给定条件的元组
(<value of i>, <value of j>)
:(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (0, 5), (1, 0), (1, 1), ..., (4, 5)
。在这一步中生成了 30 个元组。
将
<expression> i
应用于所有元组。这意味着从所有元组中选择所有i
的值:0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4.
将聚合函数
sum
应用于上述值,得到最终结果60
。
如果我们将上述查询中的 <expression>
更改为 i + j
,则查询结果为 135
,因为将 i + j
应用于所有元组会得到以下值:0, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 7, 3, 4, 5, 6, 7, 8, 4, 5, 6, 7, 8, 9
。
接下来,考虑以下查询
select count(string s | s = "hello" | s.charAt(_))
s
是聚合的输入变量。- 在此步骤中将生成一个单一元组
"hello"
。 - 将
<expression> charAt(_)
应用于此元组。charAt(_)
中的下划线_
是一个 无关表达式,表示任何值。s.charAt(_)
将生成四个不同的值h, e, l, o
。 - 最后,将
count
应用于这些值,查询返回4
。
省略聚合的部分¶
聚合的三个部分并不总是必需的,因此您通常可以以更简单的形式编写聚合。
如果您想编写形如
<aggregate>(<type> v | <expression> = v | v)
的聚合,则可以省略<variable declarations>
和<formula>
部分,并按如下方式编写<aggregate>(<expression>)
例如,以下聚合确定字母
l
在字符串"hello"
中出现的次数。这些形式是等效的。count(int i | i = "hello".indexOf("l") | i) count("hello".indexOf("l"))
如果只有一个聚合变量,则可以省略
<expression>
部分。在这种情况下,表达式被认为是聚合变量本身。例如,以下聚合是等效的。avg(int i | i = [0 .. 3] | i) avg(int i | i = [0 .. 3])
作为特例,即使有多个聚合变量,也可以从
count
中省略<expression>
部分。在这种情况下,它将计算满足公式的聚合变量的不同元组的数量。换句话说,表达式部分被认为是常量1
。例如,以下聚合是等效的。count(int i, int j | i in [1 .. 3] and j in [1 .. 3] | 1) count(int i, int j | i in [1 .. 3] and j in [1 .. 3])
您可以省略
<formula>
部分,但在这种情况下,您应该包含两个竖线。<aggregate>(<variable declarations> | | <expression>)
如果您不想进一步限制聚合变量,这很有用。例如,以下聚合返回所有文件中的最大行数。
max(File f | | f.getTotalNumberOfLines())
最后,您还可以同时省略
<formula>
和<expression>
部分。例如,以下聚合是计算数据库中文件数量的等效方法。count(File f | any() | 1) count(File f | | 1) count(File f)
单调聚合¶
除了标准聚合之外,QL 还支持单调聚合。单调聚合与标准聚合的区别在于它们处理公式的 <expression>
部分生成的值的方式。
- 标准聚合将每个
<formula>
值的<expression>
值扁平化为一个列表。单个聚合函数将应用于所有值。 - 单调聚合为
<formula>
给出的每个值获取一个<expression>
,并创建所有可能值的组合。聚合函数将应用于每个生成的组合。
通常,如果 <expression>
是总的和函数的,那么单调聚合等效于标准聚合。当每个由 <formula>
生成的值没有精确地一个 <expression>
值时,结果会不同。
- 如果缺少
<expression>
值(即,对于由<formula>
生成的值,没有<expression>
值),单调聚合将无法计算结果,因为您无法创建包括每个由<formula>
生成的值中精确地一个<expression>
值的组合。 - 如果每个
<formula>
结果有多个<expression>
,则您可以创建包括每个由<formula>
生成的值中精确地一个<expression>
值的多个值的组合。在此,聚合函数将应用于每个生成的组合。
单调聚合的示例¶
考虑此查询
string getPerson() { result = "Alice" or
result = "Bob" or
result = "Charles" or
result = "Diane"
}
string getFruit(string p) { p = "Alice" and result = "Orange" or
p = "Alice" and result = "Apple" or
p = "Bob" and result = "Apple" or
p = "Charles" and result = "Apple" or
p = "Charles" and result = "Banana"
}
int getPrice(string f) { f = "Apple" and result = 100 or
f = "Orange" and result = 100 or
f = "Orange" and result = 1
}
predicate nonmono(string p, int cost) {
p = getPerson() and cost = sum(string f | f = getFruit(p) | getPrice(f))
}
language[monotonicAggregates]
predicate mono(string p, int cost) {
p = getPerson() and cost = sum(string f | f = getFruit(p) | getPrice(f))
}
from string variant, string person, int cost
where variant = "default" and nonmono(person, cost) or
variant = "monotonic" and mono(person, cost)
select variant, person, cost
order by variant, person
该查询将生成以下结果。
变体 | 人员 | 成本 |
---|---|---|
默认值 | 爱丽丝 | 201 |
默认值 | 鲍勃 | 100 |
默认值 | 查尔斯 | 100 |
默认值 | 黛安 | 0 |
单调的 | 爱丽丝 | 101 |
单调的 | 爱丽丝 | 200 |
单调的 | 鲍勃 | 100 |
单调的 | 黛安 | 0 |
当 getPrice(f)
对给定的 f
具有多个结果或没有结果时,聚合语义的两个变体将有所不同。
在此查询中,橙子以两种不同的价格出售,默认的 sum
聚合将返回一行,其中爱丽丝以 100 的价格购买一个橙子,以 1 的价格购买另一个橙子,并以 100 的价格购买一个苹果,总计 201。另一方面,在 sum
的 *单调* 语义中,爱丽丝始终购买一个橙子和一个苹果,并且将为她完成购物清单的每种 *方式* 生成一行输出。
如果苹果也有两种不同的价格,那么单调的 sum
将为爱丽丝生成 *四* 行输出。
查尔斯想买一个香蕉,但根本没有出售。在默认情况下,为查尔斯生成的总和包括他 *可以* 购买的苹果的成本,但在单调的 sum
输出中没有查尔斯的行,因为查尔斯 *没有办法* 购买一个苹果加一个香蕉。
(黛安根本没有购买水果,在两种变体中她的总成本均为 0。 strictsum
聚合将在两种情况下都将她排除在结果之外。)
在实际的 QL 应用中,使用单调聚合以 *达到* 生成多行输出的目的(如本例中的“爱丽丝”情况)是很少见的。更重要的点是“查尔斯”情况:只要没有香蕉的价格,就不会为他生成输出。这意味着如果我们以后得知香蕉的价格,我们不需要 *删除* 已经生成的任何输出元组。这很重要,因为单调聚合行为与递归的固定点语义相得益彰,因此让 getPrice
谓词与计数聚合本身相互递归将是有意义的。(另一方面,getFruit
仍然不能递归,因为向某人的购物清单中添加另一种水果将使我们已经知道的总成本无效。)
这种使用递归的机会是请求聚合单调语义的主要实际原因。
递归单调聚合¶
单调聚合可能 递归 使用,但递归调用只能出现在表达式中,而不能出现在范围内。聚合的递归语义与 QL 其他部分的递归语义相同。例如,我们可以定义一个谓词来计算图中节点与叶节点的距离,如下所示
language[monotonicAggregates]
int depth(Node n) {
if not exists(n.getAChild())
then result = 0
else result = 1 + max(Node child | child = n.getAChild() | depth(child))
}
这里的递归调用位于表达式中,这是合法的。聚合的递归语义与 QL 其他部分的递归语义相同。如果您理解了非递归情况下聚合的工作原理,那么您应该不会发现递归使用它们很困难。但是,了解递归聚合的评估过程是值得的。
考虑一下我们刚刚看到的深度示例,其输入为以下图(箭头从子节点指向父节点)
那么 depth
谓词的评估将按如下方式进行。
阶段 | 深度 | 注释 |
---|---|---|
0 | 我们始终从空集开始。 | |
1 | (0, b), (0, d), (0, e) |
没有子节点的节点的深度为 0。a 和 c 的递归步骤无法生成值,因为它们的一些子节点没有 depth 的值。 |
2 | (0, b), (0, d), (0, e), (1, c) |
c 的递归步骤成功,因为 depth 现在对它的所有子节点(d 和 e)都有值。a 的递归步骤仍然失败。 |
3 | (0, b), (0, d), (0, e), (1, c), (2, a) |
a 的递归步骤成功,因为 depth 现在对它的所有子节点(b 和 c)都有值。 |
在这里,我们可以看到,在中间阶段,如果一些子节点缺少值,那么聚合失败非常重要 - 这将防止添加错误的值。
任何¶
any
表达式的通用语法类似于 聚合 的语法,即
any(<variable declarations> | <formula> | <expression>)
您应该始终包含 变量声明,但 公式 和 表达式 部分是可选的。
any
表达式表示任何具有特定形式并满足特定条件的值。更准确地说,any
表达式
- 引入临时变量。
- 将它们的值限制为满足
<formula>
部分(如果存在)的值。 - 为这些变量中的每一个返回
<expression>
。如果没有<expression>
部分,则返回变量本身。
下表列出了一些不同形式的 any
表达式的示例
表达式 | 值 |
---|---|
any(File f) |
数据库中的所有 File |
any(Element e | e.getName()) |
数据库中所有 Element 的名称 |
any(int i | i = [0 .. 3]) |
整数 0 、1 、2 和 3 |
any(int i | i = [0 .. 3] | i * i) |
整数 0 、1 、4 和 9 |
注意
还有一个 内置谓词
any()
。这是一个始终成立的谓词。
一元运算符¶
一元运算符是一个减号 (-
) 或一个加号 (+
),后面跟着一个类型为 int
或 float
的表达式。例如
-6.28
+(10 - 4)
+avg(float f | f = 3.4 or f = -9.8)
-sum(int i | i in [0 .. 9] | i * i)
加号保持表达式的值不变,而减号取值的算术否定。
二元运算符¶
二元运算符由一个表达式、一个二元运算符和另一个表达式组成。例如
5 % 2
(9 + 1) / (-2)
"Q" + "L"
2 * min(float f | f in [-3 .. 3])
您可以在 QL 中使用以下二元运算符
名称 | 符号 |
---|---|
加法/连接 | + |
乘法 | * |
除法 | / |
减法 | - |
模运算 | % |
如果两个表达式都是数字,这些运算符将充当标准算术运算符。例如,10.6 - 3.2
的值为 7.4
,123.456 * 0
的值为 0
,而 9 % 4
的值为 1
(将 9
除以 4
后的余数)。如果两个操作数都是整数,则结果为整数。否则结果为浮点数。
您也可以使用 +
作为字符串连接运算符。在这种情况下,至少一个表达式必须是字符串——另一个表达式将使用 toString()
谓词隐式转换为字符串。这两个表达式将被连接起来,结果是一个字符串。例如,表达式 221 + "B"
的值为 "221B"
。
强制类型转换¶
强制类型转换允许您约束表达式的 类型。这类似于其他语言中的强制类型转换,例如在 Java 中。
- 您可以通过两种方式编写强制类型转换
- 作为“后缀”强制类型转换:一个点,后面跟着括号中的类型名称。例如,
x.(Foo)
将x
的类型限制为Foo
。 - 作为“前缀”强制类型转换:括号中的类型,后面跟着另一个表达式。例如,
(Foo)x
也将x
的类型限制为Foo
。
- 作为“后缀”强制类型转换:一个点,后面跟着括号中的类型名称。例如,
请注意,后缀强制类型转换等效于用括号括起来的前缀强制类型转换——x.(Foo)
完全等效于 ((Foo)x)
。
如果您想调用仅针对更具体类型定义的 成员谓词,强制类型转换很有用。例如,以下查询选择具有名为“List”的直接超类型的 Java 类
import java
from Type t
where t.(Class).getASupertype().hasName("List")
select t
由于谓词 getASupertype()
是为 Class
定义的,但不是为 Type
定义的,因此您不能直接调用 t.getASupertype()
。强制类型转换 t.(Class)
确保 t
的类型为 Class
,因此它可以访问所需的谓词。
如果您更喜欢使用前缀强制类型转换,可以将 where
部分改写为
where ((Class)t).getASupertype().hasName("List")