PAYATHON 2026

在 dplyr 函数中创建新列,应使用 !! 还是 {{ }}?

数据小许

应该选择哪种写法?

先确定函数接收列名字符串,还是不带引号的名称。对于题目中这种只指定输出列名的函数,建议使用字符串接口,写成 "{col_name}" := "new";原来的 !!col_name := "new" 也可以。如果希望调用者写 add_col(cars, new_col),则使用 {{ col_name }}。

两种接口都合理。dplyr 官方文档分别讨论了从环境变量中的字符串生成名称和从用户传入的表达式生成名称,并展示了 glue 插值和 embracing 写法。参考 dplyr 编程指南

为什么带引号时相同,不带引号时不同?

在 := 左侧,{{ col_name }} 捕获调用者传入的表达式,再用捕获结果命名。因此,add_col1(cars, new_col) 可以使用名称 new_col,不要求环境中已经存在同名对象。

!!col_name 则先对 col_name 求值,再注入所得结果。调用 add_col2(cars, new_col) 时,R 会尝试在调用环境中查找对象 new_col;如果不存在,就会报错。即使数据中恰好有同名列,也无法满足这里的环境求值。参考 rlang 的注入操作符说明

差异在下面的调用中更明显:

nm <- "new_col"

add_col1(cars, nm)  # 创建名为 nm 的列
add_col2(cars, nm)  # 创建名为 new_col 的列

第一种使用传入的名称 nm,第二种使用对象 nm 保存的字符串值。两种函数接收字符串字面量时虽然结果相同,语义却不同。

!! 也没有强制调用处必须写引号,上面的 nm 就是合法输入。若要把接口限制为字符串,需要显式校验参数。

字符串列名接口怎么写?

如果列名来自配置、循环或其他计算结果,使用字符串接口通常更方便:

add_col <- function(data, col_name) {
  stopifnot(
    is.character(col_name),
    length(col_name) == 1L,
    !is.na(col_name),
    nzchar(col_name)
  )

  dplyr::mutate(data, "{col_name}" := "new")
}

add_col(cars, "new_col")

nm <- "another_col"
add_col(cars, nm)

这里的 "{col_name}" 读取参数的字符串值。也可以把函数最后一行写成:

dplyr::mutate(data, !!col_name := "new")

对于通过上述校验的列名,两种写法表达的意图相同。使用 glue 命名语法时,无需自行调用 glue::glue();若在 R 包中使用这种写法,应按 rlang 文档声明对 glue 的依赖。参考 rlang 名称注入说明

裸名称接口与 enquo() 怎么选?

如果更重视类似 dplyr 的交互调用方式,可以保留第一种写法:

add_col <- function(data, col_name) {
  dplyr::mutate(data, {{ col_name }} := "new")
}

add_col(cars, new_col)
add_col(cars, "new_col")

这里的 {{ col_name }} 一步完成捕获和注入。显式写法如下:

add_col <- function(data, col_name) {
  col_name <- rlang::enquo(col_name)
  dplyr::mutate(data, !!col_name := "new")
}

这也是让原来的 add_col2() 支持裸名称的一种方式,但它改变了参数处理方式。对于原本设计为接收字符串的函数,不需要为了支持字符串再添加 enquo()。参考 embrace operator 文档

enquo() 仍然是有效工具。简单捕获后立即转发时,通常使用 {{ }} 更直接;需要保存、检查或修改捕获的表达式时,则更适合显式使用 enquo()。官方文档将 enquo() 归为进阶工具,并建议先学习 embracing。参考 enquo() 文档

列名是否带引号与该列是否存在无关。在这里的 mutate() 调用中,如果目标列已存在,它会被更新;如果函数必须只新增列,需要另外检查名称冲突。

备注:内容仅供参考。