本介绍了 R 语言,解释了评估、解析、面向对象编程、语言计算等内容。
本手册适用于 R 版本 4.3.3 (2024-02-29)。
版权 © 2000–2023 R 核心团队
允许制作和分发本手册的逐字副本,前提是在所有副本上保留版权声明和本许可声明。
允许在逐字复制的条件下复制和分发本手册的修改版本,前提是整个生成的衍生作品在与本许可声明相同的许可声明条款下分发。
允许在上述修改版本的条件下复制和分发本手册的翻译版本,但本许可声明可以由 R 核心团队批准的翻译版本代替。
R 是一个用于统计计算和图形的系统。它提供了编程语言、高级图形、与其他语言的接口以及调试工具等功能。本手册详细定义了 R 语言。
R 语言是 S 的一种方言,S 是在 1980 年代设计的一种语言,自那时起就在统计学界广泛使用。其主要设计者 John M. Chambers 因 S 而获得了 1998 年 ACM 软件系统奖。
该语言语法与 C 语言表面上相似,但语义属于 FPL(函数式编程语言)类型,与 Lisp 和 APL 具有更强的亲缘关系。特别是,它允许“在语言上进行计算”,这反过来使得编写以表达式作为输入的函数成为可能,这对于统计建模和图形来说通常很有用。
可以通过交互式使用 R 来实现很多功能,从命令行执行 简单表达式。一些用户可能永远不需要超越这个级别,而另一些用户则希望编写自己的函数,无论是为了系统化重复性工作,还是为了编写新的功能的附加包。
本手册的目的是记录语言本身 per se。也就是说,它所操作的对象,以及表达式求值过程的细节,这些在编写 R 函数时很有用。本手册仅简要描述了用于特定任务的主要子系统,例如图形,这些子系统将在单独的文档中进行记录。
虽然大部分文本同样适用于 S,但也存在一些实质性差异,为了不混淆问题,我们将集中描述 R。
语言的设计包含许多细微之处和常见的陷阱,可能会让用户感到意外。大多数这些问题都是由于更深层次的一致性考虑,我们将在后面解释。还有一些有用的快捷方式和习惯用法,允许用户简洁地表达相当复杂的运算。一旦熟悉了底层概念,其中许多就会变得自然。在某些情况下,执行任务有多种方法,但其中一些技术将依赖于语言实现,而另一些则在更高的抽象级别上工作。在这种情况下,我们将指出首选用法。
假设您对 R 有所了解。这不是 R 的入门教程,而是一本程序员参考手册。其他手册提供了补充信息:特别是 前言 在 R 语言入门 中介绍了 R,而 系统和外语接口 在 编写 R 扩展 中详细介绍了如何使用编译代码扩展 R。
在每种计算机语言中, 变量提供了一种访问存储在内存中的数据的方法。R 并不提供对计算机内存的直接访问,而是提供了一些专门的数据结构,我们将称之为 对象。这些对象通过符号或变量来引用。然而,在 R 中,符号本身也是对象,可以像其他任何对象一样进行操作。这与许多其他语言不同,并且具有广泛的影响。
在本章中,我们将对 R 提供的各种数据结构进行初步描述。许多数据结构的更详细讨论将在后续章节中找到。R 特定的函数 typeof
返回 R 对象的 类型。请注意,在 R 底层的 C 代码中,所有对象都是指向具有 typedef SEXPREC
的结构的指针;不同的 R 数据类型在 C 中由 SEXPTYPE
表示,它决定了结构中各个部分的信息如何使用。
下表描述了 typeof
返回的可能值及其含义。
用户无法轻易获得标记为“***”类型的对象。
函数 mode
提供有关对象 模式 的信息,其含义与 Becker、Chambers 和 Wilks (1988) 的定义一致,并且与 S 语言的其他实现更兼容。 最后,函数 storage.mode
返回其参数的 存储模式,其含义与 Becker 等人 (1988) 的定义一致。它通常在调用用其他语言(如 C 或 FORTRAN)编写的函数时使用,以确保 R 对象具有被调用例程所期望的数据类型。(在 S 语言中,具有整数或实数值的向量都是 "numeric"
模式,因此需要区分它们的存储模式。)
> x <- 1:3 > typeof(x) [1] "integer" > mode(x) [1] "numeric" > storage.mode(x) [1] "integer"
R 对象在计算过程中经常被强制转换为不同的 类型。还有许多函数可用于执行显式 强制转换。在 R 语言中编程时,对象的类型通常不会影响计算,但是,在处理外语或操作系统时,通常需要确保对象具有正确的类型。
向量可以被认为是包含数据的连续单元。单元通过索引操作(例如 x[5]
)访问。更多细节请参见 索引。
R 有六种基本(“原子”)向量类型:逻辑型、整型、实型、复数型、字符型(在 C 中称为“字符串”)和原始型。不同向量类型的模式和存储模式在以下表格中列出。
typeof mode storage.mode 逻辑型
逻辑型
逻辑型
整型
数值型
整型
双精度型
数值型
双精度型
复数型
复数型
复数型
字符型
字符型
字符型
原始型
原始型
原始型
单个数字,例如 4.2
,以及字符串,例如 "four point two"
仍然是向量,长度为 1;没有更基本类型。长度为零的向量是可能的(并且有用)。
字符向量中的单个元素通常被称为 字符字符串 或简称为 字符串。 1
列表(“通用向量”)是另一种数据存储方式。列表具有元素,每个元素可以包含任何类型的 R 对象,即列表的元素不必是相同类型。列表元素通过三种不同的索引操作访问。这些将在 索引 中详细解释。
列表是向量,基本向量类型被称为 原子向量,在需要排除列表时使用。
构成 R 语言的三种对象类型是 调用、表达式 和 名称。 由于 R 有类型为 "expression"
的对象,因此我们将尽量避免在其他上下文中使用“表达式”一词。特别是,语法上正确的表达式将被称为 语句。
这些对象的模式分别为 "call"
、"expression"
和 "name"
。
可以使用 quote
机制直接从表达式创建它们,并通过 as.list
和 as.call
函数在列表之间进行转换。 可以使用标准索引操作提取 解析树的组件。
符号引用 R 对象。任何 R 对象的 名称通常是一个符号。可以使用函数 as.name
和 quote
创建符号。
符号的模式为 "name"
,存储模式为 "symbol"
,类型为 "symbol"
。它们可以使用 as.character
和 as.name
在字符字符串之间进行 强制转换。 它们自然地作为解析表达式的原子出现,例如尝试 as.list(quote(x + y))
。
在 R 中,可以拥有类型为 "expression"
的对象。一个 表达式 包含一个或多个语句。语句是 符号的语法上正确的集合。 表达式对象是特殊的语言对象,包含已解析但未评估的 R 语句。主要区别在于表达式对象可以包含多个这样的表达式。另一个更细微的差异是,类型为 "expression"
的对象仅在显式传递给 eval
时才 评估,而其他语言对象可能会在一些意外情况下被评估。
一个 表达式对象的行为非常像一个列表,它的组件应该以与列表组件相同的方式访问。
在 R 中,函数是对象,可以像其他任何对象一样进行操作。函数(更准确地说是函数闭包)具有三个基本组成部分:形式参数列表、函数体和 环境。参数列表是一个用逗号分隔的参数列表。一个 参数可以是一个符号,或者一个 ‘symbol = default’ 结构,或者特殊的参数 ...
。参数的第二种形式用于为参数指定默认值。如果调用函数时没有为该参数指定任何值,则将使用此值。 ...
参数是特殊的,可以包含任意数量的参数。它通常用于参数数量未知或参数将传递给另一个函数的情况。
函数体是一个解析过的 R 语句。它通常是一组用大括号括起来的语句,但它也可以是一个单独的语句、一个符号,甚至是一个常量。
函数的 环境是在创建函数时处于活动状态的环境。该环境中绑定的任何符号都会被捕获,并可供函数使用。代码和其环境中的绑定组合在一起称为“函数闭包”,这是函数式编程理论中的一个术语。在本文件中,我们通常使用术语“函数”,但使用“闭包”来强调附加环境的重要性。
可以使用 formals
、body
和 environment
结构来提取和操作闭包对象的三个部分(这三个结构也可以用在 赋值的左侧)。 最后一个结构可以用来删除不需要的环境捕获。
当调用函数时,会创建一个新的环境(称为评估环境),其封闭环境(参见环境)是函数闭包中的环境。这个新环境最初填充了函数的未评估参数;随着评估的进行,局部变量会在其中创建。
还有一种机制可以使用as.list
和as.function
将函数转换为列表结构,反之亦然。 这些机制是为了与 S 保持兼容而引入的,建议不要使用它们。
有一个特殊的对象叫做NULL
。它用于在需要指示或指定对象不存在时使用。它不应与零长度的向量或列表混淆。
NULL
对象没有类型,也没有可修改的属性。R 中只有一个NULL
对象,所有实例都引用它。要测试NULL
,请使用is.null
。你不能在NULL
上设置属性。
这两种对象包含 R 的内置 函数,即在代码列表中显示为.Primitive
的函数(以及通过.Internal
函数访问的函数,因此对用户不可见)。两者之间的区别在于参数处理。内置函数会根据按值调用,将所有参数评估并传递给内部函数,而特殊函数会将未评估的参数传递给内部函数。
从 R 语言的角度来看,这些对象只是另一种类型的函数。is.primitive
函数可以将它们与解释型 函数区分开来。
Promise 对象是 R 语言延迟求值机制的一部分。它们包含三个槽位:一个值、一个表达式和一个环境。当一个 函数被调用时,参数会被匹配,然后每个形式参数会被绑定到一个 Promise。该形式参数的表达式和函数被调用的环境的指针会被存储在 Promise 中。
在该参数被访问之前,与 Promise 关联的值不存在。当参数被访问时,存储的表达式会在存储的环境中被求值,结果会被返回。结果也会被 Promise 保存。substitute
函数会提取表达式槽位的内容。这允许程序员访问与 Promise 关联的值或表达式。
在 R 语言中,Promise 对象几乎只以隐式方式出现:实际的函数参数就是这种类型。还有一个delayedAssign
函数可以将表达式变成 Promise。通常在 R 代码中无法检查一个对象是否是 Promise,也无法使用 R 代码确定 Promise 的环境。
...
对象类型存储为一种配对列表。...
的组件可以通过 C 代码以通常的配对列表方式访问,但...
在解释代码中不容易作为对象访问,甚至不应该假设这种对象的出现,因为这在将来可能会改变。
该对象可以被捕获(强制 Promise!),例如作为列表,所以在table
中可以看到
args <- list(...) ## .... for (a in args) { ## ....
请注意,...
作为配对列表对象的实现不应被视为 R API 的一部分,并且 base R 之外的代码不应依赖于此对 ...
的当前描述。另一方面,上述 list(...)
访问,以及其他“点访问”函数 ...length()
,...elt()
,...names()
,以及“保留字” ..1
,..2
等,另请参见帮助页面 ?dots
,是稳定的 R API 的一部分。
如果一个函数具有 ...
作为形式参数,那么任何与形式参数不匹配的实际参数都将与 ...
匹配。
环境可以被认为由两部分组成。一个帧,由一组符号-值对组成,以及一个封闭,指向封闭环境的指针。当 R 查找符号的值时,会检查帧,如果找到匹配的符号,则会返回其值。如果没有,则会访问封闭环境并重复此过程。环境形成一个树状结构,其中封闭充当父节点。环境树的根是一个空 环境,可以通过 emptyenv()
获取,它没有父节点。它是基本包环境的直接父节点(可以通过 baseenv()
函数获取)。
环境是由函数调用隐式创建的,如函数对象 和词法环境 中所述。在这种情况下,环境包含函数的局部变量(包括参数),其封闭是当前调用函数的环境。环境也可以通过 new.env
直接创建。 环境的帧内容可以通过 ls
,names
,$
,[
,[[
,get
和 get0
访问, 并可以通过 $<-
,[[<-
和 assign
操作, 以及 eval
和 evalq
进行操作。
可以使用 parent.env
函数访问环境的封闭。
与大多数其他 R 对象不同,环境在传递给函数或用于赋值时不会被复制。因此,如果您将同一个环境分配给多个符号并更改其中一个,其他符号也会发生更改。特别是,将属性分配给环境会导致意外情况。
配对列表对象类似于 Lisp 的点对列表。它们在 R 的内部机制中被广泛使用,但在解释代码中很少可见,尽管它们由 formals
返回,并且可以通过(例如)pairlist
函数创建。一个零长度的配对列表是 NULL
,正如在 Lisp 中预期的那样,但与零长度的列表相反。 每个这样的对象都有三个槽,一个 CAR 值,一个 CDR 值和一个 TAG 值。TAG 值是一个文本字符串,CAR 和 CDR 通常分别表示列表项(头部)和列表的剩余部分(尾部),以 NULL 对象作为终止符(CAR/CDR 术语是传统的 Lisp,最初指的是 60 年代早期 IBM 计算机上的地址和递减寄存器)。
在 R 语言中,配对列表的处理方式与通用向量(“列表”)完全相同。特别是,元素使用相同的 [[]]
语法访问。配对列表的使用已过时,因为通用向量通常使用起来更有效率。当从 R 访问内部配对列表时,它通常(包括在子集时)被转换为通用向量。
在极少数情况下,配对列表对用户可见:其中一个是 .Options
。
对象实际上不可能是“Any”类型,但它仍然是一个有效的类型值。它在某些(相当罕见)情况下使用,例如 as.vector(x, "any")
,表示不应进行类型 强制转换。
除 NULL
外的所有对象都可以附加一个或多个属性。属性存储为一个配对列表,其中所有元素都有名称,但应视为一组 name=value 对。可以使用 attributes
获取属性列表,并使用 attributes<-
设置属性, 使用 attr
和 attr<-
访问单个组件。
某些属性具有特殊的访问器 函数(例如,因子使用的 levels<-
),在可用时应使用这些函数。除了隐藏实现细节之外,它们还可以执行其他操作。R 尝试拦截对 attr<-
和 attributes<-
的调用,这些调用涉及特殊属性,并强制执行一致性检查。
矩阵和数组只是带有属性 dim
和可选的 dimnames
附加到向量的向量。
属性用于实现 R 中使用的类结构。如果对象具有 class
属性,则该属性将在 评估期间进行检查。R 中的类结构在面向对象编程 中详细描述。
当存在 names
属性时,它会标记向量或列表的各个元素。当打印对象时,如果存在 names
属性,则会使用它来标记元素。 names
属性也可以用于索引目的,例如,quantile(x)["25%"]
。
可以使用 names
和 names<-
结构来获取和设置名称。 后者将执行必要的 一致性检查,以确保 names 属性具有正确的类型和长度。
配对列表和一维数组的处理方式有所不同。对于配对列表对象,使用虚拟的 names
属性;names
属性实际上是从列表组件的标签构建的。对于一维数组,names
属性实际上访问 dimnames[[1]]
。
dim
属性用于实现数组。数组的内容存储在按列优先顺序排列的向量中,dim
属性是一个整数向量,指定数组的各个维度的范围。R 确保向量的长度是所有维度长度的乘积。一个或多个维度的长度可以为零。
向量与一维数组不同,因为后者有一个长度为一的 dim
属性,而前者没有 dim
属性。
R 具有一个复杂的类系统2,主要通过 class
属性控制。此属性是一个字符向量,包含对象继承的类列表。这构成了 R 中“泛型方法”功能的基础。
用户可以不受限制地访问和操作此属性。没有检查对象是否实际包含类方法期望的组件。因此,更改class
属性应谨慎操作,并且在可用时,应优先使用特定的创建和强制转换函数。
当对象被修改时是否应该复制属性是一个复杂的问题,但有一些通用的规则(Becker、Chambers & Wilks,1988,第 144–6 页)。
标量函数(那些对向量逐元素操作且输出类似于输入的函数)应该保留属性(除了可能类的属性)。
二元运算通常从较长的参数复制大多数属性(如果它们长度相同,则优先考虑第一个参数的值)。这里“大多数”是指除了names
、dim
和dimnames
之外的所有属性,这些属性由运算符的代码适当地设置。
子集(除了空索引之外)通常会删除所有属性,除了names
、dim
和dimnames
,这些属性会根据需要重置。另一方面,子赋值通常会保留属性,即使长度发生变化。强制转换会删除所有属性。
排序的默认方法会删除所有属性,除了名称,名称会与对象一起排序。
因素用于描述具有有限数量值的项目(性别、社会阶层等)。因素具有 levels
属性和 "factor"
类。可选地,它还可以包含一个 contrasts
属性,该属性控制在因素用于 建模函数时使用的参数化。
因素可以是纯粹名义的,也可以具有有序类别。在后一种情况下,它应该被定义为有序的,并具有 class
向量 c("ordered"," factor")
。
因素目前使用整数数组来指定实际级别,以及第二个名称数组来映射到整数。不幸的是,用户经常利用这种实现来简化一些计算。然而,这是一个实现问题,不能保证在所有 R 实现中都适用。
数据框是 R 结构,它最接近于 SAS 或 SPSS 数据集,即“案例按变量”数据矩阵。
数据框是向量、因素和/或矩阵的列表,它们都具有相同的长度(在矩阵的情况下为行数)。此外,数据框通常具有 names
属性来标记变量,以及 row.names
属性来标记案例。
数据框可以包含一个与其他组件长度相同的列表。该列表可以包含不同长度的元素,从而提供一个用于不规则数组的数据结构。但是,截至目前,此类数组通常无法正确处理。
当用户在提示符处输入命令(或从文件中读取表达式)时,首先发生的事情是命令被解析器转换为内部表示形式。评估器执行解析后的 R 表达式并返回表达式的值。所有表达式都有一个值。这是语言的核心。
本章描述了评估器的基本机制,但避免讨论特定函数或函数组,这些将在后面的单独章节中介绍,或者帮助页面应该提供足够的文档。
用户可以构建表达式并调用评估器来执行它们。
在提示符处直接输入的任何数字都是常量,并且会被评估。
> 1 [1] 1
也许出乎意料的是,从表达式 1
返回的数字是数值。在大多数情况下,整数和数值之间的区别并不重要,因为 R 在使用数字时会做正确的事情。但是,有时我们希望显式地为常量创建整数。我们可以通过调用函数 as.integer
或使用其他各种技术来做到这一点。但也许最简单的方法是用后缀字符“L”来限定我们的常量。例如,要创建整数 1,我们可以使用
> 1L [1]
我们可以使用“L”后缀来限定任何数字,目的是使其成为显式整数。因此,“0x10L”从十六进制表示创建整数 16。常量 1e3L
将 1000 作为整数而不是数值,等效于 1000L
。(请注意,“L”被视为限定项 1e3
而不是 3
。)如果我们用“L”限定一个不是整数的值,例如 1e-3L
,我们会得到一个警告,并且会创建数值。如果数字中存在不必要的十进制点,例如 1.L
,也会创建警告。
当对复数使用“L”时,我们会得到语法错误,例如 12iL
会产生错误。
常量相当无聊,要进行更多操作,我们需要符号。
当创建一个新变量时,它必须有一个名称以便可以引用它,并且它通常有一个值。名称本身是一个符号。当一个符号被评估时,它的值将被返回。稍后我们将详细解释如何确定与符号关联的值。
在这个小例子中,y
是一个符号,它的值为 4。符号也是一个 R 对象,但很少需要直接处理符号,除非在进行“语言编程” (语言编程) 时。
> y <- 4 > y [1] 4
R 中执行的大多数计算都涉及函数的评估。我们也将其称为函数调用。函数通过名称调用,并带有一系列用逗号分隔的参数。
> mean(1:10) [1] 5.5
在这个例子中,函数 mean
被调用,带有一个参数,即从 1 到 10 的整数向量。
R 包含大量具有不同用途的函数。大多数用于生成结果,结果是一个 R 对象,但其他函数用于其副作用,例如打印和绘图函数。
函数调用可以具有标记(或命名)参数,如 plot(x, y, pch = 3)
。没有标记的参数被称为位置参数,因为函数必须从它们在调用参数中的顺序位置来区分它们的含义,例如,x
表示横坐标变量,y
表示纵坐标变量。对于具有大量可选参数的函数,使用标记/名称是一个明显的便利。
> class(x) <- "foo"
这种构造实际上是调用函数 class<-
,并带有一个原始对象和右侧。此函数执行对象的修改并返回结果,然后将结果存储回原始变量中。(至少在概念上,这就是发生的事情。一些额外的努力是为了避免不必要的重复数据。)
R 允许使用类似于 C 编程语言的运算符来进行算术表达式,例如
> 1 + 2 [1] 3
表达式可以使用括号进行分组,与函数调用混合,并以直接的方式分配给变量
> y <- 2 * (a + log(x))
R 包含许多运算符。它们列在下面的表格中。
-
减号,可以是一元或二元 +
加号,可以是一元或二元 !
一元非 ~
波浪号,用于模型公式,可以是一元或二元 ?
帮助 :
序列,二元(在模型公式中:交互) *
乘法,二元 /
除法,二元 ^
指数运算,二元 %x%
特殊二元运算符,x 可以替换为任何有效的名称 %%
模运算,二元 %/%
整数除法,二元 %*%
矩阵乘积,二元 %o%
外积,二元 %x%
克罗内克积,二元 %in%
匹配运算符,二元(在模型公式中:嵌套) <
小于,二元 >
大于,二元 ==
等于,二元 >=
大于或等于,二元 <=
小于或等于,二元 &
与,二元,向量化 &&
与,二元,非向量化 |
或,二元,向量化 ||
或,二元,非向量化 <-
左赋值,二元 ->
右赋值,二元 $
列表子集,二元
除了语法之外,应用运算符和调用函数之间没有区别。实际上,x + y
可以等效地写成 `+`(x, y)
。请注意,由于“+”是非标准函数名,因此需要用引号括起来。
R 一次处理整个数据向量,大多数基本运算符和基本数学函数(如 log
)都是向量化的(如上表所示)。这意味着例如,添加两个相同长度的向量将创建一个包含元素级总和的向量,隐式地循环遍历向量索引。这也适用于其他运算符,如 -
、*
和 /
,以及更高维度的结构。特别要注意,将两个矩阵相乘不会产生通常的矩阵乘积(%*%
运算符用于此目的)。与向量化运算相关的某些细节将在 基本算术运算 中讨论。
要访问原子向量的单个元素,通常使用 x[i]
结构。
> x <- rnorm(5) > x [1] -0.12526937 -0.27961154 -1.03718717 -0.08156527 1.37167090 > x[2] [1] -0.2796115
列表组件更常使用 x$a
或 x[[i]]
访问。
> x <- options() > x$prompt [1] "> "
与其他运算符一样,索引实际上是由函数完成的,可以使用 `[`(x, 2)
代替 x[2]
。
R 的索引操作包含许多高级功能,将在 索引 中进一步描述。
R 中的计算由顺序评估 语句 组成。语句,例如 x<-1:10
或 mean(y)
,可以用分号或换行符分隔。每当 评估器遇到语法完整的语句时,该语句就会被评估,并返回 值。评估语句的结果可以称为语句的 值3 该值始终可以分配给一个符号。
分号和换行符都可以用来分隔语句。分号始终表示语句的结束,而换行符 可能 表示语句的结束。如果当前语句在语法上不完整,换行符将被评估器简单地忽略。如果会话是交互式的,提示符将从 ‘>’ 更改为 ‘+’。
> x <- 0; x + 5 [1] 5 > y <- 1:10 > 1; 2 [1] 1 [1] 2
语句可以使用花括号 ‘{’ 和 ‘}’ 分组。一组语句有时被称为 块。当在语法完整的语句末尾输入换行符时,单个语句将被评估。块直到在结束花括号之后输入换行符才会被评估。在本节的其余部分,语句 指的是单个语句或块。
> { x <- 0 + x + 5 + } [1] 5
The if
/else
语句条件性地评估两个语句。它包含一个 条件,该条件会被评估,如果 值 为 TRUE
,则评估第一个语句;否则评估第二个语句。 if
/else
语句返回被选中的语句的值作为其值。其正式语法为
if ( statement1 ) statement2 else statement3
首先,评估 statement1 以产生 value1。如果 value1 是一个逻辑向量,其第一个元素为 TRUE
,则评估 statement2。如果 value1 的第一个元素为 FALSE
,则评估 statement3。如果 value1 是一个数值向量,则当 value1 的第一个元素为零时评估 statement3,否则评估 statement2。仅使用 value1 的第一个元素。所有其他元素都被忽略。如果 value1 的类型不是逻辑向量或数值向量,则会发出错误信号。
if
/else
语句可用于避免数值问题,例如对负数取对数。由于 if
/else
语句与其他语句相同,因此可以为它们赋值。以下两个示例是等效的。
> if( any(x <= 0) ) y <- log(1+x) else y <- log(x) > y <- if( any(x <= 0) ) log(1+x) else log(x)
else
子句是可选的。语句 if(any(x <= 0)) x <- x[x <= 0]
是有效的。当 if
语句不在块中时,如果存在 else
,它必须出现在与 statement2 结束相同的行上。否则,statement2 结尾处的换行符将完成 if
并产生一个语法完整的语句,该语句会被评估。一个简单的解决方案是使用用大括号括起来的复合语句,将 else
放在标记语句结束的闭合大括号的同一行上。
if
/else
语句可以嵌套。
if ( statement1 ) { statement2 } else if ( statement3 ) { statement4 } else if ( statement5 ) { statement6 } else statement8
将评估偶数编号的语句之一,并返回结果值。如果省略了可选的 else
子句,并且所有奇数编号的 statement 都评估为 FALSE
,则不会评估任何语句,并返回 NULL
。
奇数编号的 语句 按顺序进行评估,直到其中一个评估为 TRUE
,然后评估与之关联的偶数编号的 语句。在这个例子中,只有当 statement1 为 FALSE
且 statement3 为 FALSE
且 statement5 为 TRUE
时,才会评估 statement6。允许的 else if
子句数量没有限制。
R 有三个语句提供显式循环。 4 它们是 for
、while
和 repeat
。两个内置结构 next
和 break
提供了对评估的额外控制。R 提供了其他用于隐式循环的函数,例如 tapply
、apply
和 lapply
。此外,许多操作,尤其是算术操作,都是矢量化的,因此您可能不需要使用循环。
有两个语句可用于显式控制循环。它们是 break
和 next
。 break
语句导致退出当前正在执行的最内层循环。 next
语句立即导致控制返回到循环的开头。然后执行循环的下一个迭代(如果有)。循环中 next
下面的任何语句都不会被评估。
循环语句返回的值始终为 NULL
,并且以不可见的方式返回。
repeat
语句会导致循环体重复执行,直到明确请求退出循环。这意味着在使用 repeat
时需要谨慎,因为存在无限循环的风险。 repeat
循环的语法如下:
repeat statement
在使用 repeat
时,statement 必须是一个块语句。你需要执行一些计算并测试是否退出循环,通常这需要两个语句。
while
语句与 repeat
语句非常相似。 while
循环的语法如下:
while ( statement1 ) statement2
其中 statement1 被评估,如果它的值为 TRUE
,则评估 statement2。这个过程持续进行,直到 statement1 被评估为 FALSE
。
for
循环的语法如下:
for ( name in vector ) statement1
其中 vector 可以是向量或列表。对于 vector 中的每个元素,变量 name 被设置为该元素的值,并评估 statement1。一个副作用是,变量 name 在循环结束后仍然存在,并且它具有循环评估的 vector 的最后一个元素的值。
从技术上讲,switch
只是一个函数,但它的语义与其他编程语言的控制结构类似。
语法如下:
switch (statement, list)
其中 list 的元素可以命名。首先,评估 statement 并获得结果 value。如果 value 是 1 到 list 长度之间的数字,则评估 list 的对应元素并返回结果。如果 value 太大或太小,则返回 NULL
。
> x <- 3 > switch(x, 2+2, mean(1:10), rnorm(5)) [1] 2.2903605 2.3271663 -0.7060073 1.3622045 -0.2892720 > switch(2, 2+2, mean(1:10), rnorm(5)) [1] 5.5 > switch(6, 2+2, mean(1:10), rnorm(5)) NULL
如果 value 是一个字符向量,则会评估 ...
中与 value 完全匹配的名称的元素。如果没有匹配项,将使用单个未命名的参数作为默认值。如果未指定默认值,则返回 NULL
。
> y <- "fruit" > switch(y, fruit = "banana", vegetable = "broccoli", "Neither") [1] "banana" > y <- "meat" > switch(y, fruit = "banana", vegetable = "broccoli", "Neither") [1] "Neither"
switch
的常见用法是根据函数参数之一的字符值进行分支。
> centre <- function(x, type) { + switch(type, + mean = mean(x), + median = median(x), + trimmed = mean(x, trim = .1)) + } > x <- rcauchy(10) > centre(x, "mean") [1] 0.8760325 > centre(x, "median") [1] 0.5360891 > centre(x, "trimmed") [1] 0.6086504
switch
返回已评估语句的值,如果未评估任何语句,则返回 NULL
。
要从已存在的备选列表中进行选择,switch
可能不是选择一个进行评估的最佳方法。通常,最好直接使用 eval
和子集运算符 [[
,通过 eval(x[[condition]])
。
在本节中,我们将讨论应用于基本运算(如两个向量或矩阵的加法或乘法)的规则的细微之处。
如果尝试添加两个元素数量不同的结构,则最短的结构将循环到最长的结构的长度。例如,如果你将 c(1, 2, 3)
添加到一个六元素向量,那么你实际上会添加 c(1, 2, 3, 1, 2, 3)
。如果较长向量的长度不是较短向量的倍数,则会发出警告。
从 R 1.4.0 版本开始,任何涉及零长度向量的算术运算都会产生零长度的结果。
统计意义上的缺失值,即值未知的变量,其值为 NA
。这与函数参数未提供时的 missing
属性不同(参见 参数)。
由于原子向量中的元素必须是相同类型,因此存在多种类型的 NA
值。在一种情况下,这对用户尤为重要。默认的 NA
类型为 logical
,除非强制转换为其他类型,因此缺失值的出现可能会触发逻辑索引而不是数值索引(有关详细信息,请参阅 索引)。
使用 NA
进行数值和逻辑计算通常会返回 NA
。在操作结果对 NA
可能取的所有值都相同的情况下,操作可能会返回此值。特别是,‘FALSE & NA’ 为 FALSE
,‘TRUE | NA’ 为 TRUE
。 NA
不等于任何其他值或自身;使用 is.na
测试 NA
。 但是,NA
值将在 match
中与另一个 NA
值匹配。
结果未定义的数值计算,例如 ‘0/0’,会产生值 NaN
。它只存在于 double
类型中,以及复数类型的实部或虚部中。提供函数 is.nan
来专门检查 NaN
,is.na
也对 NaN
返回 TRUE
。 将 NaN
强制转换为逻辑或整数类型会得到相应类型的 NA
,但强制转换为字符类型会得到字符串 "NaN"
。 NaN
值不可比较,因此涉及 NaN
的相等性或排序测试将导致 NA
。它们被认为与任何 NaN
值匹配(而不是其他值,甚至不包括 NA
),由 match
确定。
从 R 1.5.0 开始,字符类型的 NA
与字符串 "NA"
不同。需要指定显式字符串 NA
的程序员应该使用 ‘NA_character_’ 而不是 "NA"
,或者使用 is.na<-
将元素设置为 NA
。
存在常量 NA_integer_
、NA_real_
、NA_complex_
和 NA_character_
,它们将在解析器中生成相应类型的 NA
值,并在反解析时使用,此时无法识别 NA
的类型(并且 control
选项要求这样做)。
原始向量没有 NA
值。
R 包含几种结构,允许通过索引操作访问单个元素或子集。对于基本向量类型,可以使用 x[i]
访问第 i 个元素,但也可以索引列表、矩阵和多维数组。除了使用单个整数进行索引外,还有几种索引形式。索引既可以用于提取对象的某一部分,也可以用于替换对象的某一部分(或添加部分)。
R 有三个基本索引运算符,其语法由以下示例显示
x[i] x[i, j] x[[i]] x[[i, j]] x$a x$"a"
对于向量和矩阵,[[
形式很少使用,尽管它们与 [
形式有一些细微的语义差异(例如,它会删除任何 names
或 dimnames
属性,并且会对字符索引使用部分匹配)。当使用单个索引索引多维结构时,x[[i]]
或 x[i]
将返回 x
的第 i
个顺序元素。
对于列表,通常使用 [[
选择任何单个元素,而 [
返回所选元素的列表。
[[
形式只允许使用整数或字符索引选择单个元素,而 [
允许使用向量进行索引。但请注意,对于列表或其他递归对象,索引可以是向量,向量的每个元素依次应用于列表、选定的组件、该组件的选定组件,等等。结果仍然是一个元素。
使用 $
的形式适用于列表和配对列表等递归对象。它只允许使用文字字符串或符号作为索引。也就是说,索引不可计算:对于需要计算表达式以查找索引的情况,请使用 x[[expr]]
。将 $
应用于非递归对象会导致错误。
R 允许使用向量作为索引进行一些强大的构造。我们将首先讨论简单向量的索引。为简单起见,假设表达式为 x[i]
。然后根据 i
的类型,存在以下可能性。
i
的所有元素必须具有相同的符号。如果它们为正数,则选择具有这些索引号的 x
的元素。如果 i
包含负元素,则选择除指示的元素之外的所有元素。
如果 i
为正数且超过 length(x)
,则相应的选择为 NA
。从 R 版本 2.6.0 开始,为了与 S 兼容,负越界值 i
会被静默忽略,因为它们意味着删除不存在的元素,这是一个空操作(“无操作”)。
一个特例是零索引,它没有效果:x[0]
是一个空向量,否则在正或负索引中包含零与省略它们具有相同的效果。
i
通常应与 x
的长度相同。如果它更短,则它的元素将像在 基本算术运算 中讨论的那样被循环使用。如果它更长,则 x
将在概念上用 NA
扩展。 x
的选定值为 i
为 TRUE
的那些值。
i
中的字符串与 x
的 names 属性进行匹配,并使用结果整数。对于 [[
和 $
,如果精确匹配失败,则使用部分匹配,因此如果 x
不包含名为 "aa"
的组件,并且 "aabb"
是唯一具有前缀 "aa"
的名称,则 x$aa
将匹配 x$aabb
。对于 [[
,可以通过 exact
参数控制部分匹配,该参数默认为 NA
,表示允许部分匹配,但如果发生部分匹配,则应发出警告。将 exact
设置为 TRUE
将阻止部分匹配发生,FALSE
值允许部分匹配,并且不会发出任何警告。请注意,[
始终需要精确匹配。字符串 ""
被特殊对待:它表示“无名称”,并且不匹配任何元素(即使是那些没有名称的元素)。请注意,部分匹配仅在提取时使用,而不是在替换时使用。
x[as.integer(i)]
相同。因子级别永远不会使用。如果需要,请使用 x[as.character(i)]
或类似的构造。
x[]
返回 x
,但会从结果中删除“无关”属性。仅保留 names
属性,以及在多维数组中保留 dim
和 dimnames
属性。
integer(0)
。
使用缺失值(即 NA
)进行索引会得到 NA
结果。此规则也适用于逻辑索引的情况,即 i
中具有 NA
选择器的 x
元素将包含在结果中,但其值将为 NA
。
但是,请注意,NA
有不同的模式——文字常量是 "logical"
模式,但它经常被自动强制转换为其他类型。这种现象的一个影响是 x[NA]
的长度与 x
相同,但 x[c(1, NA)]
的长度为 2。这是因为前一种情况下应用了逻辑索引规则,而后一种情况下应用了整数索引规则。
使用 [
索引也会对任何名称属性进行相应的子集操作。
对多维结构进行子集操作通常遵循与单维索引相同的规则,每个索引变量对应于 dimnames
的相关部分,代替 names
。不过,也有一些特殊规则。
通常,使用与结构维度相对应的索引数量来访问结构。但是,也可以使用单个索引,在这种情况下,会忽略 dim
和 dimnames
属性,结果实际上等同于 c(m)[i]
。请注意,m[1]
通常与 m[1, ]
或 m[, 1]
非常不同。
可以使用整数矩阵作为索引。在这种情况下,矩阵的列数应与结构的维度数匹配,结果将是一个向量,其长度与矩阵的行数相同。以下示例展示了如何在一个操作中提取元素 m[1, 1]
和 m[2, 2]
。
> m <- matrix(1:4, 2) > m [,1] [,2] [1,] 1 3 [2,] 2 4 > i <- matrix(c(1, 1, 2, 2), 2, byrow = TRUE) > i [,1] [,2] [1,] 1 1 [2,] 2 2 > m[i] [1] 1 4
索引矩阵不能包含负索引。 NA
和零值是允许的:索引矩阵中包含零的行将被忽略,而包含 NA
的行将在结果中产生 NA
。
无论是使用单个 索引还是矩阵索引,如果存在 names
属性,则会使用该属性,就像结构是一维的。
如果索引操作导致结果的某个维度长度为 1,例如使用 m[2, , ]
从三维矩阵中选择单个切片,则通常会从结果中删除相应的维度。如果结果是一维结构,则会得到一个向量。这有时是不可取的,可以通过在索引操作中添加 ‘drop = FALSE’ 来关闭。请注意,这是 [
函数的附加参数,不会增加索引计数。因此,选择矩阵第一行作为 1 行 n 列矩阵的正确方法是 m[1, , drop = FALSE]
。忘记禁用删除功能是通用子例程中常见的失败原因,因为索引偶尔(但通常不会)长度为 1。此规则仍然适用于一维数组,其中任何子集都会产生向量结果,除非使用 ‘drop = FALSE’。
请注意,向量与一维数组不同,后者具有 dim
和 dimnames
属性(长度均为 1)。一维数组不容易从子集操作中获得,但可以显式构造,并且由 table
返回。这有时很有用,因为 dimnames
列表的元素本身可能被命名,而 names
属性则不是这种情况。
某些操作(例如 m[FALSE, ]
)会导致结构中某个维度长度为零。R 通常会尝试合理地处理这些结构。
运算符 [
是一个通用函数,允许添加类方法,$
和 [[
运算符也是如此。因此,可以为任何结构创建用户定义的索引操作。例如,一个名为 [.foo
的函数,它接受一组参数,其中第一个参数是正在索引的结构,其余参数是索引。在 $
的情况下,即使使用 x$"abc"
形式,索引参数的模式也是 "symbol"
。需要注意的是,类方法的行为不一定与基本方法相同,例如在部分匹配方面。
对于 [
,类方法最重要的例子是用于数据框的类方法。这里没有详细描述(参见 [.data.frame
的帮助页面),但总的来说,如果提供了两个索引(即使其中一个为空),它会为一个结构创建矩阵式的索引,该结构本质上是一个相同长度的向量列表。如果只提供一个索引,则将其解释为索引列列表——在这种情况下,drop
参数会被忽略,并会发出警告。
基本运算符 $
和 [[
可以应用于环境。只允许使用字符索引,并且不会进行部分匹配。
对结构子集的赋值是复杂赋值通用机制的一种特殊情况。
x[3:5] <- 13:15
此命令的结果就好像执行了以下命令:
`*tmp*` <- x x <- "[<-"(`*tmp*`, 3:5, value=13:15) rm(`*tmp*`)
注意,索引首先被转换为数值索引,然后元素沿着数值索引依次替换,就好像使用了 for
循环一样。任何名为 `*tmp*`
的现有变量都会被覆盖并删除,并且此变量名不应该在代码中使用。
相同的机制可以应用于除 [
之外的其他函数。替换函数具有相同的名称,并在其后添加 <-
。它的最后一个参数必须称为 value
,它是要分配的新值。例如,
names(x) <- c("a","b")
等效于
`*tmp*` <- x x <- "names<-"(`*tmp*`, value=c("a","b")) rm(`*tmp*`)
复杂赋值的嵌套是递归评估的。
names(x)[3] <- "Three"
等效于
`*tmp*` <- x x <- "names<-"(`*tmp*`, value="[<-"(names(`*tmp*`), 3, value="Three")) rm(`*tmp*`)
在封闭环境中进行复杂赋值(使用 <<-
)也是允许的。
names(x)[3] <<- "Three"
等效于
`*tmp*` <<- get(x, envir=parent.env(), inherits=TRUE) names(`*tmp*`)[3] <- "Three" x <<- `*tmp*` rm(`*tmp*`)
以及
`*tmp*` <- get(x,envir=parent.env(), inherits=TRUE) x <<- "names<-"(`*tmp*`, value="[<-"(names(`*tmp*`), 3, value="Three")) rm(`*tmp*`)
只有目标变量在封闭环境中被评估,所以
e<-c(a=1,b=2) i<-1 local({ e <- c(A=10,B=11) i <-2 e[i] <<- e[i]+1 })
在超赋值语句的左侧和右侧都使用 i
的局部值,以及在右侧使用 e
的局部值。它将外部环境中的 e
设置为
a b 1 12
也就是说,超赋值等效于以下四行代码
`*tmp*` <- get(e, envir=parent.env(), inherits=TRUE) `*tmp*`[i] <- e[i]+1 e <<- `*tmp*` rm(`*tmp*`)
类似地
x[is.na(x)] <<- 0
等效于
`*tmp*` <- get(x,envir=parent.env(), inherits=TRUE) `*tmp*`[is.na(x)] <- 0 x <<- `*tmp*` rm(`*tmp*`)
而不是
`*tmp*` <- get(x,envir=parent.env(), inherits=TRUE) `*tmp*`[is.na(`*tmp*`)] <- 0 x <<- `*tmp*` rm(`*tmp*`)
只有当存在局部变量 x
时,这两个候选解释才有所不同。最好避免使用与超赋值目标变量相同的名称的局部变量。由于此情况在 1.9.1 及更早版本中处理不正确,因此此类代码应该没有严重的必要性。
几乎每种编程语言都有一套作用域规则,允许使用相同的名称来表示不同的对象。例如,这允许函数中的局部变量与全局对象具有相同的名称。
R 使用 词法作用域 模型,类似于 Pascal 等语言。但是,R 是一种 函数式编程语言,允许动态创建和操作函数和语言对象,并具有反映此事实的附加功能。
每次调用一个 函数都会创建一个 帧,其中包含在函数中创建的局部变量,并在一个环境中进行评估,两者结合创建一个新的环境。
注意术语:帧是一组变量,环境是帧的嵌套(或者等效地:最内层的帧加上封闭环境)。
环境可以被分配给变量或包含在其他对象中。但是,请注意它们不是标准对象——特别是,它们在赋值时不会被复制。
闭包(模式 "function"
)对象将包含它在创建时所在的定义环境(默认情况下。可以使用 environment<-
操作环境)。当该函数随后被调用时,它的 评估环境将使用闭包的环境作为封闭环境创建。请注意,这并不一定是调用者的环境!
因此,当在 函数内部请求一个变量时,首先会在 评估环境中查找,然后在封闭环境中查找,再在封闭环境的封闭环境中查找,等等;一旦到达全局环境或包的环境,搜索将继续向上搜索路径到基本包的环境。如果在那里没有找到该变量,搜索将继续到空环境,并将失败。
每次调用一个 函数时,都会创建一个新的评估帧。在计算过程中的任何时间点,当前活动的環境都可以通过调用栈访问。每次调用函数时,内部都会创建一个称为上下文的特殊结构,并将其放置在一个上下文列表中。当函数完成评估时,它的上下文将从调用栈中移除。
将调用堆栈中更高层定义的变量变为可访问称为 动态作用域。变量的绑定由变量的最近(按时间顺序)定义决定。这与 R 中的默认作用域规则相矛盾,默认作用域规则使用函数定义所在的 环境(词法作用域)。一些函数,特别是那些使用和操作模型公式的函数,需要通过直接访问调用堆栈来模拟动态作用域。
对 调用堆栈的访问是通过一系列函数提供的,这些函数的名称以 ‘sys.’ 开头。它们简要列出如下。
sys.call
获取指定上下文的调用。
sys.frame
获取指定上下文的评估框架。
sys.nframe
获取所有活动上下文的环境框架。
sys.function
获取指定上下文中正在调用的函数。
sys.parent
获取当前函数调用的父级。
sys.calls
获取所有活动上下文的调用。
sys.frames
获取所有活动上下文的评估框架。
sys.parents
获取所有活动上下文的数字标签。
sys.on.exit
设置一个函数,在指定上下文退出时执行。
sys.status
调用 sys.frames
、sys.parents
和 sys.calls
。
parent.frame
获取指定父级上下文的评估框架。
除了评估 环境结构之外,R 还具有一个搜索路径,该路径包含用于搜索在其他地方找不到的变量的环境。这用于两件事:函数包和附加的用户数据。
搜索路径的第一个元素是全局环境,最后一个是基本包。一个 Autoloads
环境用于保存可能按需加载的代理对象。其他环境使用 attach
或 library
插入路径中。
具有命名空间的包具有不同的搜索路径。当从此类包中的对象开始搜索 R 对象时,首先搜索包本身,然后搜索其导入的包,然后搜索基本命名空间,最后搜索全局环境和其余的常规搜索路径。这样,对同一包中其他对象的引用将解析为该包,并且全局环境或其他包中相同名称的对象不能掩盖这些对象。
虽然 R 作为数据分析工具非常有用,但大多数用户很快就会发现自己想要编写自己的函数。这是 R 的真正优势之一。用户可以对其进行编程,并且如果他们愿意,可以将系统级函数更改为他们认为更合适的函数。
R 还提供了一些工具,使您能够轻松地记录您创建的任何函数。请参阅Writing R Extensions中的编写 R 文档。
function ( arglist ) body
函数声明的第一部分是关键字function
,它指示 R 您要创建一个函数。
一个参数列表是一个用逗号分隔的形式参数列表。形式参数可以是符号、形式为 ‘symbol = expression’ 的语句,或者特殊形式参数...
。
该主体可以是任何有效的 R 表达式。通常,主体是包含在花括号(‘{’ 和 ‘}’)中的表达式组。
通常,函数被分配给符号,但它们不需要被分配。对function
的调用的返回值是一个函数。如果它没有被命名,它被称为匿名函数。匿名函数最常被用作其他函数的参数,例如apply
系列或outer
。
这是一个简单的函数:echo <- function(x) print(x)
。所以echo
是一个接受单个参数的函数,当echo
被调用时,它会打印它的参数。
函数的形式参数定义了在函数被调用时将提供值的变量。这些参数的名称可以在函数体中使用,在那里它们获得在函数调用时提供的的值。
可以使用特殊形式 ‘name = expression’ 为参数指定默认值。在这种情况下,如果用户在调用函数时没有为参数指定值,则表达式将与相应的符号相关联。当需要值时,expression 将在函数的评估框架中进行评估。
也可以使用函数missing
来指定默认行为。当missing
使用形式参数的名称调用时,如果形式参数没有与任何实际参数匹配并且在函数体中没有被随后修改,则它将返回TRUE
。因此,一个missing
的参数将具有它的默认值(如果有)。missing
函数不会强制评估参数。
特殊类型的参数...
可以包含任意数量的提供的参数。它用于各种目的。它允许你编写一个函数,该函数接受任意数量的参数。它可以用来将一些参数吸收进一个中间函数,然后这些参数可以被随后调用的函数提取。
当一个 函数被调用或执行时,会创建一个新的 评估帧。在这个帧中,形式参数会根据 参数匹配 中给出的规则与提供的参数匹配。函数体中的语句在这个 环境帧中按顺序执行。
评估帧的封闭帧是与被调用函数关联的环境帧。这可能与 S 不同。虽然许多函数的 .GlobalEnv
作为其环境,但这并不一定为真,在具有命名空间的包中定义的函数(通常)具有包命名空间作为其环境。
本节适用于闭包,但不适用于原始函数。后者通常忽略标签并进行位置匹配,但应查阅其帮助页面以了解例外情况,包括 log
、round
、signif
、rep
和 seq.int
。
在 函数求值中,首先要进行的是将形式参数与实际参数或提供的参数进行匹配。这通过一个三步过程完成。
f <- function(fumble, fooey) fbody
,则 f(f = 1, fo = 2)
是非法的,即使第二个实际参数只匹配 fooey
。 虽然 f(f = 1, fooey = 2)
是 合法的,因为第二个参数完全匹配,并且从部分匹配的考虑中移除。如果形式参数包含 ...
,则部分匹配仅应用于它之前的参数。
...
参数,它将占用剩余的参数,无论是否标记。
如果任何参数仍然未匹配,则会报错。
参数匹配由函数 match.arg
、match.call
和 match.fun
增强。 R 使用的局部匹配算法可以通过 pmatch
访问。
关于 函数参数的 评估,最重要的是要了解传递参数和默认参数的处理方式不同。传递给函数的参数在调用函数的评估框架中进行评估。函数的默认参数在函数的评估框架中进行评估。
R 函数参数的调用语义是按值调用。一般来说,传递的参数的行为就像用传递的值初始化的局部变量,以及对应形式参数的 名称。在函数内部更改传递参数的值不会影响调用框架中变量的值。
R 具有函数参数的惰性评估形式。参数只有在需要时才会被评估。重要的是要意识到,在某些情况下,参数永远不会被评估。因此,使用函数参数来产生副作用是一种不好的风格。虽然在 C 中,使用形式 foo(x = y)
来调用 foo
并同时将 y
的值赋给 x
是很常见的,但在 R 中不应该使用这种风格。不能保证参数会被评估,因此 赋值可能不会发生。
还需要注意的是,如果参数被评估,foo(x <- y)
的效果是改变调用 环境中的 x
的值,而不是 foo
的 评估环境中的值。
可以在函数内部访问作为参数使用的实际(非默认)表达式。该机制通过承诺来实现。当 函数被评估时,作为参数使用的实际表达式将与指向函数被调用的环境的指针一起存储在承诺中。当(如果)参数被评估时,存储的表达式将在函数被调用的环境中进行评估。由于只使用指向环境的指针,因此对该环境所做的任何更改都会在评估期间生效。然后,结果值也会存储在承诺中的另一个位置。后续评估将检索此存储的值(不会执行第二次评估)。还可以使用 substitute
访问未评估的表达式。
当 函数被调用时,每个形式参数在调用的局部环境中被分配一个承诺,表达式槽包含实际参数(如果存在),环境槽包含调用者的环境。如果调用中没有为形式参数提供实际参数,并且存在默认表达式,则类似地将其分配给形式参数的表达式槽,但 环境设置为局部环境。
通过在承诺的环境中 评估表达式槽的内容来填充承诺的值槽的过程称为强制承诺。承诺只会被强制一次,值槽内容之后会被直接使用。
当需要承诺的值时,就会强制承诺。这通常发生在内部 函数中,但也可以通过直接评估承诺本身来强制承诺。当默认表达式依赖于局部环境中另一个形式参数或其他变量的值时,这偶尔很有用。在下面的示例中,单独的 label
确保标签在下一行更改 x
的值之前基于 x
的值。
function(x, label = deparse(x)) { label x <- x + 1 print(label) }
承诺的表达式槽本身可能包含其他承诺。当未评估的参数作为参数传递给另一个函数时,就会发生这种情况。当强制承诺时,其表达式中的其他承诺也会在评估时递归地强制。
范围或作用域规则只是评估器用来为符号查找值的规则集。每种计算机语言都有一套这样的规则。在 R 中,这些规则相当简单,但确实存在一些机制可以颠覆通常的或默认规则。
R 遵循一组称为词法作用域的规则。这意味着在创建表达式时生效的变量绑定用于为表达式中任何未绑定的符号提供值。
大多数与作用域相关的有趣特性都与评估函数有关,我们将重点关注这个问题。符号可以是绑定或未绑定。函数的所有形式参数在函数体中提供绑定符号。函数体中的任何其他符号要么是局部变量,要么是未绑定变量。局部变量是在函数内部定义的变量。由于 R 没有对变量的正式定义,它们只是在需要时使用,因此很难确定一个变量是局部变量还是非局部变量。局部变量必须首先定义,这通常通过将它们放在赋值的左侧来完成。
在评估过程中,如果检测到未绑定符号,则 R 会尝试为其查找值。作用域规则决定此过程如何进行。在 R 中,首先搜索函数的环境,然后搜索其封闭环境,依此类推,直到到达全局环境。
全局环境是环境搜索列表的头部,这些环境按顺序搜索以查找匹配的符号。然后使用第一个匹配项的值。
当这组规则与函数可以作为其他函数的值返回这一事实相结合时,一些相当不错,但乍一看很奇怪的特性就得到了体现。
一个简单的例子
f <- function() { y <- 10 g <- function(x) x + y return(g) } h <- f() h(3)
一个相当有趣的问题是当评估h
时会发生什么。当评估函数体时,确定局部变量或绑定变量的值没有问题。作用域规则决定语言将如何为未绑定变量查找值。
当评估h(3)
时,我们看到它的主体是g
的主体。在该主体中,x
绑定到形式参数,而y
未绑定。在具有词法作用域的语言中,x
将与值 3 相关联,而y
将与f
的局部值 10 相关联,因此h(3)
应该返回值 13。在 R 中,这确实是发生的情况。
在 S 中,由于不同的作用域规则,您将收到一个错误,指示未找到 y
,除非您的工作区中存在一个名为 y
的变量,在这种情况下将使用其值。
面向对象编程是一种近年来流行的编程风格。它之所以如此受欢迎,主要是因为它使编写和维护复杂的系统变得更加容易。它通过几种不同的机制来实现这一点。
任何面向对象语言的核心都是类和方法的概念。一个 类 是一个对象的定义。通常,一个类包含几个 槽,用于保存特定于类的信息。语言中的对象必须是某个类的实例。编程基于对象或类的实例。
计算是通过 方法 执行的。方法基本上是 函数,专门用于对对象(通常是特定类的对象)执行特定计算。这就是使语言面向对象的本质。在 R 中,泛型函数 用于确定适当的方法。泛型函数负责确定其参数的类,并使用该信息来选择适当的方法。
大多数面向对象语言的另一个特点是继承的概念。在大多数编程问题中,通常存在许多彼此相关的对象。如果可以重用某些组件,则编程将大大简化。
如果一个类从另一个类继承,那么它通常会获得父类中的所有槽,并且可以通过添加新的槽来扩展它。在方法分派(通过泛型函数)中,如果类中不存在方法,则会查找父类的方法。
本章将讨论这种通用策略在 R 中的实现方式,并探讨当前设计中的一些局限性。大多数面向对象系统带来的优势之一是更高的一致性。这是通过编译器或解释器检查的规则实现的。不幸的是,由于面向对象系统在 R 中的集成方式,这种优势并没有得到体现。建议用户以直接的方式使用面向对象系统。虽然可以执行一些相当有趣的操作,但这些操作往往会导致代码混淆,并且可能依赖于不会被继承的实现细节。
面向对象编程在 R 中的最大用途是通过 print
方法、summary
方法和 plot
方法。这些方法允许我们使用一个通用的 函数调用,例如 plot
,它根据其参数的类型进行调度,并调用特定于所提供数据的绘图函数。
为了使概念清晰,我们将考虑一个旨在教学生概率的小型系统的实现。在这个系统中,对象是概率函数,我们将考虑的方法是用于查找矩和绘图的方法。概率总是可以用累积分布函数来表示,但也可以用其他方式来表示。例如,当存在时作为密度,或者当存在时作为矩生成函数。
R 并没有一个完整的 面向对象系统,而是一个类系统和一个基于对象类的调度机制。解释代码的调度机制依赖于存储在评估框架中的四个特殊对象。这些特殊对象是 .Generic
、.Class
、.Method
和 .Group
。内部函数和类型的调度机制是独立的,将在其他地方讨论。
类系统通过 class
属性实现。此属性是一个包含类名称的字符向量。因此,要创建一个名为 "foo"
的类对象,只需将一个包含字符串 ‘"foo"’ 的类属性附加到它。因此,实际上任何东西都可以转换为 "foo"
类的对象。
对象系统通过两个调度函数 UseMethod
和 NextMethod
使用 泛型函数。对象系统的典型用法是从调用一个泛型函数开始。这通常是一个非常简单的函数,只包含一行代码。系统函数 mean
就是这样一个函数,
> mean function (x, ...) UseMethod("mean")
当调用 mean
时,它可以接受任意数量的参数,但第一个参数是特殊的,第一个参数的类用于确定应该调用哪个方法。变量 .Class
设置为 x
的类属性,.Generic
设置为字符串 "mean"
,并搜索要调用的正确方法。mean
的任何其他参数的类属性将被忽略。
假设 x
的类属性包含 "foo"
和 "bar"
,按此顺序。然后 R 将首先搜索名为 mean.foo
的函数,如果找不到,它将搜索函数 mean.bar
,如果搜索仍然不成功,则将进行最后一次搜索 mean.default
。如果最后一次搜索不成功,R 将报告错误。最好始终编写一个默认方法。请注意,函数 mean.foo
等在此上下文中被称为方法。
NextMethod
提供了另一种调度机制。一个 函数可以在任何地方调用 NextMethod
。然后应该调用哪个方法的确定主要基于 .Class
和 .Generic
的当前值。这有点问题,因为方法实际上是一个普通的函数,用户可以直接调用它。如果他们这样做,则不会有 .Generic
或 .Class
的值。
如果直接调用一个方法,并且它包含对 NextMethod
的调用,则 NextMethod
的第一个参数用于确定 泛型函数。如果未提供此参数,则会发出错误信号;因此,最好始终提供此参数。
如果直接调用方法,则方法第一个参数的类属性将用作.Class
的值。
方法本身使用NextMethod
来提供一种继承形式。通常,特定方法执行一些操作来设置数据,然后通过调用NextMethod
调用下一个适当的方法。
考虑以下简单示例。二维欧几里得空间中的一个点可以通过其笛卡尔坐标 (x-y) 或极坐标 (r-theta) 来指定。因此,为了存储有关点位置的信息,我们可以定义两个类,"xypoint"
和 "rthetapoint"
。所有“xypoint”数据结构都是具有 x 分量和 y 分量的列表。所有“rthetapoint”对象都是具有 r 分量和 theta 分量的列表。
现在,假设我们想要从任何类型的对象中获取 x 位置。这可以通过泛型函数轻松实现。我们定义泛型函数xpos
如下。
xpos <- function(x, ...) UseMethod("xpos")
现在我们可以定义方法
xpos.xypoint <- function(x) x$x xpos.rthetapoint <- function(x) x$r * cos(x$theta)
用户只需使用任一表示形式作为参数调用函数xpos
。内部调度方法查找对象的类并调用相应的方法。
添加其他表示形式非常容易。无需编写新的泛型函数,只需编写方法即可。这使得向现有系统添加内容变得容易,因为用户只需处理新表示形式,而无需处理任何现有表示形式。
这种方法的大部分用途是为不同类型的对象提供专门的打印;print
有大约 40 种方法。
对象的类属性可以包含多个元素。当调用泛型函数时,主要通过NextMethod
处理第一个继承。NextMethod
确定当前正在评估的方法,从该方法中找到下一个类
FIXME: 这里缺少一些内容
泛型函数应该只包含一条语句。它们通常应该采用以下形式 foo <- function(x, ...) UseMethod("foo", x)
。当调用 UseMethod
时,它会确定适当的方法,然后调用该方法,并使用与调用泛型函数相同的参数,顺序也相同,就好像直接调用了该方法一样。
为了确定正确的方法,会获取泛型函数第一个参数的类属性,并使用它来查找正确的方法。泛型函数的名称与类属性的第一个元素组合成 generic.class
的形式,并查找具有该名称的函数。如果找到该函数,则使用它。如果找不到这样的函数,则使用类属性的第二个元素,依此类推,直到遍历完类属性的所有元素。如果此时仍未找到方法,则使用 generic.default
方法。如果泛型函数的第一个参数没有类属性,则使用 generic.default
方法。自从引入命名空间后,方法可能无法通过其名称访问(即 get("generic.class")
可能失败),但可以通过 getS3method("generic","class")
访问。
任何对象都可以具有 class
属性。此属性可以包含任意数量的元素。每个元素都是一个字符串,定义一个类。当调用泛型函数时,会检查其第一个参数的类。
UseMethod
是一个特殊函数,它的行为与其他函数调用不同。它的调用语法为 UseMethod(generic, object)
,其中 generic 是泛型函数的名称,object 是用于确定应选择哪个方法的对象。 UseMethod
只能从函数体中调用。
UseMethod
以两种方式改变了求值模型。首先,当它被调用时,它会确定下一个要调用的方法(函数)。然后,它使用当前求值 环境调用该函数;这个过程将在后面描述。 UseMethod
改变求值环境的第二种方式是它不会将控制权返回给调用函数。这意味着,在调用 UseMethod
之后,任何语句都保证不会被执行。
当调用 UseMethod
时,泛型函数是调用 UseMethod
时指定的 value。要分派的对象是提供的第二个参数或当前函数的第一个参数。确定参数的类,并将它的第一个元素与泛型名称组合起来,以确定适当的方法。因此,如果泛型名称为 foo
,而对象的类为 "bar"
,那么 R 将搜索名为 foo.bar
的方法。如果不存在这样的方法,则使用上面描述的继承机制来定位适当的方法。
一旦确定了方法,R 就会以一种特殊的方式调用它。R 不会创建新的求值 环境,而是使用当前函数调用(对泛型的调用)的环境。在调用 UseMethod
之前进行的任何 赋值或求值都将生效。用于调用泛型的参数将重新匹配到所选方法的形式参数。
当调用方法时,它使用与调用泛型时数量相同且名称相同的参数进行调用。它们根据 R 的标准参数匹配规则与方法的参数进行匹配。但是,对象(即第一个参数)已经求值。
调用 UseMethod
会在求值帧中放置一些特殊对象。它们是 .Class
、.Generic
和 .Method
。这些特殊对象由 R 用于处理方法分派和继承。 .Class
是对象的类,.Generic
是泛型函数的名称,.Method
是当前正在调用的方法的名称。如果方法是通过其中一个内部接口调用的,那么可能还存在一个名为 .Group
的对象。这将在第 组方法 节中描述。在对 UseMethod
进行初始调用之后,这些特殊变量(而不是对象本身)控制后续方法的选择。
然后以标准方式评估方法体。特别是,方法体中的变量查找遵循方法的规则。因此,如果方法具有关联的环境,则使用该环境。实际上,我们用对方法的调用替换了对泛型的调用。泛型框架中的任何局部赋值都将被传递到对方法的调用中。不鼓励使用此功能。重要的是要意识到,控制权永远不会返回到泛型,因此在调用UseMethod
之后的任何表达式都不会执行。
在调用UseMethod
之前评估的泛型的任何参数都将保持评估状态。
如果UseMethod
的第一个参数未提供,则假定它是当前函数的名称。如果向UseMethod
提供两个参数,则第一个是方法的名称,第二个假定是要在其上进行分派的。它被评估,以便可以确定所需的方法。在这种情况下,对泛型的调用中的第一个参数不会被评估,并且会被丢弃。无法更改对方法调用的其他参数;这些参数保持与对泛型的调用中的参数相同。这与NextMethod
形成对比,在NextMethod
中,对下一个方法的调用的参数可以更改。
NextMethod
用于提供简单的继承机制。
作为对NextMethod
的调用结果调用的方法的行为就像它们是从先前的方法中调用的一样。继承方法的参数与当前方法的调用顺序相同,名称也相同。这意味着它们与对泛型的调用相同。但是,参数的表达式是当前方法的相应形式参数的名称。因此,参数的值将与其在调用 NextMethod 时对应的值相对应。
未评估的参数保持未评估状态。缺少的参数仍然缺失。
NextMethod
的调用语法为 NextMethod(generic, object, ...)
。如果未提供 generic
,则使用 .Generic
的值。如果未提供 object
,则使用对当前方法调用的第一个参数。...
参数中的值用于修改下一个方法的参数。
重要的是要意识到,下一个方法的选择取决于 .Generic
和 .Class
的当前值,而不是对象。因此,在对 NextMethod
的调用中更改对象会影响下一个方法接收到的参数,但不会影响下一个方法的选择。
方法可以直接调用。如果是这样,则不会有 .Generic
、.Class
或 .Method
。在这种情况下,必须指定 NextMethod
的 generic
参数。.Class
的值被认为是作为当前函数的第一个参数的对象的类属性。.Method
的值是当前函数的名称。这些默认值的选取确保了方法的行为不会因其是直接调用还是通过对泛型的调用而改变。
一个需要讨论的问题是 NextMethod
的 ...
参数的行为。白皮书将该行为描述如下:
- 命名参数替换对当前方法调用的相应参数。未命名的参数位于参数列表的开头。
我想做的是:
- 首先对 NextMethod 进行参数匹配;- 如果对象或泛型被更改,则很好 - 首先,如果命名的列表元素与参数(命名或未命名)匹配,则列表值将替换参数值。- 第一个未命名的列表元素
查找值:类:首先来自 .Class,其次来自方法的第一个参数,最后来自对 NextMethod 的调用中指定的对象
通用:首先来自 .Generic,如果为空则来自方法的第一个参数,如果仍然缺失则来自对 NextMethod 的调用。
方法:这应该只是当前函数名。
对于几种类型的 内部函数,R 提供了一种用于运算符的调度机制。这意味着诸如 ==
或 <
之类的运算符可以针对特殊类的成员修改其行为。函数和运算符已分为三类,并且可以为每类编写组方法。目前还没有添加组的机制。可以为组内的任何函数编写特定方法。
下表列出了不同组的函数。
abs, acos, acosh, asin, asinh, atan, atanh, ceiling, cos, cosh, cospi, cumsum, exp, floor, gamma, lgamma, log, log10, round, signif, sin, sinh, sinpi, tan, tanh, tanpi, trunc
all, any, max, min, prod, range, sum
+
, -
, *
, /
, ^
, <
, >
,
<=
, >=
, !=
, ==
, %%
, %/%
,
&
, |
, !
对于 Ops 组中的运算符,如果两个操作数一起表明一个单一方法,则会调用一个特殊方法。具体来说,如果两个操作数都对应于相同的方法,或者一个操作数对应于优先于另一个操作数的方法。如果它们没有表明一个单一方法,则使用默认方法。如果另一个操作数没有对应的方法,则组方法或类方法占主导地位。类方法优先于组方法。
当组为 Ops 时,特殊变量 .Method
是一个包含两个元素的字符向量。如果对应参数是用于确定方法的类的成员,则 .Method
的元素将设置为方法的名称。否则,.Method
的对应元素将设置为零长度字符串,""
。
用户可以轻松编写自己的方法和泛型函数。一个 泛型函数只是一个包含对 UseMethod
的调用的函数。方法只是一个通过方法调度调用的函数。这可能是对 UseMethod
或 NextMethod
的调用的结果。
值得记住的是,方法可以直接调用。这意味着它们可以在没有调用 UseMethod
的情况下输入,因此特殊变量 .Generic
、.Class
和 .Method
不会被实例化。在这种情况下,将使用上面详细介绍的默认规则来确定这些变量。
最常见的 泛型函数用法是为统计对象提供 print
和 summary
方法,通常是某些模型拟合过程的输出。为此,每个模型都会将其输出附加到一个类属性,然后提供一个特殊方法,该方法接受该输出并提供其易于阅读的版本。然后,用户只需要记住 print
或 summary
将为任何分析的结果提供良好的输出。
R 属于一类编程语言,其中子例程能够修改或构建其他子例程,并将结果作为语言本身的组成部分进行评估。这类似于 Lisp 和 Scheme 以及其他“函数式编程”类型的语言,但与 FORTRAN 和 ALGOL 家族形成对比。Lisp 家族通过“一切都是列表”的范式将此功能发挥到极致,在这个范式中,程序和数据之间没有区别。
与 Lisp 相比,R 为编程提供了更友好的界面,至少对于习惯数学公式和 C 类控制结构的人来说是如此,但引擎实际上非常类似于 Lisp。R 允许直接访问 已解析的表达式和函数,并允许您更改和随后执行它们,或从头开始创建全新的函数。
此设施有许多标准应用,例如计算表达式的解析导数,或从系数向量生成多项式函数。然而,也有一些应用对 R 解释部分的工作原理更为基础。其中一些对于将函数作为其他函数中的组件重复使用至关重要,例如在几个建模和绘图例程中构建的(不可否认不太美观的)对 model.frame
的调用。其他用途只是允许对有用功能进行优雅的接口。例如,考虑 curve
函数,它允许您绘制以表达式(如 sin(x)
)给出的函数的图形,或者绘制数学表达式的工具。
本章介绍了可用于对语言进行计算的一组工具。
有三种语言对象可供修改,分别是调用、表达式和函数。在这一点上,我们将集中讨论调用对象。这些有时被称为“未评估表达式”,尽管这种术语有点令人困惑。获取调用对象的直接方法是使用 quote
并带有一个表达式参数,例如:
> e1 <- quote(2 + 2) > e2 <- quote(plot(x, y))
参数不会被评估,结果只是解析后的参数。对象 e1
和 e2
可以在以后使用 eval
进行评估,或者简单地作为数据进行操作。也许最直接的理由是为什么 e2
对象的模式为 "call"
,因为它涉及对 plot
函数的调用,并带有一些参数。然而,e1
实际上与对二元运算符 +
的调用具有完全相同的结构,带两个参数,这是一个事实,通过以下内容可以清楚地显示出来
> quote("+"(2, 2)) 2 + 2
调用对象的组件使用类似列表的语法进行访问,实际上可以使用 as.list
和 as.call
将它们转换为列表或从列表转换而来
> e2[[1]] plot > e2[[2]] x > e2[[3]] y
当使用关键字参数匹配时,可以使用关键字作为列表标签
> e3 <- quote(plot(x = age, y = weight)) > e3$x age > e3$y weight
在前面的示例中,调用对象的全部组件的模式均为 "name"
。对于调用中的标识符来说,这是正确的,但是调用的组件也可以是常量——可以是任何类型,尽管如果要成功评估调用,第一个组件最好是一个函数——或者其他调用对象,对应于子表达式。可以使用 as.name
从字符串构造模式为 name 的对象,因此可以修改 e2
对象,如下所示
> e2[[1]] <- as.name("+") > e2 x + y
为了说明子表达式只是本身也是调用的组件这一事实,请考虑以下内容
> e1[[2]] <- e2 > e1 x + y + 2
输入中的所有分组括号在解析的表达式中都被保留。它们被表示为一个带有一个参数的函数调用,因此 4 - (2 - 2)
在前缀表示法中变为 "-"(4, "(" ("-"(2, 2)))
。在评估中,‘(’ 运算符只返回其参数。
这有点不幸,但编写一个 解析器/反解析器组合,既能保留用户输入,又能以最小形式存储它,并确保解析反解析的表达式会得到相同的表达式,这并不容易。
碰巧的是,R 的解析器并不完全可逆,它的反解析器也不可逆,如下面的例子所示
> str(quote(c(1,2))) language c(1, 2) > str(c(1,2)) num [1:2] 1 2 > deparse(quote(c(1,2))) [1] "c(1, 2)" > deparse(c(1,2)) [1] "c(1, 2)" > quote("-"(2, 2)) 2 - 2 > quote(2 - 2) 2 - 2
然而,反解析的表达式应该评估为与原始表达式等效的值(在舍入误差范围内)。
...流程控制结构的内部存储...注意 Splus 不兼容...
事实上,人们并不经常想要像上一节那样修改表达式的内部结构。更常见的是,人们只是想得到一个表达式,以便反解析它并将其用于标记绘图,例如。在 plot.default
的开头可以看到一个例子:
xlabel <- if (!missing(x)) deparse(substitute(x))
这会导致作为 x
参数传递给 plot
的变量或表达式稍后用于标记 x 轴。
实现此功能的函数是 substitute
,它接受表达式 x
并替换通过形式参数 x
传递的表达式。请注意,为了使此操作发生,x
必须包含有关创建其值的表达式的信息。这与 R 的 惰性求值方案有关(参见 Promise 对象)。形式参数实际上是一个 promise,一个具有三个槽的对象,一个用于定义它的表达式,一个用于评估该表达式的环境,以及一个用于评估该表达式后的值。 substitute
将识别一个 promise 变量并替换其表达式槽的值。如果 substitute
在函数内部调用,则函数的局部变量也会被替换。
substitute
的参数不必是简单的标识符,它可以是包含多个变量的表达式,并且将对这些变量中的每一个进行替换。此外,substitute
还有一个额外的参数,它可以是查找变量的环境或列表。例如
> substitute(a + b, list(a = 1, b = quote(x))) 1 + x
请注意,引用对于替换 x
是必要的。这种构造在将数学表达式放入图形的设施方面非常有用,如下例所示
> plot(0) > for (i in 1:4) + text(1, 0.2 * i, + substitute(x[ix] == y, list(ix = i, y = pnorm(i))))
重要的是要意识到替换纯粹是词法上的;如果对结果的调用对象进行评估,不会检查它们是否有意义。 substitute(x <- x + 1, list(x = 2))
将愉快地返回 2 <- 2 + 1
。但是,R 的某些部分制定了自己的规则来确定什么有意义,什么没有意义,并且实际上可能对这种格式错误的表达式有用。例如,使用“图形中的数学”功能通常涉及语法上正确的构造,但评估起来毫无意义,例如 ‘{}>=40*" years"’。
Substitute 不会评估其第一个参数。这导致了如何对包含在变量中的对象进行替换的难题。解决方案是再次使用 substitute
,如下所示
> expr <- quote(x + y) > substitute(substitute(e, list(x = 3)), list(e = expr)) substitute(x + y, list(x = 3)) > eval(substitute(substitute(e, list(x = 3)), list(e = expr))) 3 + y
替换的确切规则如下:第一个的 解析树中的每个 符号都与第二个参数匹配,第二个参数可以是带标签的列表或环境帧。如果它是一个简单的局部对象,则会插入它的值,除非与全局环境匹配。如果它是一个 promise(通常是函数参数),则会替换 promise 表达式。如果符号没有匹配,则保持不变。在顶层进行替换的特殊例外无疑是奇怪的。它继承自 S,其基本原理很可能是,无法控制在该级别可能绑定哪些变量,因此最好让 substitute 充当 quote
。
如果在使用 substitute
之前修改了局部变量,则 promise 替换规则与 S 的规则略有不同。R 将使用变量的新值,而 S 将无条件地使用参数表达式,除非它是常量,这会导致 f((1))
可能与 f(1)
在 S 中有很大不同。R 规则要干净得多,尽管它确实与 惰性求值有关,这会让一些人感到意外。考虑
logplot <- function(y, ylab = deparse(substitute(y))) { y <- log(y) plot(y, ylab = ylab) }
这看起来很简单,但你会发现 y 标签变成了一个难看的 c(...)
表达式。这是因为惰性求值规则导致 ylab
表达式的求值发生在 之后 y
被修改。解决方法是强制 ylab
首先求值,即
logplot <- function(y, ylab = deparse(substitute(y))) { ylab y <- log(y) plot(y, ylab = ylab) }
请注意,在这种情况下不应该使用 eval(ylab)
。如果 ylab
是一个语言或表达式对象,那么这会导致该对象也被求值,如果传递的是像 quote(log[e](y))
这样的数学表达式,这将是不可取的。
substitute
的一个变体是 bquote
,它用于将一些子表达式替换为它们的值。上面的例子
> plot(0) > for (i in 1:4) + text(1, 0.2 * i, + substitute(x[ix] == y, list(ix = i, y = pnorm(i))))
可以更简洁地写成
plot(0) for(i in 1:4) text(1, 0.2*i, bquote( x[.(i)] == .(pnorm(i)) ))
除了 .()
子表达式的内容外,表达式都被引用,这些内容被替换为它们的值。有一个可选参数可以在不同的环境中计算值。 bquote
的语法借鉴了 LISP 的反引号宏。
本章前面介绍了 eval
函数作为评估调用对象的一种方式。然而,这并非全部。还可以指定 评估发生的 环境。默认情况下,这是调用 eval
的评估框架,但很多时候需要将其设置为其他内容。
通常,相关的评估框架是当前框架的父框架(参见 ???)。特别是,当要评估的对象是函数参数的 substitute
操作的结果时,它将包含对调用者有意义的变量(注意,没有理由期望调用者的变量在 被调用者的词法范围内)。由于在父框架中进行评估的情况很常见,因此存在一个 eval.parent
函数作为 eval(expr, sys.frame(sys.parent()))
的简写。
另一个经常出现的情况是在列表或数据框中进行评估。例如,这在与 model.frame
函数相关联时发生,当给出 data
参数时。通常,模型公式的项需要在 data
中进行评估,但它们偶尔也可能包含对 model.frame
调用者的项目的引用。这在与模拟研究相关的连接中有时很有用。因此,为此目的,不仅需要在列表中评估表达式,还需要指定一个封闭范围,如果变量不在列表中,则搜索将继续进行。因此,调用具有以下形式
eval(expr, data, sys.frame(sys.parent()))
请注意,在给定环境中进行评估实际上可能会改变该环境,最明显的是涉及 赋值运算符的情况,例如
eval(quote(total <- 0), environment(robert$balance)) # rob Rob
这在列表中进行评估时也是如此,但原始列表不会改变,因为实际上是在副本上进行操作。
模式为 "expression"
的对象在 表达式对象 中定义。它们与调用对象的列表非常相似。
> ex <- expression(2 + 2, 3 + 4) > ex[[1]] 2 + 2 > ex[[2]] 3 + 4 > eval(ex) [1] 7
请注意,评估表达式对象会依次评估每个调用,但最终值是最后一个调用的值。在这方面,它的行为几乎与复合语言对象 quote({2 + 2; 3 + 4})
相同。但是,存在细微的差异:调用对象与解析树中的子表达式无法区分。这意味着它们会以与子表达式相同的方式自动评估。表达式对象可以在评估期间被识别,并在某种程度上保留其引号。评估器不会递归地评估表达式对象,而是在将其直接传递给 eval
函数时才会评估,如上所示。差异可以这样看
> eval(substitute(mode(x), list(x = quote(2 + 2)))) [1] "numeric" > eval(substitute(mode(x), list(x = expression(2 + 2)))) [1] "expression"
解析器通过创建表达式对象的调用来表示该对象。这类似于它处理数值向量和几个没有特定外部表示的其他对象的方式。但是,这会导致以下一些困惑。
> e <- quote(expression(2 + 2)) > e expression(2 + 2) > mode(e) [1] "call" > ee <- expression(2 + 2) > ee expression(2 + 2) > mode(ee) [1] "expression"
例如,e
和 ee
打印出来看起来相同,但一个是生成表达式对象的调用,另一个是对象本身。
一个 函数可以通过查看 sys.call
的结果来了解它是如何被调用的,以下示例是一个简单地返回自身调用的函数。
> f <- function(x, y, ...) sys.call() > f(y = 1, 2, z = 3, 4) f(y = 1, 2, z = 3, 4)
但是,除了调试之外,这并没有什么用,因为它需要函数跟踪参数匹配才能解释调用。例如,它必须能够看到第二个实际参数与第一个形式参数匹配(上面的示例中的 x
)。
更常见的是,需要所有实际参数都绑定到相应形式参数的调用。为此,使用 match.call
函数。以下是前面示例的变体,一个返回自身调用并匹配参数的函数。
> f <- function(x, y, ...) match.call() > f(y = 1, 2, z = 3, 4) f(x = 2, y = 1, z = 3, 4)
注意,第二个参数现在与 x
匹配,并出现在结果中的相应位置。
这种技术的首要用途是使用相同的参数调用另一个函数,可能删除一些参数并添加其他参数。一个典型的应用是在 lm
函数的开头看到的。
mf <- cl <- match.call() mf$singular.ok <- mf$model <- mf$method <- NULL mf$x <- mf$y <- mf$qr <- mf$contrasts <- NULL mf$drop.unused.levels <- TRUE mf[[1]] <- as.name("model.frame") mf <- eval(mf, sys.frame(sys.parent()))
请注意,生成的调用是在父级框架中评估的,在该框架中可以确定所涉及的表达式是有意义的。该调用可以被视为一个列表对象,其中第一个元素是函数的名称,其余元素是实际参数表达式,其对应的形式参数名称作为标签。因此,消除不需要的参数的技术是分配NULL
,如第 2 行和第 3 行所示,并添加一个参数使用带标签的列表赋值(这里传递drop.unused.levels = TRUE
),如第 4 行所示。要更改调用的函数名称,请分配给列表的第一个元素,并确保该值是一个名称,可以使用as.name("model.frame")
构造或quote(model.frame)
。
match.call
函数有一个expand.dots
参数,这是一个开关,如果设置为FALSE
,则所有...
参数将被收集为一个带有标签...
的单个参数。
> f <- function(x, y, ...) match.call(expand.dots = FALSE) > f(y = 1, 2, z = 3, 4) f(x = 2, y = 1, ... = list(z = 3, 4))
...
参数是一个列表(更准确地说是一个配对列表),而不是像在 S 中那样对list
的调用。
> e1 <- f(y = 1, 2, z = 3, 4)$... > e1 $z [1] 3 [[2]] [1] 4
使用这种形式的match.call
的一个原因仅仅是为了摆脱任何...
参数,以便不将未指定的参数传递给可能不认识它们的函数。以下是一个来自plot.formula
的改写示例。
m <- match.call(expand.dots = FALSE) m$... <- NULL m[[1]] <- "model.frame"
一个更复杂的应用是在update.default
中,其中一组可选的额外参数可以添加、替换或取消原始调用的参数。
extras <- match.call(expand.dots = FALSE)$... if (length(extras) > 0) { existing <- !is.na(match(names(extras), names(call))) for (a in names(extras)[existing]) call[[a]] <- extras[[a]] if (any(!existing)) { call <- c(as.list(call), extras[!existing]) call <- as.call(call) } }
请注意,如果extras[[a]] == NULL
,则会谨慎地逐个修改现有参数。在没有强制转换的情况下,连接对调用对象不起作用;这可能是一个错误。
还有两个函数用于构造函数调用,即call
和do.call
。
函数call
允许从函数名称和参数列表创建调用对象。
> x <- 10.5 > call("round", x) round(10.5)
如您所见,在调用中插入的是 x
的值,而不是 符号,因此它与 round(x)
明显不同。这种形式很少使用,但在函数名称作为字符变量可用时偶尔有用。
函数 do.call
相关,但立即评估调用并从一个模式为 "list"
的对象中获取参数,该对象包含所有参数。这在您想要将类似 cbind
的函数应用于列表或数据框的所有元素时非常有用。
is.na.data.frame <- function (x) { y <- do.call(cbind, lapply(x, is.na)) rownames(y) <- row.names(x) y }
其他用途包括对类似 do.call("f", list(...))
的构造的变体。但是,您应该注意,这涉及在实际函数调用之前评估参数,这可能会破坏函数本身中的延迟评估和参数替换的方面。类似的评论适用于 call
函数。
能够操作 函数或闭包的组件通常很有用。R 提供了一组用于此目的的接口函数。
body
¶返回作为函数主体表达式的表达式。
formals
¶返回函数的形式参数列表。这是一个 pairlist
。
environment
¶返回与函数关联的环境。
body<-
¶这将函数的主体设置为提供的表达式。
formals<-
¶将函数的形式参数设置为提供的列表。
environment<-
¶将函数的环境设置为指定的环境。
也可以使用类似于 evalq(x <- 5, environment(f))
的代码来更改函数环境中不同变量的绑定。
还可以使用 as.list
将 函数转换为列表。结果是将形式参数列表与函数体连接起来。反之,可以使用 as.function
将这样的列表转换为函数。此功能主要用于与 S 兼容。请注意,使用 as.list
时会丢失环境信息,而 as.function
具有允许设置环境的参数。
通过 R 函数 system
访问操作系统 shell。 具体细节因平台而异(参见在线帮助),并且可以安全地假设的是,第一个参数将是一个字符串 command
,它将被传递以执行(不一定由 shell 执行),第二个参数将是 internal
,如果为真,则将收集命令的输出到 R 字符向量中。
函数 system.time
和 proc.time
可用于计时(尽管在非类 Unix 平台上可用的信息可能有限)。
Sys.getenv
操作系统环境变量 Sys.putenv
Sys.getlocale
系统区域设置 Sys.putlocale
Sys.localeconv
Sys.time
当前时间 Sys.timezone
时区
所有平台都提供了一套统一的文件访问函数
file.access
确定文件可访问性 file.append
连接文件 file.choose
提示用户输入文件名 file.copy
复制文件 file.create
创建或截断文件 file.exists
测试是否存在 file.info
其他文件信息 file.remove
删除文件 file.rename
重命名文件 file.show
显示文本文件 unlink
删除文件或目录。
还有一些函数用于以平台无关的方式操作文件名和路径。
basename
不含目录的文件名 dirname
目录名 file.path
构造文件路径 path.expand
扩展 Unix 路径中的 ~
有关通过编译代码向 R 添加功能的详细信息,请参阅 Writing R Extensions 中的 系统和外语接口。
函数 .C
和 .Fortran
提供了一个标准接口,用于访问已链接到 R 的编译代码,这些代码可以在构建时或通过 dyn.load
链接。它们主要用于编译的 C 和 FORTRAN 代码,但 .C
函数可用于其他可以生成 C 接口的语言,例如 C++。
函数 .Call
和 .External
提供了接口,允许编译代码(主要是编译的 C 代码)操作 R 对象。
.Internal
和 .Primitive
接口用于调用在构建时编译到 R 的 C 代码。请参阅 R Internals 中的 .Internal vs .Primitive。
R 中的异常处理机制通过两种方式提供。函数(如 stop
或 warning
)可以直接调用,或者可以使用 "warn"
等选项来控制问题的处理方式。
函数 warning
接受一个字符字符串作为参数。调用 warning
的行为取决于选项 "warn"
的值。如果 "warn"
为负数,则忽略警告。如果为零,则存储警告并在顶层函数完成后打印。如果为 1,则在发生时打印警告;如果为 2(或更大),则将警告转换为错误。
如果 "warn"
为零(默认值),则会创建一个变量 last.warning
,并将与每次调用 warning
关联的消息按顺序存储在这个向量中。如果警告少于 10 个,则在函数完成评估后打印它们。如果超过 10 个,则会打印一条消息,指示发生了多少个警告。在这两种情况下,last.warning
都包含消息向量,而 warnings
提供了一种访问和打印它的方法。
函数可以在函数体中的任何位置插入对 on.exit
的调用。调用 on.exit
的效果是存储代码体的值,以便在函数退出时执行它。这允许函数更改某些系统参数,并确保在函数完成时将它们重置为适当的值。当函数直接退出或由于警告而退出时,保证 on.exit
会被执行。
在评估 on.exit
代码时发生的错误会导致立即跳转到顶层,而不会进一步处理 on.exit
代码。
on.exit
接受一个参数,该参数是在函数退出时要执行的表达式。
有一些 options
变量可以用来控制 R 如何处理错误和警告。它们列在下面的表格中。
控制警告的打印。
设置一个表达式,该表达式将在发生警告时执行。如果设置了此选项,则会抑制警告的正常打印。
安装一个表达式,该表达式将在发生错误时执行。错误消息和警告消息的正常打印在表达式执行之前进行。
通过 options("error")
安装的表达式在执行 on.exit
的调用之前执行。
可以使用 options(error = expression(q("yes")))
使 R 在出现错误时退出。在这种情况下,错误会导致 R 关闭,并且全局环境将被保存。
调试代码一直是一门艺术。R 提供了一些工具来帮助用户查找代码中的问题。这些工具会在代码中的特定点暂停执行,并且可以检查计算的当前状态。
大多数调试都是通过调用 browser
或 debug
来完成的。这两个函数都依赖于相同的内部机制,并且都为用户提供了一个特殊的提示。任何命令都可以在提示符下键入。命令的评估 环境是当前活动的環境。这允许您检查任何变量等的当前状态。
R 对五个特殊命令的解释不同。它们是:
如果正在调试函数,则转到下一条语句。如果浏览器被调用,则继续执行。
继续执行。
执行函数中的下一条语句。这在浏览器中也有效。
显示调用堆栈
立即停止执行并跳转到顶层。
如果存在与上述特殊命令之一同名的局部变量,则可以使用 get
访问其值。使用带引号的名称调用 get
将检索当前 环境中的值。
调试器仅提供对解释表达式的访问。如果函数调用外语(例如 C),则不提供对该语言中语句的访问。执行将在 R 中评估的下一条语句处停止。可以使用诸如 gdb
之类的符号调试器来调试编译后的代码。
调用函数 browser
会导致 R 在该点停止执行,并为用户提供一个特殊的提示。对 browser
的参数将被忽略。
> foo <- function(s) { + c <- 3 + browser() + } > foo(4) Called from: foo(4) Browse[1]> s [1] 4 Browse[1]> get("c") [1] 3 Browse[1]>
可以使用命令 debug(fun)
在任何函数上调用调试器。随后,每次评估该函数时都会调用调试器。调试器允许您控制函数主体中语句的评估。在执行每个语句之前,会打印出该语句并提供一个特殊的提示。可以给出任何命令,上表中的命令具有特殊含义。
通过调用带有函数作为参数的 undebug
函数来关闭调试。
> debug(mean.default) > mean(1:10) debugging in: mean.default(1:10) debug: { if (na.rm) x <- x[!is.na(x)] trim <- trim[1] n <- length(c(x, recursive = TRUE)) if (trim > 0) { if (trim >= 0.5) return(median(x, na.rm = FALSE)) lo <- floor(n * trim) + 1 hi <- n + 1 - lo x <- sort(x, partial = unique(c(lo, hi)))[lo:hi] n <- hi - lo + 1 } sum(x)/n } Browse[1]> debug: if (na.rm) x <- x[!is.na(x)] Browse[1]> debug: trim <- trim[1] Browse[1]> debug: n <- length(c(x, recursive = TRUE)) Browse[1]> c exiting from: mean.default(1:10) [1] 5.5
另一种监控 R 行为的方式是通过 trace
机制。 trace
函数接受一个参数,即要跟踪的函数的名称。该名称不需要加引号,但对于某些函数,您可能需要加引号以避免语法错误。
当对某个函数调用 trace
后,每次该函数被执行时,都会打印出对该函数的调用。通过调用带有函数作为参数的 untrace
函数来移除该机制。
> trace("[<-") > x <- 1:10 > x[3] <- 4 trace: "[<-"(*tmp*, 3, value = 4)
当错误导致跳转到顶层时,一个名为 .Traceback
的特殊变量会被放置到基本环境中。 .Traceback
是一个字符向量,其中每个条目对应于错误发生时处于活动状态的每个函数调用。可以通过调用 traceback
函数来检查 .Traceback
。
解析器将 R 代码的文本表示转换为内部形式,然后可以将其传递给 R 评估器,从而执行指定的指令。内部形式本身是一个 R 对象,可以在 R 系统中保存和操作。
R 中的解析有三种不同的变体
读-求值-打印循环构成了 R 的基本命令行界面。文本输入将一直读取,直到获得完整的 R 表达式。表达式可以跨多行输入。主提示符(默认情况下为‘> ’)表示解析器已准备好接受新的表达式,而继续提示符(默认情况下为‘+ ’)表示解析器正在等待不完整表达式的其余部分。表达式在输入期间被转换为内部形式,解析后的表达式被传递给评估器,结果被打印(除非被明确设置为不可见)。如果解析器发现自己处于与语言语法不兼容的状态,则会标记“语法错误”,解析器会重置自身并在下一行输入的开头恢复输入。
可以使用 parse
函数解析文本文件。特别是,这在执行 source
函数期间完成,该函数允许将命令存储在外部文件中,并像在键盘上键入一样执行它们。但是,请注意,整个文件将在任何评估发生之前被解析和语法检查。
可以使用 parse
的 text=
参数解析字符字符串或其向量。这些字符串的处理方式与输入文件中的行完全相同。
解析后的表达式存储在一个包含解析树的 R 对象中。有关此类对象的更详细描述,请参见 语言对象 和 表达式对象。简而言之,每个基本 R 表达式都存储在 函数调用形式中,作为一个列表,第一个元素包含函数名称,其余元素包含参数,这些参数可能反过来又是进一步的 R 表达式。列表元素可以命名,对应于形式参数和实际参数的标记匹配。请注意,所有 R 语法元素都以这种方式处理,例如,赋值 x <- 1
被编码为 "<-"(x, 1)
。
任何 R 对象都可以使用 deparse
转换为 R 表达式。这在输出结果时经常使用,例如,用于标记绘图。请注意,只有模式为 "expression"
的对象才能预期在反解析输出的重新解析后保持不变。例如,数值向量 1:5
将反解析为 "c(1, 2, 3, 4, 5)"
,这将重新解析为对函数 c
的调用。在尽可能的情况下,评估反解析和重新解析的表达式会产生与评估原始表达式相同的结果,但存在一些尴尬的例外,主要涉及最初不是从文本表示生成的表达式。
R 中的注释被解析器忽略。从 #
字符到行尾的任何文本都被视为注释,除非 #
字符位于引号字符串内。例如,
> x <- 1 # This is a comment... > y <- " #... but this is not."
词法单元是编程语言的基本构建块。它们在词法分析期间被识别,词法分析(至少在概念上)发生在解析器本身执行的语法分析之前。
有五种类型的常量:整数、逻辑、数值、复数和字符串。
此外,还有四个特殊常量,NULL
、NA
、Inf
和 NaN
。
NULL
用于表示空对象。 NA
用于表示缺失(“不可用”)数据值。 Inf
表示无穷大,NaN
是 IEEE 浮点运算中的非数字(例如,分别为 1/0 和 0/0 操作的结果)。
逻辑常量要么是 TRUE
要么是 FALSE
。
数值常量的语法类似于 C 语言。它们由一个包含零个或多个数字的整数部分组成,后面可选地跟着 ‘.’ 和一个包含零个或多个数字的小数部分,后面可选地跟着一个指数部分,该部分由 ‘E’ 或 ‘e’、一个可选的符号和一个或多个数字的字符串组成。小数部分或整数部分可以为空,但不能同时为空。
Valid numeric constants: 1 10 0.1 .2 1e-7 1.2e+7
数值常量也可以是十六进制,以 ‘0x’ 或 ‘0x’ 开头,后面跟着零个或多个数字、‘a-f’ 或 ‘A-F’。十六进制浮点常量使用 C99 语法支持,例如 ‘0x1.1p1’。
现在有一类独立的整数常量。它们通过在数字末尾使用限定符 L
来创建。例如,123L
给出一个整数值而不是数值。后缀 L
可用于限定任何非复数,目的是创建整数。因此,它可用于十六进制或科学记数法给出的数字。但是,如果该值不是有效的整数,则会发出警告并创建数值。以下显示了有效整数常量的示例、将生成警告并给出数值常量以及语法错误的值。
Valid integer constants: 1L, 0x10L, 1000000L, 1e6L Valid numeric constants: 1.1L, 1e-3L, 0x1.1p-2 Syntax error: 12iL 0x1.1
对于包含不必要的十进制点的十进制值,例如 1.L
,会发出警告。在十六进制常量中没有二进制指数的情况下使用小数点是错误的。
还要注意,前面的符号 (+
或 -
) 被视为一元运算符,而不是常量的一部分。
有关当前接受格式的最新信息,请参见 ?NumericConstants
。
复数常量的形式为十进制数值常量后跟 ‘i’。请注意,只有纯虚数才是真正的常量,其他复数被解析为数值和虚数上的单目或二元运算。
Valid complex constants: 2i 4.1i 1e-2i
字符串常量由一对单引号 (‘'’) 或双引号 (‘"’) 括起来,可以包含所有其他可打印字符。字符串中的引号和其他特殊字符使用 转义序列 指定
\'
单引号
\"
双引号
\n
换行符(也称为“换行符”,LF)
\r
回车符 (CR)
\t
制表符
\b
退格键
\a
响铃
\f
换页符
\v
垂直制表符
\\
反斜杠本身
\nnn
具有给定八进制代码的字符 - 接受范围为 0 ... 7
的一位、两位或三位数字序列。
\xnn
具有给定十六进制代码的字符 - 一位或两位十六进制数字序列(包含条目 0 ... 9 A ... F a ... f
)。
\unnnn \u{nnnn}
(在支持多字节语言环境的情况下,否则为错误)。具有给定十六进制代码的 Unicode 字符 - 最多四位十六进制数字序列。该字符需要在当前语言环境中有效。
\Unnnnnnnn \U{nnnnnnnn}
(在支持多字节语言环境的情况下,否则会报错)。给定十六进制代码的 Unicode 字符 - 最多八个十六进制数字的序列。
单引号也可以直接嵌入双引号分隔的字符串中,反之亦然。
字符字符串中不允许使用“空字符”(\0
),因此在字符串常量中使用 \0
会终止常量(通常会发出警告):直到结束引号的后续字符会被扫描但会被忽略。
标识符由字母、数字、句点(‘.’)和下划线组成。它们不能以数字、下划线或句点后跟数字开头。
字母的定义取决于当前语言环境:允许的精确字符集由 C 表达式 (isalnum(c) || c == ‘.’ || c == ‘_’)
给出,并且在许多西欧语言环境中将包括带重音字母。
请注意,以句点开头的标识符默认情况下不会被 ls
函数列出,并且 ...
和 ..1
、..2
等是特殊的。
还要注意,对象可以具有不是标识符的名称。这些通常通过 get
和 assign
访问,尽管在没有歧义的情况下(例如 "x" <- 1
),它们也可以在某些有限情况下由文本字符串表示。由于 get
和 assign
不限于标识符的名称,因此它们不识别下标运算符或替换函数。以下对 不等效
x$a<-1
assign("x$a",1)
x[[1]]
get("x[[1]]")
names(x)<-nm
assign("names(x)",nm)
以下标识符具有特殊含义,不能用作对象名称
if else repeat while function for in next break
TRUE FALSE NULL Inf NaN
NA NA_integer_ NA_real_ NA_complex_ NA_character_
... ..1 ..2 etc.
R 允许用户定义中缀运算符。这些运算符的形式为用 ‘%’ 字符分隔的字符字符串。该字符串可以包含除 ‘%’ 之外的任何可打印字符。字符串的转义序列在此处不适用。
请注意,以下运算符是预定义的
%% %*% %/% %in% %o% %x%
虽然不是严格的标记,但空白字符(空格、制表符和换页符,在 Windows 和 UTF-8 本地化中还有其他 Unicode 空白字符5)的连续出现用于在出现歧义的情况下分隔标记(比较 x<-5
和 x < -5
)。
换行符的功能是标记分隔符和表达式终止符的组合。如果表达式可以在行尾终止,解析器将假设它确实如此,否则换行符将被视为空白字符。分号 (‘;’) 可用于在同一行上分隔基本 表达式。
特殊规则适用于 else
关键字:在复合表达式中,else
之前的换行符将被丢弃,而在最外层,换行符将终止 if
结构,而随后的 else
将导致语法错误。这种有点反常的行为发生是因为 R 应该可以在交互模式下使用,然后它必须在用户按下 RET 时立即决定输入表达式是完整的、不完整的还是无效的。
逗号 (‘,’) 用于分隔函数参数和多个索引。
R 使用以下运算符标记
+ - * / %% ^
算术运算符 > >= < <= == !=
关系运算符 ! & |
逻辑型 ~
模型公式 -> <-
赋值运算符 $
列表索引 :
序列
(一些运算符在模型公式中具有不同的含义)
普通括号 - ‘(’ 和 ‘)’ - 用于表达式中的显式分组,以及界定函数定义和函数调用的参数列表。
花括号 - ‘{’ 和 ‘}’ - 界定函数定义、条件表达式和迭代结构中的表达式块。
R 程序由一系列 R 表达式组成。表达式可以是简单的表达式,仅包含常量或标识符,也可以是复合表达式,由其他部分(这些部分本身也可能是表达式)构成。
以下部分详细介绍了可用的各种语法结构。
函数调用采用函数引用后跟一组括号内的逗号分隔的参数列表的形式。
function_reference ( arg1, arg2, ...... , argn )
函数引用可以是
每个参数都可以被标记(tag=expr
),或者只是一个简单的表达式。它也可以为空,或者可以是特殊标记之一 ...
, ..2
等。
标记可以是标识符或文本字符串。
示例
f(x) g(tag = value, , 5) "odd name"("strange tag" = 5, y) (function(x) x^2)(5)
运算符的优先级顺序(从高到低)为
:: $ @ ^ - + (unary) : %xyz% |> * / + - (binary) > >= < <= == != ! & && | || ~ (unary and binary) -> ->> <- <<- = (as assignment)
请注意,:
优先于二元 +/-, 但不优先于 ^
。因此,1:3-1
是 0 1 2,但 1:2^3
是 1:8
。
指数运算符 ‘^’ 和 左赋值加减运算符 ‘<- - = <<-’ 从右到左分组,所有其他运算符从左到右分组。也就是说,2 ^ 2 ^ 3
是 2 ^ 8,而不是 4 ^ 3,而 1 - 1 - 1
是 -1,而不是 1。
请注意,用于整数余数和除法的运算符 %%
和 %/%
的优先级高于乘法和除法。
虽然它严格来说不是一个运算符,但还需要提及的是,‘=’ 符号用于在函数调用中标记参数,并在函数定义中分配默认值。
‘$’ 符号在某种程度上是一个运算符,但不允许任意右侧,并在 索引构造 中讨论。它的优先级高于任何其他运算符。
一元或二元运算的解析形式完全等同于以运算符为函数名,以操作数为函数参数的函数调用。
圆括号被记录为等效于一元运算符,名称为 "("
,即使在圆括号可以从运算符优先级推断出来的情况下(例如,a * (b + c)
)。
请注意, 赋值符号与算术、关系和逻辑运算符一样是运算符。任何表达式都可以在赋值的目标侧使用,只要解析器认为是有效的(2 + 2 <- 5
对于解析器来说是一个有效的表达式。但是,评估器会提出异议)。类似的评论适用于模型公式运算符。
R 有三种索引构造,其中两种在语法上相似,但语义略有不同
object [ arg1, ...... , argn ] object [[ arg1, ...... , argn ]]
object 形式上可以是任何有效的表达式,但它被理解为表示或评估为可子集的对象。参数通常评估为数字或字符索引,但其他类型的参数也是可能的(特别是 drop = FALSE
)。
在内部,这些索引构造被存储为函数调用,函数名称分别为 "["
和 "[["
。
第三种索引构造是
object $ tag
这里,object 与上面一样,而 tag 是一个标识符或文本字符串。在内部,它被存储为一个函数调用,函数名称为 "$"
复合表达式具有以下形式
{ expr1 ; expr2 ; ...... ; exprn }
分号可以替换为换行符。在内部,这被存储为一个函数调用,函数名称为 "{"
,表达式作为参数。
R 包含以下控制结构作为特殊的语法结构
if ( cond ) expr if ( cond ) expr1 else expr2 while ( cond ) expr repeat expr for ( var in list ) expr
这些结构中的表达式通常是复合表达式。
在循环结构(while
、repeat
、for
)中,可以使用 break
(终止循环)和 next
(跳到下一个迭代)。
在内部,这些结构被存储为函数调用
"if"(cond, expr) "if"(cond, expr1, expr2) "while"(cond, expr) "repeat"(expr) "for"(var, list, expr) "break"() "next"()
function ( arglist ) body
函数体是一个表达式,通常是一个复合表达式。 arglist 是一个用逗号分隔的项目列表,每个项目可以是标识符,或者形式为 ‘identifier = default’,或者特殊标记 ...
。 default 可以是任何有效的表达式。
请注意,与列表标签等不同,函数参数不能使用作为文本字符串给出的“奇怪名称”。
在内部,函数定义被存储为一个函数调用,函数名为 function
,有两个参数,arglist 和 body。 arglist 被存储为一个带标签的配对列表,其中标签是参数名称,值是默认表达式。
解析器目前只支持一个指令,#line
。这类似于同名的 C 预处理器指令。语法为
#line nn [ "filename"
]
其中 nn 是一个整数行号,可选的 filename(在必需的双引号中)命名源文件。
与 C 指令不同,#line
必须出现在一行的前五个字符中。与 C 一样,nn 和 "filename"
条目可以用空格与它分隔。与 C 不同的是,该行上的任何后续文本将被视为注释并被忽略。
此指令告诉解析器,应将下一行假定为文件 filename 的第 nn 行。(如果未给出文件名,则假定它与前一个指令相同。)这通常不用于用户,但可用于预处理器,以便诊断消息引用原始文件。
跳转到: | .
[
#
$
A B D E F G I M N O P Q R S T U W |
---|
跳转到: | .
[
#
$
A B D E F G I M N O P Q R S T U W |
---|
跳转到: | .
#
A B C E F I M N O P S T V |
---|
跳转到: | .
#
A B C E F I M N O P S T V |
---|
Richard A. Becker, John M. Chambers 和 Allan R. Wilks (1988), The New S Language. Chapman & Hall, New York. 这本书通常被称为“蓝皮书”。