多个 类 的 S3 运算符重载

S3 operator overloading for multiple classes

我定义了两个类可以成功添加两个自己的对象或者一个数字和一个自己的对象。

a <- structure(list(val = 1), class = 'customClass1')
b <- structure(list(val = 1), class = 'customClass2')
`+.customClass1` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass1')
  return(structure(list(val = val_res), class = 'customClass1'))
}
`+.customClass2` <- function(e1, e2, ...){
  val1 <- ifelse(is.numeric(e1), e1, e1$val)
  val2 <- ifelse(is.numeric(e2), e2, e2$val)
  val_res <- val1  + val2
  print('customClass2')
  return(structure(list(val = val_res), class = 'customClass1'))
}
print.customClass1 <- function(x, ...){
  print(x$val)
}
print.customClass2 <- function(x, ...){
  print(x$val)
}
a + a
# [1] 2
a + 1
# [1] 2
b + b
# [1] 2
1 + b
# [1] 2

但显然,当我尝试添加两个自定义 类 时出错了。

a + b
# Error in a + b : non-numeric argument to binary operator
# In addition: Warning message:
# Incompatible methods ("+.customClass1", "+.customClass2") for "+" 

我可以只为 customClass1 定义一个函数,但是当我尝试添加两个 customClass2 对象时该函数将不起作用。有什么方法可以使一个功能优先于另一个功能?

R 似乎通过将我的函数优先于基本函数(例如数字或整数类型)来自然地做到这一点。当两个参数之一具有 customClass 类型时,R 会自动将其重定向到我的函数而不是默认函数。

?base::Ops

Details 部分讨论了 R 如何选择要调度的方法

The classes of both arguments are considered in dispatching any member of this group. For each argument its vector of classes is examined to see if there is a matching specific (preferred) or 'Ops' method. If a method is found for just one argument or the same method is found for both, it is used. If different methods are found, there is a warning about 'incompatible methods': in that case or if no method is found for either argument the internal method is used.

如果 customClass1customClass2 相关,您可以使用虚拟 class 来允许使用两个不同的 class 进行操作。例如,您可以混合使用 POSIXctPOSIXlt,因为它们都继承自 POSIXt。这记录在 ?DateTimeClasses:

"POSIXct" is more convenient for including in data frames, and "POSIXlt" is closer to human-readable forms. A virtual class "POSIXt" exists from which both of the classes inherit: it is used to allow operations such as subtraction to mix the two

例如:

class(pct <- Sys.time())
# [1] "POSIXct" "POSIXt"
Sys.sleep(1)
class(plt <- as.POSIXlt(Sys.time()))
# [1] "POSIXlt" "POSIXt"
plt - pct
# Time difference of 1.001677 secs

如果 class 不是这样关联的,Emulating multiple dispatch using S3 for “+” method - possible? 的答案中有一些有用的信息。

Joshua 解释了为什么在使用 S3 时如果不构建虚拟超级 classes 等,您的方法永远无法顺利运行。使用 S3,您必须在每个可能使用的函数中手动管理 class 分配。忘记分配 super class 一次,你就会去寻找可能持续一段时间的错误。

我强烈建议放弃 S3 并转到 S4。然后你可以为组"Ops"定义两个方向的方法。这样做的好处是所有算术、逻辑和比较运算符现在都为两个 class 定义。如果要将其限制为子组或单个运算符,请将 "Ops" 替换为子组或运算符。有关帮助页面的更多信息 ?S4GroupGeneric.

基于您的 S3 classes 的示例使用虚拟 class 使事情变得更容易:

# Define the superclass
setClass("super", representation(x = "numeric"))
# Define two custom classes
setClass("foo", representation(slot1 = "character"),
         contains = "super")
setClass("bar", representation(slot1 = "logical"),
         contains = "super")

# Set the methods
setMethod("Ops",
          signature = c('super','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('ANY','super'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })
# Redundant actually, but limits the amount of times callGeneric
# has to be executed. 
setMethod("Ops",
          signature = c('super','super'),
          function(e1,e2){
            callGeneric(e1@x, e2@x)
          })

foo1 <- new("foo", x = 3, slot1 = "3")
bar1 <- new("bar", x = 5, slot1 = TRUE)

foo1 + bar1
#> [1] 8
bar1 + foo1
#> [1] 8
bar1 < foo1
#> [1] FALSE
foo1 / bar1
#> [1] 0.6

有 2 个 classes 的示例,其中插槽名称不同:

setClass("foo", representation(x = "numeric"))
setClass("bar", representation(val = "numeric"))

setMethod("Ops",
          signature = c('foo','ANY'),
          function(e1,e2){
            callGeneric(e1@x, e2)
          })
setMethod("Ops",
          signature = c('bar','ANY'),
          function(e1,e2){
            callGeneric(e1@val, e2)
          })
setMethod("Ops",
          signature = c('ANY','bar'),
          function(e1,e2){
            callGeneric(e1, e2@val)
          })
setMethod("Ops",
          signature = c('ANY','foo'),
          function(e1,e2){
            callGeneric(e1, e2@x)
          })

您可以再次使用上面的代码来检查结果。请注意,在此处以交互方式尝试此操作时,您将获得有关所选方法的注释。为避免这种情况,您可以为签名 c('foo','bar')c('bar','foo')

添加一个方法