【问题标题】:S3 operator overloading for multiple classes多个类的 S3 运算符重载
【发布时间】:2017-03-28 10:01:28
【问题描述】:

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

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 会自动将其重定向到我的函数而不是默认函数。

【问题讨论】:

  • @JoshuaUlrich 我试图深入了解 R 如何在内部为某个类选择一个函数而不是另一个函数。当两种类型相似时,没有问题。当两个参数中的一个是基本类型而另一个不是时,它似乎工作正常。但是,当您使用不同的非基本类型执行此操作时,会更加困难。重新思考这个问题后,可能更容易创建一个同时继承 customClass1 和 2 属性的超类。
  • @takje 这确实是最好的解决方案。有关如何做到这一点的想法,请参阅我的答案。

标签: r operator-overloading r-s3


【解决方案1】:

?base::Ops

详细信息 部分讨论了 R 如何选择调度哪个方法

两个参数的类都在调度中被考虑 该组的任何成员。对于每个参数,它的向量 检查类以查看是否有匹配的特定 (首选)或“操作”方法。如果找到一个方法只是 为两者找到一个参数或相同的方法,它是 用过的。如果发现不同的方法,会有警告 关于“不兼容的方法”:在这种情况下,或者如果没有方法 发现任一参数都使用了内部方法。

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

"POSIXct" 更方便包含在数据框中,并且 "POSIXlt" 更接近人类可读的形式。一个虚拟类 "POSIXt" 存在,这两个类都继承自:它是 用于允许诸如减法之类的操作将两者混合

例如:

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

如果类不以这种方式相关,Emulating multiple dispatch using S3 for “+” method - possible? 的答案中有一些很好的信息。

【讨论】:

    【解决方案2】:

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

    我强烈建议放弃 S3 并转向 S4。然后,您可以为“Ops”组定义双向的方法。这样做的好处是现在为这两个类定义了所有算术、逻辑和比较运算符。如果您想将此限制为子组或单个运算符,请将“Ops”替换为子组或运算符。更多信息请参见帮助页面?S4GroupGeneric

    一个基于您的 S3 类的示例,使用虚拟类使事情变得更容易:

    # 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 个插槽名称不同的类的示例:

    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')

    【讨论】:

    • 这是一个非常好的建议。到目前为止,我只使用过 S3。我将对此进行调查,这将是一个更清洁的解决方案。但是,Joshua 的回答更接近问题,因此我接受了他的回答。
    • @takje 不用担心,我只是添加了这个答案来说明 S4 如何以更强大的方式解决这个问题。如果你想试一试:Hadley 在他的书 Advanced R 中对 S4 做了很好的介绍:adv-r.had.co.nz/S4.html 在他关于包的书中,他还解释了如何轻松处理 S4 类和方法的导出和文档使用roxygen2:r-pkgs.had.co.nz/man.html#man-classesr-pkgs.had.co.nz/namespace.html#exports
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-06
    • 2019-09-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-01
    相关资源
    最近更新 更多