关于R语言rlang包中!!、!!!、{{}}运算符的含义、用途及工作原理的技术咨询
我完全懂这种困惑——刚接触准引用的时候,这些符号看起来就像乱码一样!别担心,咱们一步步拆解,用最简单的例子讲清楚它们到底是干嘛的。
先搞懂核心问题:为什么需要这些运算符?
咱们先从一个常见的场景入手:假设你想写一个自己的函数,用来计算数据框某列的均值。如果不用这些符号,你会遇到这样的问题:
# 尝试写一个简单的汇总函数 my_summary <- function(df, x) { df %>% dplyr::summarize(mean_val = mean(x)) } # 调用函数,传入mtcars和mpg列 my_summary(mtcars, mpg) # 报错!提示"object 'x' not found"
为什么会报错?因为普通函数是标准计算(SE):它会直接找当前环境里的x变量,而不是把x当成数据框里的列名。而像dplyr这类tidyverse函数用的是非标准计算(NSE)——它们需要识别你传入的「符号」(比如列名),而不是符号对应的值。
这时候就需要准引用:把符号/表达式打包(引用),再告诉函数什么时候把它拆包用(解引用),这些!!、!!!、{{}}就是干这个的。
逐个拆解运算符
1. {{}}:最常用的「一键打包+拆包」
{{}}是enquo()(引用参数)+!!(解引用)的简写,专门用来帮函数识别「你传的是数据框里的列名」,是写自定义tidyverse函数的首选。
用它改写刚才的函数:
my_summary <- function(df, x) { df %>% dplyr::summarize(mean_val = mean({{x}})) } my_summary(mtcars, mpg) # 成功输出:mean_val = 20.09062
原理:{{x}}会自动把你传入的mpg打包成一个「引用的表达式」,然后告诉summarize:别找当前环境的x,去df里找mpg这列的值。
2. !!:解引用单个表达式/变量
!!读作「bang bang」,意思是:把我包起来的这个东西,从「引用状态」变成「实际执行的表达式/值」。
比如你先把列名存在变量里,想动态传给dplyr函数:
# 先把目标列名存成一个引用的表达式(用quo()打包) target_col <- rlang::quo(mpg) # 用!!拆包,告诉dplyr:别把target_col当变量,用它里面的mpg mtcars %>% dplyr::summarize(mean_mpg = mean(!!target_col))
再比如动态生成筛选条件:
# 打包一个筛选表达式 filter_rule <- rlang::quo(hp > 100) # 用!!拆包执行筛选 mtcars %>% dplyr::filter(!!filter_rule)
3. !!!:解引用多个表达式(「拆包列表」)
!!!是!!的复数版,用来把一个列表/向量里的多个表达式一次性拆解开,当成多个参数传给函数。
比如你想同时按多个列分组,而这些列名存在一个向量里:
# 要分组的列名存成向量 group_cols <- c("cyl", "am") # 把向量转换成引用的表达式列表(syms()把字符串转成符号) group_quos <- rlang::syms(group_cols) # 用!!!把列表拆成单独的参数传给group_by mtcars %>% dplyr::group_by(!!!group_quos) %>% dplyr::summarize(mean_mpg = mean(mpg))
如果直接传group_cols,函数会把它当成一个列名(显然不存在),但!!!会把列表里的cyl和am拆成两个独立的参数,就像你手动写group_by(cyl, am)一样。
核心原理:引用 vs 解引用
用大白话讲:
- 引用:就是把一段代码「打包」,不执行,当成数据保存(比如
rlang::quo()、base::quote())。比如quo(mpg)不是要mpg的数值,而是把「mpg这个符号」存起来。 - 解引用:就是把打包好的代码「拆包」,让它变成正常执行的代码(用
!!或!!!)。
而{{}}是简化版,它帮你做了两件事:先把传入的参数引用(相当于enquo(x)),再自动解引用(相当于!!enquo(x)),所以在大多数自定义函数场景下,{{}}是最省心的选择。
总结什么时候用哪个
- 写自定义tidyverse函数,传单个列名/表达式:用**
{{}}** - 提前把单个表达式存成变量,要在函数里用:用**
!!** - 要传多个表达式/列名(存在列表/向量里):用**
!!!**
内容的提问来源于stack exchange,提问作者rg4s




