You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在含可选参数的函数中使用tidydots(...)?是否为不良实践?

问题分析与解决方案

原函数的核心问题

你的两个函数存在几个tidyeval使用误区,同时有包开发需要规避的不良实践:

  1. tidydots参数传递的语法混淆
    第一个函数createTable的参数顺序是counts, mapper = NULL, day = NULL, week = NULL, month = NULL, ...,当你用命名传参指定mapper = "mcglonee"等参数后,后续的mapper, month, week作为位置参数会被传入...,但你实际想引用的是数据框中的列名,而这些符号在调用环境中没有对应的对象,导致group_by(!!!group_vars)无法识别列名。

  2. 全局依赖(严重不良实践)
    函数直接依赖全局变量summaryVars和全局函数calcScores,自用场景下可能没问题,但包开发中这会导致函数可移植性差、容易出现命名冲突,完全不符合模块化设计原则。

  3. 过滤逻辑冗余与变量名冲突
    你把函数参数重新赋值为m, d, w, mp的操作完全冗余,还容易引发数据框列名(比如month)和函数参数名的混淆。此外,多次调用filter()会降低代码效率,完全可以合并为一次。

  4. 分组参数的错误处理方式
    createTable2尝试用group_vars参数接收分组变量,但传入list(mapper, month, week)时,这些符号是在调用环境中求值,而非函数内部或数据框环境,导致R找不到对应的对象——tidyeval的核心就是捕获表达式而非求值后的对象。

优化后的函数实现

下面是修复后的函数,同时兼顾可选参数、tidydots支持,以及包开发的最佳实践:

library(dplyr)

createTable <- function(counts, mapper = NULL, day = NULL, week = NULL, month = NULL, 
                        summary_vars = c("default_col1", "default_col2"), ...) {
  # 捕获分组变量的表达式(保留列名的符号形式)
  group_vars <- enquos(...)
  
  # 构建过滤条件,自动忽略空参数
  filter_conds <- list(
    if (!is.null(mapper)) expr(mapper == !!mapper),
    if (!is.null(day)) expr(day == !!day),
    if (!is.null(week)) expr(week == !!week),
    if (!is.null(month)) expr(month == !!month)
  )
  # 移除空条件(base R写法,无需额外包)
  filter_conds <- Filter(Negate(is.null), filter_conds)
  
  # 参数验证:确保输入合规(包开发必备)
  stopifnot("counts必须是tibble格式" = is_tibble(counts))
  stopifnot("summary_vars必须是counts中的列" = all(summary_vars %in% colnames(counts)))
  
  counts %>%
    group_by(!!!group_vars) %>%
    # 用dplyr 1.0+推荐的across替代已软弃用的summarise_at
    summarise(across({{ summary_vars }}, sum), .groups = "drop_last") %>%
    # 一次性应用所有过滤条件
    filter(!!!filter_conds) %>%
    # 如果calcScores是包内函数,需确保它在命名空间中;若需用户自定义,可设为参数
    calcScores()
}

关键改进点:

  • tidyeval正确用法:用enquos(...)捕获分组变量的符号表达式,!!明确引用函数参数(避免和数据框列名冲突),expr()动态构建过滤条件。
  • 消除全局依赖:将summaryVars改为函数参数,允许用户自定义汇总列,同时保留默认值。
  • 简化逻辑提升效率:合并多次filter()调用,用Filter自动移除空条件。
  • 参数验证:添加stopifnot确保输入合规,这是包开发中提升鲁棒性的关键步骤。

正确调用方式

无论是位置传参还是命名传参,只需将分组变量作为最后一批参数传入即可:

# 命名传参示例
test <- createTable(
  counts = featureCounts2019,
  mapper = "mcglonee",
  month = 5,
  # 分组变量(直接传入数据框中的列名符号)
  mapper, month, week
)

包开发层面的额外注意事项

如果要将这个函数纳入R包,还需注意:

  • 函数导出与命名空间:确保calcScores作为包的导出函数或内部函数(用:::定义),避免全局依赖。
  • 文档化:用roxygen2为函数添加详细文档,说明每个参数的用途、返回值,以及示例调用代码。
  • 测试覆盖:用testthat编写测试用例,覆盖各种场景(比如不传可选参数、传多个分组变量、传入非法参数等)。
  • 语法兼容性:如果需要兼容旧版dplyr,可保留summarise_at作为备选,但优先推荐across。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 20:52:30