Bioconductor, Matrix and many large packages are built on S4, and RC/R6 are used for stateful objects.
S4 (the methods package) is a formal object system. setClass("Name", representation) declares a class with typed slots, new("Name", ...) creates instances, and obj@slot accesses slots (slot() and @<- modify them). A validity function enforces invariants and validObject rechecks them. Classes support inheritance with contains =.
Behaviour lives in generics and methods: setGeneric("area", function(shape) standardGeneric("area")) creates a generic and setMethod("area", "Circle", function(shape) ...) registers an implementation. S4 supports multiple dispatch, choosing a method based on the classes of several arguments, which suits operations like matrix arithmetic. show is S4's print. isVirtualClass, existsMethod and showMethods help explore.
S4 objects still have copy-on-modify value semantics. Reference Classes (setRefClass) are built on S4 plus environments: objects are mutable, methods can change fields with <<-, and copies must be made explicitly with $copy(). They feel like classes in Java or Python. The R6 package offers the same style with less overhead and is the popular choice outside base R (Shiny uses it).
Choose S3 for most work, S4 when you need formal definitions, validity and multiple dispatch (and in Bioconductor), and R6/RC when objects genuinely represent mutable state such as a connection or a simulation.
Example
setClass("Shape", representation("VIRTUAL"))
setClass("Circle", contains = "Shape", representation(r = "numeric"),
validity = function(object) if (object@r < 0) "r must be >= 0" else TRUE)
setClass("Rect", contains = "Shape", representation(w = "numeric", h = "numeric"))
setGeneric("area", function(shape) standardGeneric("area")) #> [1] "area"
setMethod("area", "Circle", function(shape) pi * shape@r^2)
setMethod("area", "Rect", function(shape) shape@w * shape@h)
shapes <- list(new("Circle", r = 1), new("Rect", w = 2, h = 3))
print(round(sapply(shapes, area), 2)) #> [1] 3.14 6.00
print(is(shapes[[1]], "Shape")) #> [1] TRUE
print(tryCatch(new("Circle", r = -1), error = \(e) "invalid")) #> [1] "invalid"
Account <- setRefClass("Account", fields = list(balance = "numeric"),
methods = list(deposit = function(x) { balance <<- balance + x; invisible(.self) }))
a <- Account$new(balance = 10)
b <- a # same object, not a copy
b$deposit(5)
print(a$balance) #> [1] 15Remember
- S4:
setClasswith typed slots,@access and validity checks setGeneric+setMethod, with multiple dispatch- Reference Classes and R6 are mutable with reference semantics
- Default to S3; use S4 or R6 when you need what they add
Watch out for
- Assuming b <- a copies a Reference Class object; use a$copy().