如何简化使用NSE在函数中拼接数据框列的代码?
问题解决:简化tidyverse风格的列拼接函数
核心问题
你需要编写支持无引号列名的函数,实现类似c(df$colA, df$colB)的列拼接操作,现有基于enquo()/expr()/!!的实现过于冗长,希望找到更简洁的方案,同时纠结是否改用base R的NSE(非标准求值)。
先还原你的示例函数
假设你当前可用的myFun3是这样的:
myFun3 <- function(df, col1, col2) { col1_enq <- enquo(col1) col2_enq <- enquo(col2) eval_tidy(expr(c(!!col1_enq, !!col2_enq)), data = df) } # 测试:返回c("A","B","C","D","E","F") myFun3(df, col1, col2)
而失败的myFun4大概是:
myFun4 <- function(df, col1, col2) { eval_tidy(expr(c(col1, col2)), data = df) } # 报错:找不到对象'col1' # 原因:expr()保留了符号本身,未将参数注入到df的环境中求值
简化方案
方案1:用{{ }}(Curly-Curly)简化rlang代码
{{ }}是enquo()+!!的语法糖,能直接捕获参数并注入表达式,大幅简化代码:
myFun_simple <- function(df, col1, col2) { eval_tidy(expr(c({{ col1 }}, {{ col2 }})), data = df) } # 测试正常 myFun_simple(df, col1, col2)
如果结合tidyverse的pull()函数,代码更直观:
myFun_tidy <- function(df, col1, col2) { c(pull(df, {{ col1 }}), pull(df, {{ col2 }})) }
方案2:支持多列输入的扩展版本
如果需要拼接任意数量的列,用...参数结合purrr:
myFun_multi <- function(df, ...) { cols <- enquos(...) map(cols, ~pull(df, !!.x)) %>% flatten_chr() } # 测试:拼接col1、col3、col4 myFun_multi(df, col1, col3, col4)
方案3:base R的NSE实现
如果倾向用base R的非标准求值,可通过substitute()捕获表达式,再构造df$col形式的调用:
myFun_base <- function(df, col1, col2) { # 捕获输入的列名表达式 col_expr <- substitute(c(col1, col2)) # 将每个列名替换为df$col的形式 col_expr[[2]] <- call("$", substitute(df), col_expr[[2]]) col_expr[[3]] <- call("$", substitute(df), col_expr[[3]]) # 执行表达式 eval(col_expr) } # 测试正常 myFun_base(df, col1, col2)
如果要支持多列,可扩展为:
myFun_base_multi <- function(df, ...) { col_list <- substitute(list(...))[-1] # 生成每个列的df$col调用 col_calls <- lapply(col_list, function(x) call("$", substitute(df), x)) # 拼接所有列 do.call(c, lapply(col_calls, eval)) }
方案选择建议
- 如果你习惯tidyverse生态,优先用
{{ }}+pull()的方案,代码简洁且符合tidyverse的风格,学习成本更低。 - 如果需要完全基于base R实现,
substitute()+eval()的NSE方案是标准做法,但代码相对繁琐,适合需要脱离tidyverse依赖的场景。
内容的提问来源于stack exchange,提问作者ScottyJ
相关产品推荐
相关产品推荐

