You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

关于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,函数会把它当成一个列名(显然不存在),但!!!会把列表里的cylam拆成两个独立的参数,就像你手动写group_by(cyl, am)一样。


核心原理:引用 vs 解引用

用大白话讲:

  • 引用:就是把一段代码「打包」,不执行,当成数据保存(比如rlang::quo()base::quote())。比如quo(mpg)不是要mpg的数值,而是把「mpg这个符号」存起来。
  • 解引用:就是把打包好的代码「拆包」,让它变成正常执行的代码(用!!!!!)。

{{}}是简化版,它帮你做了两件事:先把传入的参数引用(相当于enquo(x)),再自动解引用(相当于!!enquo(x)),所以在大多数自定义函数场景下,{{}}是最省心的选择。


总结什么时候用哪个

  • 写自定义tidyverse函数,传单个列名/表达式:用**{{}}**
  • 提前把单个表达式存成变量,要在函数里用:用**!!**
  • 要传多个表达式/列名(存在列表/向量里):用**!!!**

内容的提问来源于stack exchange,提问作者rg4s

火山引擎 最新活动