Reference Classes and R6
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. Almost everything in R copies: assign a vector, a list or a data frame to a second name, change the second one, and the first is untouched. This page is about the exceptions, the objects that every holder of a reference sees change. R has two mainstream systems for writing them as classes with methods in the style of Java or C++: Reference Classes, created with setRefClass() and shipped in the methods package of base R, and the add-on package R6, created with R6Class(). Both are built on environments. This page uses base R only for the Reference Class sections and R6 2.6.1 (CRAN) for the R6 sections, so those sections begin with library(R6); the methods package is attached by Rscript by default (since R 3.5.0), so the Reference Class examples need no library() call. Two other pages of this section are close relatives: S3 Classes and Generics and S4 Classes and Methods are the value-semantics systems, and The S7 Object System ends with a script that builds a trimmed book in S3, S4, R6, S7 and as a Reference Class, and with a table that compares S3, S4, R6 and S7 (the Reference Class is described in the prose next to it).
Every example below is a complete script that was run with Rscript on R 4.6.1 with warnings printed as they happen (options(warn = 1)); the lines starting with #> are what it printed, and the scripts pass the default lintr linters. Class generators are conventionally written in CamelCase (BookRC, BookR6), which the default object_name_linter dislikes, so each of those assignments carries # nolint: object_name_linter.. The names books, loans, shelf_today, loan_days and the book class come from the scenario.R file of the running library-shelf scenario described on the section index; scripts that use them start with source("scenario.R"). The three object-oriented systems of the section model the same book with the same six fields in the same order, title, author, isbn, year, genre and pages, and the same validation rules as validate_book() in that file, so that the classes of the different pages line up field by field. Errors and warnings that the scripts provoke on purpose are caught so that every script exits with status 0, and the message text shown is the real one. Memory addresses are never printed: the output shows class names, never the address of an environment.
Why objects with reference semantics exist
R is built around values. When a function receives a vector it receives what behaves like a copy, and an assignment such as y <- x makes a second name for the same value that is copied only when one of them is modified (see Assignment, Names and Values and Memory and Copy Semantics). That is exactly what is wanted for data analysis, and it is why a function can never change the data frame it was given by accident. The help page of Reference Classes describes the functional model in these terms and then names the situations in which it is the wrong one: sometimes one would like all the code dealing with an object to see the exact same content, so that a change made in any computation is reflected everywhere, which suits an object with some objective reality such as a window in a user interface.
R already has one kind of object that is never copied: the environment. The manual lists environments among the basic object types and describes them as a frame plus an enclosure (R-lang: Environments); Scoping, Environments and Closures covers the mechanism. Assigning an environment to a second name, or passing it to a function, copies the reference and not the environment, so every holder sees every change.
# A list is a value: a function that "modifies" it works on a copy.
shelf_list <- list(log = character())
add_to_list <- function(x, entry) {
x$log <- c(x$log, entry)
x
}
add_to_list(shelf_list, "L001 out")$log
#> [1] "L001 out"
shelf_list$log
#> character(0)
# An environment is a reference: the function and the caller share it.
shelf_env <- new.env()
shelf_env$log <- character()
add_to_env <- function(e, entry) {
e$log <- c(e$log, entry)
invisible(e)
}
add_to_env(shelf_env, "L001 out")
shelf_env$log
#> [1] "L001 out"
# Assignment copies the reference, not the environment.
alias <- shelf_env
alias$log <- c(alias$log, "L002 out")
shelf_env$log
#> [1] "L001 out" "L002 out"
identical(alias, shelf_env)
#> [1] TRUE
The list shelf_list behaves as a value: the function returned a changed copy and left the original empty. The environment shelf_env behaves as a reference: the function changed it in place, and alias is not a copy of it but a second name for it, which is why identical() reports TRUE. The R FAQ draws the same line when it separates the frame of a function from its enclosing environment (R FAQ 7.12: What are the enclosing and parent environments?).
A bare environment is a poor class: nothing says which names it is supposed to hold, nothing checks what is stored in them, and the functions that operate on it are just functions that happen to receive it. Reference Classes and R6 both add the missing parts on top of an environment:
-
a class: a named definition of the fields (state) and the methods (functions that work on that state), so that objects of the class all look the same;
-
methods called with
$:obj$lend(...)runs a function that belongs to the object and can read and change its fields, which is the style of Java, C++ and Python; -
inheritance: a subclass adds fields and methods and can override the methods of its superclass;
-
initialisation and printing hooks, and, in R6 and (partly) in Reference Classes, locking and active bindings.
"R5" is not a separate system. It is a community nickname for Reference Classes (R’s other systems are S3 and S4, so the third was jokingly called R5), and the R6 package, which is a different implementation, took its own name from the joke. The R6 documentation also records that "R5" had been the code name of a different, never released object system. This page says Reference Classes for the base R system and R6 for the package.
Reference Classes with setRefClass()
A Reference Class is an S4 class whose instances contain an environment. The documentation says so directly: reference classes are implemented as S4 classes with a data part of type environment, fields correspond to named objects in that environment, and a field with a declared class is implemented as a special form of active binding that enforces the class on assignment (R documentation: refClass). That is why they live in the methods package, why they are created with a function from the S4 family, and why isVirtualClass(), validObject() and is() work on them.
Fields and the generator
setRefClass(Class, fields = , contains = , methods = ) defines the class and returns a generator. The fields argument is either a character vector of field names, which gives every field the class "ANY", or a named list in which each element is usually the name of a class. A field of a declared class accepts only objects of that class or a subclass. The six fields of the scenario book are declared in that way, and BookRC$new(...) (equivalently BookRC(...)) creates an object:
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", author = "character", isbn = "character",
year = "integer", genre = "character", pages = "integer"
)
)
class(BookRC)
#> [1] "refObjectGenerator"
#> attr(,"package")
#> [1] "methods"
BookRC$fields()
#> title author isbn year genre pages
#> "character" "character" "character" "integer" "character" "integer"
b1 <- BookRC$new(
title = "The Quiet Streets", author = "Iris Fenwick",
isbn = "9780306406157", year = 1999L, genre = "Fiction", pages = 288L
)
b1
#> Reference class object of class "BookRC"
#> Field "title":
#> [1] "The Quiet Streets"
#> Field "author":
#> [1] "Iris Fenwick"
#> Field "isbn":
#> [1] "9780306406157"
#> Field "year":
#> [1] 1999
#> Field "genre":
#> [1] "Fiction"
#> Field "pages":
#> [1] 288
class(b1)
#> [1] "BookRC"
#> attr(,"package")
#> [1] ".GlobalEnv"
is.environment(b1)
#> [1] TRUE
isVirtualClass("BookRC")
#> [1] FALSE
b1$title
#> [1] "The Quiet Streets"
b1$year <- 2001L
b1$year
#> [1] 2001
# Fields that were not supplied keep the empty value of their class.
BookRC$new(title = "Untitled")$year
#> integer(0)
Source: R documentation: refClass
Several things in that output are worth reading slowly. BookRC is not a class definition but an object of class refObjectGenerator, which is a function (it can be called as BookRC(...)) and also a reference object with methods of its own: $new(), $methods(), $fields(), $help(), $lock() and $accessors(). BookRC$fields() returns the declared class of every field. The default print method, $show() of the parent class envRefClass, prints Reference class object of class "BookRC" followed by every field. A field that is not supplied is not NA but the empty vector of its class (integer(0), character(0)): the object exists in a state the book rules forbid, which is the reason the next subsection writes an initialize method. And is.environment(b1) is TRUE: the object is an environment with an S4 class attached.
The documentation names the methods that every generator and every object inherit from envRefClass:
| Method | What it does |
|---|---|
|
Creates an object; the same as calling the generator. The arguments are passed to the |
|
With no arguments it lists the method names; with a name it returns that method; named function arguments install new methods (a |
|
Named character vector of field classes (the class of a function-valued field is |
|
Prints the calling sequence of a method and its docstring, or the class definition when no topic is given. |
|
Makes the named fields settable once; with no argument it returns the locked names. |
|
Creates |
|
Installs a traced version of a method for every object of the class. |
|
Inside a method: calls the method of the same name in the superclass. |
|
Makes an independent copy of the object. |
|
Gets or sets a field whose name is held in a variable. |
|
The default initialisation: assigns named arguments to fields. |
|
Called when the object is printed automatically. |
|
The generator and the class definition of the object. |
|
Coerce to a superclass, or copy the fields of a superclass object into this one. |
|
Declares the methods that a method uses indirectly, for example through |
|
Tracing of one method of one object. |
Source: R documentation: refClass
Two reserved names exist as fields of every object: .self is the whole object and .refClassDef is its class definition. Both are read-only, and the documentation advises against defining any field whose name begins with a dot.
Methods, initialize() and validation
Methods are ordinary functions in a named list passed as methods, or added later with Gen$methods(...). Inside a method the object itself is not an argument. The environment of the method is the object, so fields and other methods are used by name, and the whole object is available as .self. A field is changed with the non-local assignment <<-: the documentation stresses that non-local assignment is required, because title <- value just creates a local variable as it would in any function. The reason is the one explained on the scoping page: <<- starts the search for the name in the enclosing environment of the function, which for a method is the object, where the field lives.
Two method names are special. initialize is called when an object is created, and its job here is the work of the scenario’s book() function: trim the strings, normalise the ISBN, convert the numbers to integer and apply the rules of validate_book(). finalize is called before the object is garbage collected (see the section on debugging and finalizers below). The documentation’s advice for an initialize method comes to three rules, and the example follows all of them:
-
It should pass
...on, withcallSuper(...)orinitFields(...), so that subclasses can add fields. -
It must work when called with no arguments, because
$copy()calls the generator that way; here that is themissing(title)branch. -
The standard initialisation, which is
callSuper(...)and ends ininitFields(...), assigns the named arguments to the fields. Any computation on the arguments (heretrimws()andas.integer()) comes before it and any check of the finished object after it.
source("scenario.R")
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", author = "character", isbn = "character",
year = "integer", genre = "character", pages = "integer"
),
methods = list(
initialize = function(title, author = NA_character_, isbn = NA_character_,
year = NA_integer_, genre = NA_character_,
pages = NA_integer_, ...) {
"Create a book; with no title (as copy() does) create an empty shell."
if (missing(title)) {
return(callSuper(...))
}
callSuper(
title = trimws(title), author = trimws(author),
isbn = if (is.na(isbn)) isbn else normalise_isbn(isbn),
year = as.integer(year), genre = trimws(genre),
pages = as.integer(pages), ...
)
validate()
},
validate = function() {
"Apply the rules of validate_book(); return the object invisibly."
lens <- c(length(title), length(author), length(isbn),
length(year), length(genre), length(pages))
if (any(lens != 1L)) {
stop("every field of a book must have length 1", call. = FALSE)
}
if (is.na(title) || !nzchar(title)) {
stop("a book needs a title", call. = FALSE)
}
if (!is.na(isbn) && !is_valid_isbn13(isbn)) {
stop("invalid ISBN-13: ", isbn, call. = FALSE)
}
if (!is.na(year) && year > 2100L) {
stop("implausible year: ", year, call. = FALSE)
}
invisible(.self)
},
describe = function() {
yr <- if (is.na(year)) "n.d." else year
sprintf("%s (%s, %s)", title, author, yr)
},
show = function() {
cat("<", class(.self)[[1L]], "> ", describe(), "\n", sep = "")
}
)
)
b1 <- BookRC$new(
title = " the quiet streets ", author = "Iris Fenwick",
isbn = "978-0-306-40615-7", year = 1999, genre = "Fiction", pages = 288
)
b1
#> <BookRC> the quiet streets (Iris Fenwick, 1999)
b1$isbn
#> [1] "9780306406157"
b1$year
#> [1] 1999
class(b1$year)
#> [1] "integer"
try_book <- function(...) {
tryCatch(BookRC$new(...), error = \(e) conditionMessage(e))
}
try_book(title = "")
#> [1] "a book needs a title"
try_book(title = "Orchard Logic", isbn = "9780306406158")
#> [1] "invalid ISBN-13: 9780306406158"
try_book(title = "Far Future", year = 2101L)
#> [1] "implausible year: 2101"
try_book(title = c("A", "B"))
#> [1] "every field of a book must have length 1"
Source: R documentation: refClass
Look at what the listing shows. The strings arrive trimmed and the ISBN arrives normalised, because callSuper() received the cleaned values. The year 1999 was a double in the call and is an integer in the object, because the field would otherwise have rejected it (the error text is in the section on what goes wrong). Each failed validation raises the same message as validate_book() does for the S3 class: the rules moved from a function that checks a list to a method that checks .self. The class check of a field is not a validity rule, though: title = c("A", "B") is a perfectly good character vector, which is why the length check is still needed.
The docstring at the start of initialize and validate is a convention of Reference Classes: if the first element of a method body is a literal string, Gen$help("validate") prints it. The method show is also special by name. It is what the console calls to print an object, and the documentation recommends printing the class name explicitly so that subclasses can reuse the method, which is why class(.self)[[1L]] appears instead of the literal "BookRC".
Methods call each other by name (validate() inside initialize()), and the object is searched first, so the names of fields and methods must not collide. For that reason the scenario’s format() became describe(): a method named format would hide base::format() inside every other method of the class. When a method uses another method indirectly, through lapply() or do.call(), the documentation asks for a usingMethods(describe) call, because only the methods that the code analysis sees are installed in the object.
The generator object: methods added later, help and fields
source("scenario.R")
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", author = "character", isbn = "character",
year = "integer", genre = "character", pages = "integer"
),
methods = list(
describe = function() {
"One line for a catalogue: title, author and year."
yr <- if (length(year) == 0L || is.na(year)) "n.d." else year
sprintf("%s (%s, %s)", title, author, yr)
}
)
)
b <- BookRC$new(
title = "The Quiet Streets", author = "Iris Fenwick",
isbn = "9780306406157", year = 1999L, genre = "Fiction", pages = 288L
)
# The generator is itself an object with methods of its own.
class(BookRC)
#> [1] "refObjectGenerator"
#> attr(,"package")
#> [1] "methods"
BookRC$className
#> [1] "BookRC"
BookRC$fields()
#> title author isbn year genre pages
#> "character" "character" "character" "integer" "character" "integer"
BookRC$help("describe")
#> Call:
#> $describe()
#>
#>
#> One line for a catalogue: title, author and year.
#>
# Methods can be added after the class exists (and after objects do).
BookRC$methods(
age = function(today) {
"Whole calendar years between publication and `today`."
as.integer(format(today, "%Y")) - year
}
)
b$age(shelf_today)
#> [1] 27
BookRC$help(age)
#> Call:
#> $age(today)
#>
#>
#> Whole calendar years between publication and `today`.
#>
class(BookRC$methods("age"))
#> [1] "refMethodDef"
#> attr(,"package")
#> [1] "methods"
sort(BookRC$methods())
#> [1] ".objectPackage" ".objectParent" "age" "callSuper"
#> [5] "copy" "describe" "export" "field"
#> [9] "getClass" "getRefClass" "import" "initFields"
#> [13] "show" "trace" "untrace" "usingMethods"
# Every object can find its own generator and class definition.
b$getRefClass()$className
#> [1] "BookRC"
#> attr(,"package")
#> [1] ".GlobalEnv"
class(b$getClass())
#> [1] "refClassRepresentation"
#> attr(,"package")
#> [1] "methods"
b$field("year")
#> [1] 1999
b$field("year", 2005L)
b$year
#> [1] 2005
Source: R documentation: refClass
Gen$methods(...) adds a method after the class exists, and the documentation prefers it to the methods argument of setRefClass() for the sake of readability. In the listing b was created before age was defined and can still call it, because methods are looked up when they are first used. The caveat in the documentation concerns methods that were already analysed: a method defined earlier is not re-analysed, so it will not call a new method of the same name unless it is redefined too. The R 3.3.2 NEWS records the repair of an earlier bug in this area (calls to $methods() did not re-analyse previously defined methods). The listing also shows Gen$help() printing the docstring, Gen$methods() returning the names of the inherited helpers (callSuper, copy, field, export, import, initFields, show, trace, untrace, usingMethods, getClass, getRefClass, and two internal ones whose names begin with a dot), and obj$field(name, value) for a field whose name is computed.
The documentation also warns that all the methods of a class belong in the source that defines it. In a package with a namespace the class definition is locked once the package is loaded, so $methods() and $lock() raise an error if they are called later from outside the package.
Aliasing and $copy()
An assignment of a reference object copies the reference. The scenario’s S3 book has value semantics, so the same three lines give different answers:
source("scenario.R")
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", author = "character", isbn = "character",
year = "integer", genre = "character", pages = "integer"
),
methods = list(
describe = function() {
yr <- if (is.na(year)) "n.d." else year
sprintf("%s (%s, %s)", title, author, yr)
},
show = function() {
cat("<", class(.self)[[1L]], "> ", describe(), "\n", sep = "")
}
)
)
# The S3 book of the scenario has value semantics ...
s3_a <- book("The Quiet Streets", "Iris Fenwick", year = 1999L)
s3_b <- s3_a
s3_b$year <- 2000L
c(a = s3_a$year, b = s3_b$year)
#> a b
#> 1999 2000
# ... a reference class object does not.
b1 <- BookRC$new(
title = "The Quiet Streets", author = "Iris Fenwick",
isbn = "9780306406157", year = 1999L, genre = "Fiction", pages = 288L
)
b2 <- b1
b2$year <- 2000L
c(b1 = b1$year, b2 = b2$year)
#> b1 b2
#> 2000 2000
identical(b1, b2)
#> [1] TRUE
# A function can change its argument, as with an environment.
reissue <- function(book, new_year) {
book$year <- new_year
invisible(NULL)
}
reissue(b1, 2010L)
b1
#> <BookRC> The Quiet Streets (Iris Fenwick, 2010)
# copy() makes an independent object.
b3 <- b1$copy()
b3$year <- 1950L
c(b1 = b1$year, b3 = b3$year)
#> b1 b3
#> 2010 1950
identical(b1, b3)
#> [1] FALSE
b3
#> <BookRC> The Quiet Streets (Iris Fenwick, 1950)
Source: R documentation: refClass
b2 <- b1 made a second name, b2$year <- 2000L changed the one object they share, and a function that receives the object, reissue() above, can change the caller’s object although it returns nothing. Nothing like it happens with the S3 book, where s3_b$year <- 2000L creates a modified copy and leaves s3_a alone. To get an independent object, ask for one: b1$copy() makes a new object with the same field values. This is the single most common confusion about Reference Classes, and it has a mirror image: people who expect copy-on-modify are surprised by aliasing, and people who expect aliasing are surprised that b3 <- b1$copy() does not stay linked.
$copy() is also where a field that is itself a reference object matters. The argument shallow = FALSE (the default) copies such fields too, recursively; with shallow = TRUE the copy shares them:
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(title = "character", year = "integer")
)
LoanRC <- setRefClass("LoanRC", # nolint: object_name_linter.
fields = list(loan_id = "character", book = "BookRC", member = "character")
)
b <- BookRC$new(title = "The Quiet Streets", year = 1999L)
l1 <- LoanRC$new(loan_id = "L001", book = b, member = "Ana Ruiz")
deep <- l1$copy()
shallow <- l1$copy(shallow = TRUE)
# Change the book that l1 holds.
l1$book$year <- 2000L
c(original = l1$book$year, deep = deep$book$year,
shallow = shallow$book$year, source = b$year)
#> original deep shallow source
#> 2000 1999 2000 2000
# Reassigning the field on a shallow copy does not touch the original ...
shallow$book <- BookRC$new(title = "Another", year = 1980L)
c(original = l1$book$title, shallow = shallow$book$title)
#> original shallow
#> "The Quiet Streets" "Another"
Source: R documentation: refClass
Read the source entry as well: it is 2000. When a field is assigned the reference object b, the field holds b itself, not a copy of it, so l1$book$year <- 2000L changed b. The documentation states this for initialisation as well: fields are simply assigned, and if the field is itself a reference object that object is not copied. The deep copy deep was made while the year was still 1999 and so keeps its own value; the shallow copy shares the book with the original until its field is reassigned to something else, after which only the shallow copy changes.
Inheritance and callSuper()
A subclass is declared with contains = "BookRC". Fields and methods are inherited, a subclass may add fields, and it may override a field only with a subclass of the inherited class. The scenario’s audio book adds a narrator and a length in minutes. An overriding method reaches the method it overrides with callSuper(...). The documentation points out that the arguments of the superclass method must be supplied explicitly: there is no convention that passes them on automatically, unlike NextMethod() for S3 or callNextMethod() for S4.
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", author = "character", isbn = "character",
year = "integer", genre = "character", pages = "integer"
),
methods = list(
initialize = function(...) {
initFields(...)
invisible(.self)
},
describe = function() {
yr <- if (length(year) == 0L || is.na(year)) "n.d." else year
sprintf("%s (%s, %s)", title, author, yr)
},
show = function() {
cat("<", class(.self)[[1L]], "> ", describe(), "\n", sep = "")
}
)
)
AudioBookRC <- setRefClass("AudioBookRC", # nolint: object_name_linter.
contains = "BookRC",
fields = list(narrator = "character", minutes = "integer"),
methods = list(
initialize = function(..., narrator = NA_character_,
minutes = NA_integer_) {
callSuper(..., narrator = narrator, minutes = minutes)
},
describe = function() {
paste0(callSuper(), ", read by ", narrator, ", ", minutes, " min")
}
)
)
a <- AudioBookRC$new(
title = "The Long Table", author = "Mara Quill", year = 2015L,
narrator = "Dev Patel", minutes = 540L
)
a
#> <AudioBookRC> The Long Table (Mara Quill, 2015), read by Dev Patel, 540 min
class(a)
#> [1] "AudioBookRC"
#> attr(,"package")
#> [1] ".GlobalEnv"
is(a, "BookRC")
#> [1] TRUE
inherits(a, "BookRC")
#> [1] TRUE
AudioBookRC$fields()
#> title author isbn year genre pages
#> "character" "character" "character" "integer" "character" "integer"
#> narrator minutes
#> "character" "integer"
AudioBookRC$className
#> [1] "AudioBookRC"
AudioBookRC$def@refSuperClasses
#> [1] "BookRC" "envRefClass"
# Inherited and overridden methods live in the same method table.
setdiff(AudioBookRC$methods(), BookRC$methods())
#> [1] "describe#BookRC" "initialize#BookRC"
# An AudioBookRC can be used wherever a BookRC is expected.
shelf <- list(
BookRC$new(title = "The Quiet Streets", author = "Iris Fenwick",
year = 1999L),
a
)
vapply(shelf, \(x) x$describe(), character(1L))
#> [1] "The Quiet Streets (Iris Fenwick, 1999)"
#> [2] "The Long Table (Mara Quill, 2015), read by Dev Patel, 540 min"
Source: R documentation: refClass
Three details of the output. class(a) is "AudioBookRC" with a package attribute that records where the class was defined (.GlobalEnv here, the package name for a class defined in a package), exactly as for an S4 class; inherits() and is() both see the superclass. setdiff(AudioBookRC$methods(), BookRC$methods()) shows how overriding is stored: the superclass versions survive under mangled names, describe#BookRC and initialize#BookRC, and callSuper() is resolved to them when the method is installed. And the initialize method of AudioBookRC has ... first: the named arguments narrator and minutes are taken, and everything else goes to the superclass, so the same method continues to work if BookRC gains a field.
Read-only fields, accessors and function fields
Gen$lock("loan_id") makes a field settable once. The documentation specifies that the lock method must be called as part of the class definition, that a locked field can be set one time, and that it can never be unlocked. A field that initialize assigns, even with a default, has used up its one assignment, which is why the listing locks a field that is left alone by the generator.
LoanRC <- setRefClass("LoanRC", # nolint: object_name_linter.
fields = list(
loan_id = "character", member = "character", returned = "logical"
)
)
LoanRC$lock("loan_id")
LoanRC$lock()
#> [1] "loan_id"
l <- LoanRC$new(member = "Ana Ruiz", returned = FALSE)
l$loan_id
#> character(0)
l$loan_id <- "L001"
l$loan_id
#> [1] "L001"
tryCatch(l$loan_id <- "L002", error = \(e) conditionMessage(e))
#> [1] "invalid replacement: reference class field ‘loan_id’ is read-only"
l$member <- "Dev Patel"
l$member
#> [1] "Dev Patel"
# $accessors() creates getFoo() and setFoo() methods.
LoanRC$accessors("member")
l2 <- LoanRC$new(member = "Ana Ruiz")
l2$getMember()
#> [1] "Ana Ruiz"
l2$setMember("Dev Patel")
l2$getMember()
#> [1] "Dev Patel"
# A field defined by a function is an active binding.
DueRC <- setRefClass("DueRC", # nolint: object_name_linter.
fields = list(
loan_date = "Date",
due = function(value) {
if (!missing(value)) stop("due is computed, not assigned", call. = FALSE)
loan_date + 21L
}
)
)
d <- DueRC$new(loan_date = as.Date("2026-06-01"))
d$due
#> [1] "2026-06-22"
tryCatch(d$due <- as.Date("2026-07-01"), error = \(e) conditionMessage(e))
#> [1] "due is computed, not assigned"
DueRC$fields()
#> loan_date due
#> "Date" "activeBindingFunction"
Source: R documentation: refClass
Gen$accessors("member") generated getMember() and setMember(). The documentation says that unless you like that style there is no reason to use the mechanism. It is not related to the last feature in the listing: a field given as a function instead of a class name is an accessor function, implemented as an active binding. It is called with no argument to read the field and with one argument to assign to it, and Gen$fields() reports its class as "activeBindingFunction". A computed field like due is the Reference Class counterpart of an R6 active binding (below). The documentation describes the mechanism as available but not generally recommended, because it blurs fields as data; a method due() that returns the date is usually clearer. The function receives the object’s fields by name like a method, and cannot be locked.
What goes wrong
A reference object checks more than a list does, and each check has a message. The listing collects the ones met in practice with the real text from R 4.6.1. The quotation marks in the messages are the typographic quotes that R uses in a UTF-8 session.
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(
title = "character", year = "integer", pages = "integer"
)
)
b <- BookRC$new(title = "The Quiet Streets", year = 1999L)
msg <- function(expr) tryCatch(expr, error = \(e) conditionMessage(e))
# 1. A field only accepts its declared class: 1999 is a double, NA is a
# logical, and neither is an integer.
msg(b$year <- "1999")
#> [1] "invalid assignment for reference class field ‘year’, should be from class “integer” or a subclass (was class “character”)"
msg(b$year <- 1999)
#> [1] "invalid assignment for reference class field ‘year’, should be from class “integer” or a subclass (was class “numeric”)"
msg(b$year <- NA)
#> [1] "invalid assignment for reference class field ‘year’, should be from class “integer” or a subclass (was class “logical”)"
msg(BookRC$new(title = "x", year = "1999"))
#> [1] "invalid assignment for reference class field ‘year’, should be from class “integer” or a subclass (was class “character”)"
b$year <- NA_integer_
b$year
#> [1] NA
# 2. Unknown fields and methods.
msg(b$publisher <- "Fenwick Press")
#> [1] "‘publisher’ is not a field in class “BookRC”"
msg(b$publisher)
#> [1] "‘publisher’ is not a valid field or method name for reference class “BookRC”"
msg(b$checkout())
#> [1] "‘checkout’ is not a valid field or method name for reference class “BookRC”"
msg(BookRC$new(title = "x", publisher = "y"))
#> [1] "‘publisher’ is not a field in class “BookRC”"
# 3. An unnamed argument of the default initializer must be an object of a
# reference class; here it only produces a warning and an empty object.
tryCatch(BookRC$new("The Quiet Streets"), warning = \(w) conditionMessage(w))
#> [1] "unnamed arguments to $new() must be objects from a reference class; got an object of class “character”"
# 4. .self is read-only.
msg(b$.self <- BookRC$new(title = "x"))
#> [1] "‘.self’ is not a field in class “BookRC”"
# 5. A local assignment to a field name is caught when the method is added.
withCallingHandlers(
BookRC$methods(wrong = function() {
pages <- 100L
pages
}),
warning = \(w) {
cat(conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
#> local assignment to field name will not change the field:
#> pages <- 100L
#> Did you mean to use "<<-"? ( in method "wrong" for class "BookRC")
b$wrong()
#> [1] 100
b$pages
#> integer(0)
# 6. A misspelled <<- target is caught too, but only by a heuristic.
Typo <- withCallingHandlers( # nolint: object_name_linter.
setRefClass("Typo",
fields = list(year = "integer"),
methods = list(
next_year = function() {
yeer <<- year + 1L
invisible(.self)
}
)
),
warning = \(w) {
cat(conditionMessage(w), "\n")
invokeRestart("muffleWarning")
}
)
#> non-local assignment to non-field names (possibly misspelled?)
#> yeer <<- year + 1L
#> ( in method "next_year" for class "Typo")
t1 <- Typo$new(year = 1999L)
exists("yeer")
#> [1] FALSE
t1$next_year()
exists("yeer")
#> [1] TRUE
t1$year
#> [1] 1999
# 7. copy() calls the generator with no arguments.
Strict <- setRefClass("Strict", # nolint: object_name_linter.
fields = list(title = "character"),
methods = list(
initialize = function(title) {
title <<- title
}
)
)
s <- Strict$new(title = "x")
msg(s$copy())
#> [1] "argument \"title\" is missing, with no default"
Source: R documentation: refClass
| Situation | What R reports |
|---|---|
Assigning a value of the wrong class ( |
|
Assigning to or creating a name that is not a field |
|
Reading an unknown name, or calling an unknown method |
|
An unnamed argument to the default initialiser |
|
Assigning |
|
Assigning a locked field a second time |
|
A local assignment to a field name in a method |
A warning when the method is installed: |
A |
A warning at the same time: |
|
|
Source: R documentation: refClass
The two warnings come from a heuristic check that runs when methods are installed. The documentation calls it a heuristic: it looks at the code of the method for local assignments to field names and for non-local assignments to names that are not fields. It cannot see a typo in a name that is also a variable somewhere else, nor an assignment hidden in a string passed to assign(), so <<- remains the most error-prone feature of the system.
R6 with R6Class()
R6 is a CRAN package by Winston Chang. Its documentation describes it as an implementation of encapsulated (classical) object-oriented programming that is similar to Reference Classes but more efficient and independent of S4 and the methods package. The first R6 release on CRAN was 1.0.1 (2014-07-17, from the CRAN archive); version 2.0 (2014-08-19) added the portable option and the $set() method; this page was checked against R6 2.6.1, which CRAN lists as published in February 2025. R6 is an add-on package and not part of R: library(R6) is needed, and a package that uses it lists R6 under Imports in its DESCRIPTION.
An R6 class is defined by one call, R6Class(classname, public = list(...), private = list(...), active = list(...), inherit = ...). Unlike setRefClass() it has no separate fields list and no field classes: fields and methods sit in the same public list, a field is declared with an initial value (NULL for "set later"), and every method reaches the object through self and the private members through private. The arguments:
| Argument | Meaning (default) |
|---|---|
|
The name of the class, used for S3 dispatch and printing. |
|
A list of public fields and methods. |
|
An optional list of private fields and methods, reached as |
|
An optional list of active-binding functions. |
|
The generator of the superclass; captured unevaluated and evaluated in |
|
|
|
|
|
|
|
|
|
|
|
The environment used as the parent of the method environments and of new objects; by default the environment where the class is defined ( |
Public members, initialize() and printing
The R6 version of the book re-implements the same rules in the same order as the Reference Class: initialize trims and converts, then calls validate(). Because R6 fields have no declared class, the conversion with as.integer() is the only thing that keeps year an integer, and nothing stops a later b1$year <- "x"; this is the main thing the Reference Class gives that R6 does not, and the active bindings of the next subsection are how R6 restores it where it matters.
library(R6)
source("scenario.R")
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL,
author = NULL,
isbn = NULL,
year = NULL,
genre = NULL,
pages = NULL,
initialize = function(title, author = NA_character_,
isbn = NA_character_, year = NA_integer_,
genre = NA_character_, pages = NA_integer_) {
self$title <- trimws(title)
self$author <- trimws(author)
self$isbn <- if (is.na(isbn)) isbn else normalise_isbn(isbn)
self$year <- as.integer(year)
self$genre <- trimws(genre)
self$pages <- as.integer(pages)
self$validate()
},
validate = function() {
fields <- list(self$title, self$author, self$isbn,
self$year, self$genre, self$pages)
if (any(lengths(fields) != 1L)) {
stop("every field of a book must have length 1", call. = FALSE)
}
if (is.na(self$title) || !nzchar(self$title)) {
stop("a book needs a title", call. = FALSE)
}
if (!is.na(self$isbn) && !is_valid_isbn13(self$isbn)) {
stop("invalid ISBN-13: ", self$isbn, call. = FALSE)
}
if (!is.na(self$year) && self$year > 2100L) {
stop("implausible year: ", self$year, call. = FALSE)
}
invisible(self)
},
format = function(...) {
yr <- if (is.na(self$year)) "n.d." else self$year
sprintf("%s (%s, %s)", self$title, self$author, yr)
},
print = function(...) {
cat("<BookR6> ", self$format(), "\n", sep = "")
invisible(self)
}
)
)
b1 <- BookR6$new(
title = " the quiet streets ", author = "Iris Fenwick",
isbn = "978-0-306-40615-7", year = 1999, genre = "Fiction", pages = 288
)
b1
#> <BookR6> the quiet streets (Iris Fenwick, 1999)
print(b1)
#> <BookR6> the quiet streets (Iris Fenwick, 1999)
b1$print()
#> <BookR6> the quiet streets (Iris Fenwick, 1999)
format(b1)
#> [1] "the quiet streets (Iris Fenwick, 1999)"
b1$isbn
#> [1] "9780306406157"
b1$year <- 2001L
b1$format()
#> [1] "the quiet streets (Iris Fenwick, 2001)"
try_book <- function(...) {
tryCatch(BookR6$new(...), error = \(e) conditionMessage(e))
}
try_book(title = "")
#> [1] "a book needs a title"
try_book(title = "Orchard Logic", isbn = "9780306406158")
#> [1] "invalid ISBN-13: 9780306406158"
try_book(title = "Far Future", year = 2101L)
#> [1] "implausible year: 2101"
BookR6$new(...) creates the environment, fills it with the fields and methods, locks it and calls initialize. Inside methods self is the object, so a field is read as self$year and written as self$year <- value with the ordinary <- (which is why there is no <<- in R6 code and no equivalent of the Reference Class trap). A method that returns invisible(self) can be chained, which the shelf example below uses.
The two methods named print and format are special. R6 registers S3 methods for class "R6", and print(b1), autoprinting and b1$print() all end at the public print method when it exists. If it does not, print.R6() prints format(x), and format.R6() calls a public format method when there is one and otherwise prints the default listing of the members. The arguments of print(x, ...) are passed on to x$print(...). That is why R6 objects print through your own method: the method is found by the S3 methods that R6 provides, not by a mechanism inside the object.
Private members and active bindings
The private list holds members that are reachable only from methods, through private$name; the object itself does not carry them, so b$.isbn is NULL. Active bindings look like fields but call a function every time they are read or assigned: the function takes one argument, which is missing on a read and holds the right-hand side on an assignment. They are always public. The listing moves isbn behind an active binding that normalises and validates on assignment (the Reference Class version validates only in initialize) and adds a read-only age, computed from year and the scenario’s fixed shelf_today.
library(R6)
source("scenario.R")
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL,
author = NULL,
year = NULL,
genre = NULL,
pages = NULL,
initialize = function(title, author = NA_character_,
isbn = NA_character_, year = NA_integer_,
genre = NA_character_, pages = NA_integer_) {
self$title <- trimws(title)
self$author <- trimws(author)
self$isbn <- isbn
self$year <- as.integer(year)
self$genre <- trimws(genre)
self$pages <- as.integer(pages)
}
),
private = list(
.isbn = NA_character_,
is_blank = function(x) is.na(x) || !nzchar(x)
),
active = list(
isbn = function(value) {
if (missing(value)) {
return(private$.isbn)
}
if (length(value) != 1L) {
stop("an ISBN must have length 1", call. = FALSE)
}
if (!private$is_blank(value)) {
value <- normalise_isbn(value)
if (!is_valid_isbn13(value)) {
stop("invalid ISBN-13: ", value, call. = FALSE)
}
}
private$.isbn <- value
},
age = function(value) {
if (!missing(value)) {
stop("age is read-only", call. = FALSE)
}
if (is.na(self$year)) {
return(NA_integer_)
}
as.integer(format(shelf_today, "%Y")) - self$year
}
)
)
b <- BookR6$new("The Quiet Streets", "Iris Fenwick", year = 1999L)
b$isbn
#> [1] NA
b$isbn <- "978-0-306-40615-7"
b$isbn
#> [1] "9780306406157"
b$age
#> [1] 27
msg <- function(expr) tryCatch(expr, error = \(e) conditionMessage(e))
msg(b$isbn <- "9780306406158")
#> [1] "invalid ISBN-13: 9780306406158"
msg(b$age <- 3L)
#> [1] "age is read-only"
msg(BookR6$new("Orchard Logic", isbn = "9780306406158"))
#> [1] "invalid ISBN-13: 9780306406158"
# Private members are not reachable through the object.
b$.isbn
#> NULL
b$is_blank
#> NULL
sort(names(b))
#> [1] ".__enclos_env__" "age" "author" "clone"
#> [5] "genre" "initialize" "isbn" "pages"
#> [9] "title" "year"
# The default print method lists public and private members.
b
#> <BookR6>
#> Public:
#> age: active binding
#> author: Iris Fenwick
#> clone: function (deep = FALSE)
#> genre: NA
#> initialize: function (title, author = NA_character_, isbn = NA_character_,
#> isbn: active binding
#> pages: NA
#> title: The Quiet Streets
#> year: 1999
#> Private:
#> .isbn: 9780306406157
#> is_blank: function (x)
An active binding that has no argument cannot be assigned to (the R6 documentation shows the error unused argument (quote(3))); the listing instead accepts a value argument and signs an error of its own, which gives a better message. Notice that the validation of isbn now applies on every assignment, not just at creation, and that BookR6$new(..., isbn = "9780306406158") reaches the same code because initialize does self$isbn <- isbn. The default print method lists public and private members and shows active bindings as active binding. It shows only the first line of a long function signature, which is why the initialize line ends in the middle of its arguments.
What an R6 object is
R6 objects are environments with a class attribute. The documentation says that an object consists of a public environment, and may also contain a private environment and environments for superclasses, and that in one sense the object and the public environment are the same. The generator is an object too, and has a print method.
library(R6)
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL,
year = NULL,
initialize = function(title, year = NA_integer_) {
self$title <- title
self$year <- as.integer(year)
},
age = function(today) {
as.integer(format(today, "%Y")) - self$year
}
),
private = list(
secret = "not for display"
),
active = list(
decade = function() self$year %/% 10L * 10L
)
)
# The generator is an object too, and has its own print method.
BookR6
#> <BookR6> object generator
#> Public:
#> title: NULL
#> year: NULL
#> initialize: function (title, year = NA_integer_)
#> age: function (today)
#> clone: function (deep = FALSE)
#> Active bindings:
#> decade: function ()
#> Private:
#> secret: not for display
#> Parent env: <environment: R_GlobalEnv>
#> Locked objects: TRUE
#> Locked class: FALSE
#> Portable: TRUE
b <- BookR6$new("The Quiet Streets", 1999L)
b
#> <BookR6>
#> Public:
#> age: function (today)
#> clone: function (deep = FALSE)
#> decade: active binding
#> initialize: function (title, year = NA_integer_)
#> title: The Quiet Streets
#> year: 1999
#> Private:
#> secret: not for display
is.environment(b)
#> [1] TRUE
class(b)
#> [1] "BookR6" "R6"
typeof(b)
#> [1] "environment"
isS4(b)
#> [1] FALSE
is.R6(b)
#> [1] TRUE
is.R6Class(BookR6)
#> [1] TRUE
inherits(b, "BookR6")
#> [1] TRUE
inherits(b, "environment")
#> [1] FALSE
# The S3 methods that R6 registers for class "R6".
methods(class = "R6")
#> [1] as.list format plot print
#> see '?methods' for accessing help and source code
# A public format() alone is enough to change how the object prints.
Label <- R6Class("Label", # nolint: object_name_linter.
public = list(format = function(...) "<label for shelf 3>")
)
Label$new()
#> <label for shelf 3>
format(Label$new())
#> [1] "<label for shelf 3>"
class(b) is c("BookR6", "R6"), typeof(b) is "environment", and isS4(b) is FALSE: an R6 object is an ordinary S3 object. The consequence for inherits(b, "environment") is easy to miss. inherits() looks at class(b), which has an explicit class attribute, so it does not see the implicit class "environment" and answers FALSE although is.environment(b) is TRUE. inherits(b, "BookR6") and inherits(b, "R6") are the tests to use, and R6::is.R6(b) and R6::is.R6Class(gen) are shortcuts for the second. Because the object is an environment, get(), assign(), ls() and exists() work on it like on any other environment; ls() omits names that begin with a dot, which is how R6 hides its internal member .__enclos_env__. The last lines of the listing show the rule for printing: a public format() method alone changes both format(x) and how the object is printed, because print.R6() falls back to format(x).
Inheritance and super
inherit = BookR6 makes a subclass. The expression is captured unevaluated and evaluated when an object is created, so the superclass may be defined in another package. A subclass sees the superclass through super: super$initialize(...) and super$format(...) call the superclass methods, which is the counterpart of callSuper() in Reference Classes. Unlike classical OOP in C++, a subclass can also call the private methods of its superclass through super$. The class vector of an object lists the whole chain, subclass first and "R6" last.
library(R6)
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL, author = NULL, isbn = NULL,
year = NULL, genre = NULL, pages = NULL,
initialize = function(title, author = NA_character_,
isbn = NA_character_, year = NA_integer_,
genre = NA_character_, pages = NA_integer_) {
self$title <- title
self$author <- author
self$isbn <- isbn
self$year <- as.integer(year)
self$genre <- genre
self$pages <- as.integer(pages)
},
format = function(...) {
yr <- if (is.na(self$year)) "n.d." else self$year
sprintf("%s (%s, %s)", self$title, self$author, yr)
},
print = function(...) {
cat("<", class(self)[[1L]], "> ", self$format(), "\n", sep = "")
invisible(self)
}
)
)
AudioBookR6 <- R6Class("AudioBookR6", # nolint: object_name_linter.
inherit = BookR6,
public = list(
narrator = NULL,
minutes = NULL,
initialize = function(..., narrator = NA_character_,
minutes = NA_integer_) {
super$initialize(...)
self$narrator <- narrator
self$minutes <- as.integer(minutes)
},
format = function(...) {
paste0(super$format(...), ", read by ", self$narrator,
", ", self$minutes, " min")
}
)
)
a <- AudioBookR6$new(
title = "The Long Table", author = "Mara Quill", year = 2015L,
narrator = "Dev Patel", minutes = 540L
)
a
#> <AudioBookR6> The Long Table (Mara Quill, 2015), read by Dev Patel, 540 min
class(a)
#> [1] "AudioBookR6" "BookR6" "R6"
inherits(a, "BookR6")
#> [1] TRUE
inherits(a, "R6")
#> [1] TRUE
AudioBookR6$classname
#> [1] "AudioBookR6"
shelf <- list(
BookR6$new("The Quiet Streets", "Iris Fenwick", year = 1999L),
a
)
vapply(shelf, \(x) x$format(), character(1L))
#> [1] "The Quiet Streets (Iris Fenwick, 1999)"
#> [2] "The Long Table (Mara Quill, 2015), read by Dev Patel, 540 min"
vapply(shelf, \(x) class(x)[[1L]], character(1L))
#> [1] "BookR6" "AudioBookR6"
vapply() over a list that mixes both classes works, and class(x)[[1L]] is the name of the most specific class: that is the dispatch-by-object style that Reference Classes and R6 share. If a superclass has cloneable = FALSE, its subclasses cannot be cloneable either (an R6 2.6.0 change).
Cloning
R6 objects have reference semantics, so alias <- b is another name for b and a change through one name is seen through the other. $clone() makes an independent copy. The copy is shallow by default: a field that holds another R6 object, an environment or a Reference Class object is copied as a reference, so the clone and the original share it. $clone(deep = TRUE) clones the fields that are R6 objects but, as the documentation says, not environments, Reference Class objects, or reference objects stored inside other data structures such as a list. For any other behaviour a class can define a private method deep_clone(name, value), which is called once for each field and returns the value that the clone gets.
library(R6)
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL, year = NULL,
initialize = function(title, year = NA_integer_) {
self$title <- title
self$year <- as.integer(year)
}
)
)
LoanR6 <- R6Class("LoanR6", # nolint: object_name_linter.
public = list(
loan_id = NULL, book = NULL, notes = NULL,
initialize = function(loan_id, book) {
self$loan_id <- loan_id
self$book <- book
self$notes <- list(book)
}
)
)
b <- BookR6$new("The Quiet Streets", 1999L)
alias <- b
alias$year <- 2000L
b$year
#> [1] 2000
copy <- b$clone()
copy$year <- 1950L
c(b = b$year, copy = copy$year)
#> b copy
#> 2000 1950
l1 <- LoanR6$new("L001", b)
shallow <- l1$clone()
deep <- l1$clone(deep = TRUE)
l1$book$year <- 2010L
c(original = l1$book$year, shallow = shallow$book$year,
deep = deep$book$year)
#> original shallow deep
#> 2010 2010 2000
# deep = TRUE copies R6 objects held directly in a field, but not those
# hidden in a list.
l1$notes[[1L]]$year <- 2011L
c(original = l1$notes[[1L]]$year, deep = deep$notes[[1L]]$year)
#> original deep
#> 2011 2011
# A private deep_clone() method decides what happens to each field.
LoanDeep <- R6Class("LoanDeep", # nolint: object_name_linter.
inherit = LoanR6,
private = list(
deep_clone = function(name, value) {
if (name == "notes") {
lapply(value, \(x) x$clone())
} else if (inherits(value, "R6")) {
value$clone()
} else {
value
}
}
)
)
l2 <- LoanDeep$new("L002", b)
d2 <- l2$clone(deep = TRUE)
l2$notes[[1L]]$year <- 1900L
c(original = l2$notes[[1L]]$year, deep = d2$notes[[1L]]$year)
#> original deep
#> 1900 2011
# A reference object in a field default is shared by all instances.
Shared <- R6Class("Shared", # nolint: object_name_linter.
public = list(log = new.env())
)
s1 <- Shared$new()
s2 <- Shared$new()
s1$log$entry <- "L001 out"
s2$log$entry
#> [1] "L001 out"
identical(s1$log, s2$log)
#> [1] TRUE
NotShared <- R6Class("NotShared", # nolint: object_name_linter.
public = list(
log = NULL,
initialize = function() self$log <- new.env()
)
)
n1 <- NotShared$new()
n2 <- NotShared$new()
n1$log$entry <- "L001 out"
n2$log$entry
#> NULL
Follow the numbers. After l1$book$year <- 2010L, the original and the shallow clone both show 2010 because they hold the same book; the deep clone, made earlier, holds its own book and keeps 2000. After l1$notes[[1L]]$year <- 2011L the deep clone still shows 2011: the book inside the list notes was never cloned by deep = TRUE, so deep$notes[[1L]] is still b. LoanDeep fixes that with a deep_clone that clones the list elements and any other R6 field. The last part of the listing shows the related trap that R6 and Reference Classes share, a reference object used as a default: Shared creates one environment when the class is defined and every object gets that same environment, so s2$log$entry shows what s1 wrote. The documented fix is the one in NotShared: declare the field as NULL and create the object in initialize.
Locking, $set(), cloneable and class = FALSE
R6 locks aggressively. The environment of an object is locked unless lock_objects = FALSE, so a typo cannot silently create a new field, and the methods are locked bindings. The generator can be locked with lock_class = TRUE; $set() adds a member to an unlocked generator, but only objects created afterwards have it.
library(R6)
Point <- R6Class("Point", # nolint: object_name_linter.
public = list(x = 0, y = 0)
)
msg <- function(expr) tryCatch(expr, error = \(e) conditionMessage(e))
# lock_objects = TRUE (the default): no new members on an object.
p <- Point$new()
msg(p$z <- 1)
#> [1] "cannot add bindings to a locked environment"
p$x <- 10
p$x
#> [1] 10
# lock_objects = FALSE allows ad hoc members.
Loose <- R6Class("Loose", # nolint: object_name_linter.
public = list(x = 0),
lock_objects = FALSE
)
q <- Loose$new()
q$z <- 1
q$z
#> [1] 1
# Methods are locked: they cannot be replaced on an object.
Greeter <- R6Class("Greeter", # nolint: object_name_linter.
public = list(hello = function() "hello")
)
g <- Greeter$new()
msg(g$hello <- function() "bye")
#> [1] "cannot change value of locked binding for 'hello'"
# $set() adds a member to the generator; only objects created afterwards
# get it.
Greeter$set("public", "bye", function() "bye")
older <- g
fresh <- Greeter$new()
c(older = is.null(older$bye), fresh = is.null(fresh$bye))
#> older fresh
#> TRUE FALSE
fresh$bye()
#> [1] "bye"
# lock_class = TRUE freezes the generator itself.
Frozen <- R6Class("Frozen", # nolint: object_name_linter.
public = list(x = 1),
lock_class = TRUE
)
msg(Frozen$set("public", "y", 2))
#> [1] "Can't modify a locked R6 class."
Frozen$is_locked()
#> [1] TRUE
Frozen$unlock()
Frozen$set("public", "y", 2)
Frozen$lock()
Frozen$is_locked()
#> [1] TRUE
# cloneable = FALSE removes $clone().
Once <- R6Class("Once", # nolint: object_name_linter.
public = list(x = 1),
cloneable = FALSE
)
o <- Once$new()
"clone" %in% names(o)
#> [1] FALSE
msg(o$clone())
#> [1] "attempt to apply non-function"
"clone" %in% names(Point$new())
#> [1] TRUE
# class = FALSE: a plain environment, no S3 dispatch.
Plain <- R6Class("Plain", # nolint: object_name_linter.
public = list(x = 1),
class = FALSE
)
pl <- Plain$new()
class(pl)
#> [1] "environment"
class(Point$new())
#> [1] "Point" "R6"
is.environment(pl)
#> [1] TRUE
The messages are those of base R for locked environments and bindings (cannot add bindings to a locked environment, cannot change value of locked binding for 'hello'; see lockEnvironment() and lockBinding() on the R documentation: bindenv page) plus R6’s own Can't modify a locked R6 class. for the generator. cloneable = FALSE removes the $clone method, so o$clone() fails with attempt to apply non-function because o$clone is NULL. class = FALSE returns a plain environment without the "R6" class attribute: the documentation notes that it avoids the class-based dispatch of $ and so is slightly faster, but without a class attribute S3 methods, including the pretty printing, do not apply. $set() takes overwrite = TRUE to replace an existing member and, since R6 2.3.0, adds a member with a NULL value instead of removing it.
Portable and non-portable classes
By default (portable = TRUE) an R6 class can be inherited from another package, and every member must be reached with self$ or private$. With portable = FALSE an R6 class behaves more like a Reference Class: members are found by name and <<- assigns them, but inheritance across packages does not work well. The difference is the enclosing environment of the methods. In a portable class each method is enclosed in a small environment that holds self (and private, super), and that environment’s parent is given by parent_env (by default the environment where the class was defined); this is why an inherited method that calls a non-exported function of its own package still finds it. In a non-portable class the methods are enclosed in the object’s own environment, so fields are visible by name but a subclass defined in another package cannot see the helper functions that the superclass used. The Portable article explains it with two simulated packages, which this page does not repeat.
library(R6)
# Non-portable: fields are found by name, and <<- assigns them.
Counter <- R6Class("Counter", # nolint: object_name_linter.
portable = FALSE,
public = list(
n = 0L,
increment = function() {
n <<- n + 1L
invisible(self)
},
get = function() n
)
)
cnt <- Counter$new()
cnt$increment()$increment()
cnt$get()
#> [1] 2
# Portable (the default): the same body does not work.
Broken <- R6Class("Broken", # nolint: object_name_linter.
public = list(
n = 0L,
increment = function() {
n <<- n + 1L
invisible(self)
},
get = function() n
)
)
br <- Broken$new()
tryCatch(br$increment(), error = \(e) conditionMessage(e))
#> [1] "object 'n' not found"
tryCatch(br$get(), error = \(e) conditionMessage(e))
#> [1] "object 'n' not found"
# The portable version names the object explicitly.
Fixed <- R6Class("Fixed", # nolint: object_name_linter.
public = list(
n = 0L,
increment = function() {
self$n <- self$n + 1L
invisible(self)
},
get = function() self$n
)
)
fx <- Fixed$new()
fx$increment()$increment()
fx$get()
#> [1] 2
# In a non-portable class the methods live in the object's own
# environment, so the object is its own enclosure.
identical(environment(cnt$get), as.environment(cnt))
#> [1] TRUE
identical(environment(fx$get), as.environment(fx))
#> [1] FALSE
class(environment(fx$get))
#> [1] "environment"
ls(environment(fx$get))
#> [1] "self"
The portable Broken class has the same method bodies as the non-portable Counter, and both of its methods fail with object 'n' not found. The Portable article points out a worse form of the same mistake: y <<- 20 in a portable method assigns to a variable y in the global environment, and no error is raised, because <<- found no y to modify in the enclosing environments and created one. Reading the enclosing environments in the last four lines shows the structure: for the non-portable object the method’s environment is the object, for the portable one it is a separate environment that holds self.
Debugging and finalizers
The R6 Debugging article gives the tools: add browser() where the debugger should stop (the article warns that RStudio breakpoints do not work in R6 methods); call debug() on one method of one object, debug(s$getx); or call the generator’s $debug("getx"), which flags the method for objects created afterwards, and $undebug("getx") to stop. trace() can insert a breakpoint at a given place. For Reference Classes, debug(obj$method) and obj$trace(what, browser) do the same. A non-interactive script cannot answer the browser prompt, so the listing only checks the flags with isdebugged(). See Debugging for the interactive tools.
A finalizer is a function that R runs when an object is garbage collected, and also when R exits. In R6 it is a private method named finalize (R6 2.4.0 allowed it to be private; 2.6.0 prints a message when it is public). In Reference Classes it is the method finalize, registered by the object with atexit = TRUE. Both are implemented with reg.finalizer() and are meant for releasing a resource, such as a file or a database connection, that the object owns.
library(R6)
Simple <- R6Class("Simple", # nolint: object_name_linter.
public = list(
x = 10,
getx = function() self$x
)
)
# debug() on one method of one object: nothing is run here, because a
# non-interactive script cannot answer the browser prompt.
s <- Simple$new()
isdebugged(s$getx)
#> [1] FALSE
debug(s$getx)
isdebugged(s$getx)
#> [1] TRUE
undebug(s$getx)
isdebugged(s$getx)
#> [1] FALSE
# The generator's $debug() flags the method for objects created later.
Simple$debug("getx")
isdebugged(Simple$new()$getx)
#> [1] TRUE
isdebugged(s$getx)
#> [1] FALSE
Simple$undebug("getx")
isdebugged(Simple$new()$getx)
#> [1] FALSE
# Finalizers run when the object is garbage collected, and at exit.
Resource <- R6Class("Resource", # nolint: object_name_linter.
public = list(
initialize = function(name) private$name <- name
),
private = list(
name = NULL,
finalize = function() cat("closing", private$name, "\n")
)
)
res <- Resource$new("loans.csv")
rm(res)
invisible(gc())
#> closing loans.csv
ResourceRC <- setRefClass("ResourceRC", # nolint: object_name_linter.
fields = list(name = "character"),
methods = list(
finalize = function() cat("closing", name, "\n")
)
)
res2 <- ResourceRC$new(name = "books.csv")
rm(res2)
invisible(gc())
#> closing books.csv
The finalizer lines depend on the garbage collector: rm() only removes the name, and the finalizer runs at the next collection, which the explicit gc() forces here. In a real program the closing may happen much later, or only when R exits, so a finalizer is a safety net and not a substitute for an explicit close() method.
Performance notes
The R6 Performance article compares Reference Classes, R6 and plain environments created by a function. Its conclusions are qualitative and are worth knowing, but the numbers depend on the machine and on the versions, so this page quotes none of them. Creating a Reference Class object is much slower than creating an R6 object, the first Reference Class object in a session costs a large amount of memory that later Reference Class objects share, and reading and writing a field is slower in a Reference Class (partly because of the class check on every assignment). Within R6, a class attribute adds a small cost to $ (so class = FALSE is faster), portable = FALSE avoids the self$ lookup, and cloneable = FALSE saves the memory of the $clone method. The article says that for most uses these differences are negligible; choose between the systems on features, not on speed, and measure (see Performance, Profiling and Benchmarking) before changing the options.
Living with S4 and S3
Because the two systems are built on different foundations, they meet the rest of R differently. A Reference Class is an S4 class: S4 methods defined with setMethod() apply to its objects, setValidity() rules are run by validObject() (and, since R 3.4.0, automatically by the default initialiser), and isVirtualClass("BookRC") is FALSE while is(b, "envRefClass") is TRUE. An R6 object is an S3 object whose class attribute is c("BookR6", "R6"): S3 methods such as length.BookR6, summary.BookR6 and as.character.BookR6 dispatch on it, and S4 needs setOldClass(c("BookR6", "R6")) to treat the class as a registered old class; after that setMethod() and setGeneric() can name "BookR6". The registered class is a virtual S4 class (isVirtualClass("BookR6") is TRUE in the listing): it exists so that S4 can talk about the S3 class, not to create instances.
library(R6)
# Reference classes are S4 classes: S4 methods and validity apply.
BookRC <- setRefClass("BookRC", # nolint: object_name_linter.
fields = list(title = "character", year = "integer")
)
setMethod("length", "BookRC", function(x) 1L)
setMethod("summary", "BookRC", function(object, ...) {
cat("RC book:", object$title, "\n")
})
invisible(setValidity("BookRC", function(object) {
year <- object$year
if (length(year) == 1L && !is.na(year) && year > 2100L) {
"year is implausible"
} else {
TRUE
}
}))
b <- BookRC$new(title = "Far Future", year = 2025L)
length(b)
#> [1] 1
summary(b)
#> RC book: Far Future
b$year <- 2101L
tryCatch(validObject(b), error = \(e) conditionMessage(e))
#> [1] "invalid class “BookRC” object: year is implausible"
is(b, "envRefClass")
#> [1] TRUE
existsMethod("length", "BookRC")
#> [1] TRUE
# R6 objects are ordinary S3 objects: S3 methods apply, S4 needs setOldClass.
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL,
initialize = function(title) self$title <- title
)
)
length.BookR6 <- function(x) 1L
summary.BookR6 <- function(object, ...) {
cat("R6 book:", object$title, "\n")
}
r <- BookR6$new("The Quiet Streets")
length(r)
#> [1] 1
summary(r)
#> R6 book: The Quiet Streets
setOldClass(c("BookR6", "R6"))
setGeneric("shelf_label", function(x) standardGeneric("shelf_label"))
#> [1] "shelf_label"
setMethod("shelf_label", "BookR6", function(x) paste("R6:", x$title))
shelf_label(r)
#> [1] "R6: The Quiet Streets"
is(r, "BookR6")
#> [1] TRUE
isVirtualClass("BookR6")
#> [1] TRUE
Source: R documentation: setOldClass
The setGeneric() call prints its name, as it always does. Methods for length() and summary() can be added with setMethod() without creating a generic first, because the generics already exist. The documentation also says why S4 inheritance from a non-reference S4 class is a bad idea: slots are treated functionally and fields by reference, so changes to the two kinds of property behave inconsistently inside one object.
A worked example: lending books
The scenario needs an object that lends books and takes them back, and whose log of what happened is mutated in place as the day goes by. That is state with an identity: there is one shelf, every part of the program that talks to the shelf must talk to the same one, and a function that lends a book must not have to return a new shelf. The listing builds BookR6 objects from the valid rows of books (it reuses the scenario’s book() for the cleaning and validation), puts them on a ShelfR6, and keeps the loan log as a private character vector. The methods that change the shelf return invisible(self) so that calls can be chained, and each business rule raises an error with a message of its own.
library(R6)
source("scenario.R")
BookR6 <- R6Class("BookR6", # nolint: object_name_linter.
public = list(
title = NULL, author = NULL, isbn = NULL, year = NULL,
genre = NULL, pages = NULL,
initialize = function(title, author = NA_character_,
isbn = NA_character_, year = NA_integer_,
genre = NA_character_, pages = NA_integer_) {
b <- book(title, author, isbn, year, genre, pages)
for (name in names(b)) {
self[[name]] <- b[[name]]
}
},
format = function(...) {
sprintf("%s (%s)", self$title, self$isbn)
}
)
)
ShelfR6 <- R6Class("ShelfR6", # nolint: object_name_linter.
public = list(
initialize = function(books) {
names(books) <- vapply(books, \(b) b$isbn, character(1L))
private$books <- books
},
lend = function(isbn, member, on) {
if (is.null(private$books[[isbn]])) {
stop("no book with ISBN ", isbn, call. = FALSE)
}
if (isbn %in% names(private$out)) {
stop("already on loan to ", private$out[[isbn]], call. = FALSE)
}
private$out[[isbn]] <- member
private$record("out", isbn, member, on)
invisible(self)
},
give_back = function(isbn, on) {
if (!isbn %in% names(private$out)) {
stop("not on loan: ", isbn, call. = FALSE)
}
private$record("back", isbn, private$out[[isbn]], on)
private$out <- private$out[names(private$out) != isbn]
invisible(self)
},
available = function() {
private$books[!names(private$books) %in% names(private$out)]
},
history = function() private$log
),
private = list(
books = list(),
out = character(),
log = character(),
record = function(what, isbn, member, on) {
entry <- sprintf("%s %-4s %s %s", format(on), what, isbn, member)
private$log <- c(private$log, entry)
}
)
)
rows <- books[is_valid_isbn13(books$isbn), ]
catalogue <- lapply(seq_len(nrow(rows)), \(i) {
do.call(BookR6$new, as.list(rows[i, ]))
})
length(catalogue)
#> [1] 14
shelf <- ShelfR6$new(catalogue)
first <- catalogue[[1L]]$isbn
second <- catalogue[[2L]]$isbn
shelf$lend(first, "Ana Ruiz", shelf_today - 5L)$lend(
second, "Dev Patel", shelf_today - 2L
)
length(shelf$available())
#> [1] 12
# A second name for the shelf sees the same log: it is one object.
desk <- shelf
desk$give_back(first, shelf_today)
shelf$history()
#> [1] "2026-06-25 out 9780306406157 Ana Ruiz"
#> [2] "2026-06-28 out 9780141439518 Dev Patel"
#> [3] "2026-06-30 back 9780306406157 Ana Ruiz"
unname(vapply(shelf$available()[1:3], \(b) b$format(), character(1L)))
#> [1] "The Dispossessed (9780306406157)" "the quiet streets (9780001000100)"
#> [3] "Loud Roofs (9780002000291)"
msg <- function(expr) tryCatch(expr, error = \(e) conditionMessage(e))
msg(shelf$lend(second, "Ana Ruiz", shelf_today))
#> [1] "already on loan to Dev Patel"
msg(shelf$lend("9780000000000", "Ana Ruiz", shelf_today))
#> [1] "no book with ISBN 9780000000000"
msg(shelf$give_back(first, shelf_today))
#> [1] "not on loan: 9780306406157"
Points to read in the output. length(catalogue) is 14: fourteen of the sixteen ISBNs in books are valid (the scenario’s reference result), and the two rows left out are the empty ISBN and the one with the wrong check digit. The chained call shelf$lend(...)$lend(...) works because lend() returns invisible(self). desk is the same shelf under a second name, so desk$give_back(...) appears in shelf$history(); passing the shelf to a function would behave the same way. Private state (books, out, log) can only be read through the methods, so history() returns a copy of the log vector that callers can keep without affecting the shelf. The vapply() over shelf$available() is the usual way to use a list of reference objects: each element is an object with a method, and vapply() asks each for a string.
The same design in a Reference Class would use fields = list(out = "character", log = "character"), an assignment log <<- c(log, entry) in record(), and callSuper(...) in initialize. The behaviour is identical; what changes is the vocabulary of the class and the cost of creating the objects.
When reference semantics are the right tool
Reference semantics are a tool for state that has an identity and a lifetime. A rule of thumb that fits the examples of this page: use a reference object when "the same one" matters more than "an equal one".
-
Connections and other resources. A file, a database connection or a socket is a handle that must be opened once, used by several pieces of code and closed once. R’s own connections are reference-like (see Connections and the File System), and a class that owns one can open it in
initialize, expose methods that use it, and close it in aclose()method and, as a safety net, infinalize. -
Caches and memoisation. A cache must outlive any one call and must be shared by all callers.
IsbnCachebelow keeps its state in an environment; compare it with the closure-based memoisation of Function Factories and Higher-Order Functions, which gets the same effect without a class. -
Shiny and web servers. A Shiny session and its reactive values are shared mutable objects:
reactiveValues()returns an object that is read and written like a list but is modified in place byvalues$a <- 3and shared by every reactive that holds it, and modules and plumber routers hold state between requests (see Shiny, plumber and Web APIs). Many of the packages in that area are themselves written with R6, as the listing below shows. -
Stateful parsers, queues and loggers. A tokenizer that remembers its position, a queue of members waiting for a book, a logger that appends to one file: each is an object whose methods advance or consume its state.
-
Simulation entities. Agents that each have a position and a history, and that are updated one step at a time, are the textbook case in other languages as well.
library(R6)
source("scenario.R")
# A cache whose state outlives any one call.
IsbnCache <- R6Class("IsbnCache", # nolint: object_name_linter.
public = list(
hits = 0L,
misses = 0L,
initialize = function() {
private$store <- new.env(parent = emptyenv())
},
is_valid = function(isbn) {
if (exists(isbn, envir = private$store, inherits = FALSE)) {
self$hits <- self$hits + 1L
return(get(isbn, envir = private$store, inherits = FALSE))
}
self$misses <- self$misses + 1L
result <- is_valid_isbn13(isbn)
assign(isbn, result, envir = private$store)
result
},
size = function() length(ls(private$store))
),
private = list(store = NULL)
)
cache <- IsbnCache$new()
check_all <- function(isbns, cache) {
vapply(isbns, cache$is_valid, logical(1L), USE.NAMES = FALSE)
}
isbns <- books$isbn[1:6]
check_all(isbns, cache)
#> [1] TRUE TRUE TRUE TRUE TRUE TRUE
check_all(isbns, cache)
#> [1] TRUE TRUE TRUE TRUE TRUE TRUE
c(hits = cache$hits, misses = cache$misses, size = cache$size())
#> hits misses size
#> 6 6 6
# Which of the packages installed in this session import R6 (or depend on it)?
db <- installed.packages()
users <- tools::package_dependencies(
"R6", db = db, which = c("Depends", "Imports"), reverse = TRUE
)[["R6"]]
sort(users)
#> [1] "callr" "dbplyr" "desc" "dplyr" "fiery"
#> [6] "fireproof" "firesafety" "firesale" "firestorm" "httpuv"
#> [11] "httr" "httr2" "pkgbuild" "plumber" "plumber2"
#> [16] "processx" "progress" "promises" "rcmdcheck" "readr"
#> [21] "reqres" "routr" "roxygen2" "sass" "scales"
#> [26] "shiny" "storr" "testthat" "websocket"
The second listing lists the installed packages that import or depend on R6. The list is for the library this page was checked with, not for CRAN as a whole (the CRAN page of R6 lists all reverse dependencies); it includes shiny, plumber, httr2, testthat, roxygen2 and dplyr, which is a fair picture of how R6 is used: inside infrastructure packages for servers, sessions, requests and test doubles.
The cases where reference semantics are the wrong tool are as clear:
-
Value-like data. A
book, a date, a money amount or a data frame is a value: two books with the same fields are the same book, and nobody wants a function to change the argument it was given. Use a list, an S3, S4 or S7 object and copy-on-modify. The S3bookof the scenario is the model; the table at the end of The S7 Object System compares the four ways of writing it. -
Code that is run in parallel. Workers started with
parallelorfuturereceive a copy of an object (or a forked copy-on-write image), so changes made to a reference object in a worker never reach the parent; see Parallel and Asynchronous Computing. -
Code that is saved and restored.
saveRDS()serialises the environment with its contents, so a restored object is a copy that no longer shares anything with the session it came from (Serialization: RDS, RData and JSON). -
Pipelines. The native pipe
|>passes the result of one step to the next, which reads naturally for functions that take and return values (Pipes and the Native Pipe). Methods that change one object in place are chained with$andinvisible(self)instead, and mixing the two styles in one expression is confusing. -
Anything that needs to be reasoned about locally. When a function can change what it receives, a reader of the calling code can no longer assume that its variables keep their values. Keep the reference object small, hide its fields behind methods, and document which methods change it.
Reference Classes or R6?
Both systems give the same capabilities in different dress. R6 is the choice in new code that needs mutable objects, for the reasons given in its documentation (lighter objects, private members, cross-package inheritance that works) and because so much infrastructure already uses it. Reference Classes remain the right tool when a class must be an S4 class (typed fields checked on every assignment, setValidity(), setMethod() on the class) or when no add-on package may be used. S4 and S7 are better choices when objects are values and behaviour is attached to functions rather than to objects.
| Aspect | Reference Classes | R6 |
|---|---|---|
Where it lives |
|
R6 package on CRAN (2.6.1 here) |
Defined with |
|
|
Field classes |
Declared and checked on every assignment |
None; validate in methods or active bindings |
State inside a method |
By name; |
|
Constructor |
|
|
Copying |
|
|
Inheritance |
|
|
Private members |
None; every field is reachable with |
|
Active bindings |
A function as the field’s class: |
|
Printing |
|
|
Locking |
|
|
Add members later |
|
|
Debugging |
|
|
Finalizer |
|
private |
Integration |
S4: |
S3 methods; |
Across packages |
Limited: |
|
Speed and size |
Slower to create, larger (R6 Performance article) |
Faster to create, lighter |
In CRAN packages |
Nothing to declare beyond |
|
Source: R documentation: refClass
Version notes
| Version | Change | Source |
|---|---|---|
R 2.12.0 |
Reference Classes are added to the |
NEWS 2.12.0 |
R 3.0.0 |
|
NEWS 3.0.0 |
R 3.3.0 |
Reference class finalizers run at exit as well as on garbage collection. |
NEWS 3.3.0 |
R 3.3.2 |
Calls to |
NEWS 3.3.2 |
R 3.4.0 |
A reference class validity method is called by the default initialisation method. |
NEWS 3.4.0 |
R 3.5.0 |
|
NEWS 3.5.0 |
R 4.0.0 to 4.6.1 |
No entry in the release NEWS mentions Reference Classes or |
R NEWS |
R6 1.0.1 (2014-07-17) |
First release on CRAN. |
CRAN archive, R6 changelog |
R6 2.0 (2014-08-19) |
|
R6 changelog |
R6 2.4.0 (2019-02-14) |
Finalizers can be private; finalizers run on cloned objects. |
R6 changelog |
R6 2.5.0 (2020-10-28) |
Clearer message when |
R6 changelog |
R6 2.6.0 (2025-02-12) |
A message when a finalizer is public; a non-cloneable superclass makes subclasses non-cloneable; the deprecated |
R6 changelog |
R6 2.6.1 (CRAN, February 2025) |
|
R6 changelog |
The NEWS entries for R 2.12.0 are at NEWS.2; those for R 4.0.0 to 4.6.1 are in the current NEWS, and the development version in the R-devel NEWS. The R6 entries are from the R6 changelog.
Where older tutorials differ
-
"R5". Older material calls Reference Classes R5. They are not a separate system and R does not use the name; they are
setRefClass()in themethodspackage. The R6 package was named after the joke. -
Creating objects and adding methods. Older tutorials create objects with
Gen$new(...)ornew("BookRC", ...)and add methods in separateGen$methods(...)calls. All of that still works. Since R 3.0.0 the value ofsetRefClass()is also a function, soBookRC(...)is the same asBookRC$new(...), and supplying the methods in the call tosetRefClass()or in one or more$methods()calls has the same effect; the help page recommends the second for readability. -
Closures with
<<-as the only way to keep state. A function that returns a list of functions sharing an environment was the classic idiom for objects with state, and it still works (see Scoping, Environments and Closures). It gives no class, no inheritance and no printing; Reference Classes and R6 are the supported ways to write classes of mutable objects. -
The
protopackage. Tutorials from the era before Reference Classes recommend theprotopackage for prototype-based programming (objects that inherit from other objects). It is still on CRAN, but its last release is from 2016 and R6 or Reference Classes are the usual choices now. -
R6 in non-portable style. Some tutorials write
portable = FALSEeverywhere, so that methods can usexandx <<- valuewithoutself$. It works in a single package and is a little faster, but it breaks inheritance across packages and, when the habit is carried into a portable class, invites the global-variable mistake described above. The R6 documentation makes portable classes the default for that reason. -
The
lockargument ofR6Class(). It was deprecated and has been removed in R6 2.6.0; uselock_objects. -
Public
finalize. Before R6 2.4.0 a finalizer had to be public. Since 2.4.0 it can (and should) be private, and 2.6.0 prints a message for a public one. -
$copy()and$clone()confusion. Many tutorials showy <- xand then are surprised thatychangesx; the cure is$copy()for Reference Classes and$clone()for R6, and for R6deep = TRUEonly when a field is another R6 object (an environment or an object in a list needs adeep_clonemethod). Both names appear in older material for the other system. -
"Reference classes are slow, so avoid them." The R6 performance article shows that creating a Reference Class object costs more than an R6 object and field access is slower. It does not say that Reference Classes are unusable, and R Core has not deprecated them: there is no NEWS entry about them in R 4.0.0 to 4.6.1.
-
Typed fields in R6. Tutorials sometimes say that R6 has no way to check a field. The usual way is an active binding that validates assignment, as
isbndoes above.
Where to go next
-
OOP Overview and Base Types for the five systems side by side and the base types underneath them.
-
S3 Classes and Generics and S4 Classes and Methods for the function-based systems whose objects keep value semantics.
-
The S7 Object System for the newer system and a comparison script that runs trimmed versions of this page’s
BookRCandBookR6next to S3, S4 and S7 classes; its summary table has an R6 column, and the Reference Class is described in the text. -
Scoping, Environments and Closures and Memory and Copy Semantics for the environments and copy rules that explain the behaviour.
-
Function Factories and Higher-Order Functions for caches and state kept in closures.
-
Debugging for
debug(),browser()andtrace(). -
Connections and the File System for the best-known reference-like resource.
-
Shiny, plumber and Web APIs for stateful servers.
-
Packages, Libraries and Namespaces and Creating Packages for putting
R6underImportsand defining classes in a package.