Menu
Coddy logo textTech

Защита состояния

Часть раздела Объектно-ориентированное программирование путешествия по R на Coddy. Урок 20 из 57.

Сеттер — это единственное место, где значение может измениться, поэтому именно здесь следует применять правила. Сначала он выполняет проверку и вызывает stop(), прежде чем что-либо изменится, поэтому объект остаётся корректным:

make_account <- function(balance = 0) {
  withdraw <- function(x) {
    if (x <= 0) stop("amount must be positive", call. = FALSE)
    if (x > balance) stop("insufficient funds", call. = FALSE)
    balance <<- balance - x
  }
  list(withdraw = withdraw, balance = function() balance)
}
acc <- make_account(50)
msg <- tryCatch(acc$withdraw(80), error = function(e) conditionMessage(e))
cat(msg, "/", acc$balance())

Вывод:

insufficient funds / 50

lockBinding("rate", e) делает одну переменную окружения доступной только для чтения. Присваивание ей после этого вызывает ошибку, а остальные переменные остаются доступными для записи:

loan <- new.env()
loan$rate <- 0.05
loan$owed <- 1000
lockBinding("rate", loan)
loan$owed <- 900
msg <- tryCatch({ loan$rate <- 0; "changed" }, error = function(e) conditionMessage(e))
cat(msg, "\n")
cat(loan$rate, loan$owed, "\n")

Вывод:

cannot change value of locked binding for 'rate' 
0.05 900 

lockEnvironment(e) запрещает добавлять новые переменные. При использовании bindings = TRUE она также блокирует каждую существующую переменную, что делает весь объект доступным только для чтения:

cfg <- new.env()
cfg$theme <- "dark"
lockEnvironment(cfg, bindings = TRUE)
for (step in c("change", "add")) {
  msg <- tryCatch({
    if (step == "change") cfg$theme <- "light" else cfg$size <- 12
    "done"
  }, error = function(e) conditionMessage(e))
  cat(step, ":", msg, "\n")
}

Результат:

change : cannot change value of locked binding for 'theme' 
add : cannot add bindings to a locked environment 

local() выполняет код в новом окружении и возвращает его последнее значение. Вспомогательные функции, определённые внутри, остаются скрытыми, благодаря чему файл может сохранять функцию приватной, даже если source() помещает всё остальное в глобальное окружение:

circle_area <- local({
  square <- function(x) x * x
  function(r) pi * square(r)
})
print(round(circle_area(2), 2))
print(exists("square"))

Результат:

[1] 12.57
[1] FALSE
challenge icon

Задание

Легко

Завершите фабрику make_account(owner) в Account.R. Баланс начинается с 0, а приватная история записывает каждое изменение как +50 или -20. Возвращаемый список содержит четыре функции:

  • deposit(x) завершается с ошибкой amount must be positive, если только x не больше 0;
  • withdraw(x) завершается с ошибкой amount must be positive или insufficient funds;
  • balance() возвращает баланс;
  • history() возвращает историю как вектор символов.

Проверяйте всё до внесения изменений и используйте call. = FALSE. Предоставленный код считывает владельца, затем строки dep 50 или wd 20, выводит ok или error: и сообщение для каждой строки, пытается изменить копию истории, а затем выводит баланс и историю.

Ваш код должен находиться в Account.R. В main.R содержится предоставленный код ввода/вывода, и его нельзя редактировать.

Попробуйте сами

source("Account.R")

# Предоставленный код ввода/вывода: оставьте его как есть
input <- suppressWarnings(readLines(file("stdin")))
acc <- make_account(input[1])
for (line in input[-1]) {
  p <- strsplit(line, " ")[[1]]
  amount <- as.numeric(p[2])
  msg <- tryCatch({
    if (p[1] == "dep") acc$deposit(amount) else acc$withdraw(amount)
    "ok"
  }, error = function(e) paste("error:", conditionMessage(e)))
  cat(line, ": ", msg, "\n", sep = "")
}
h <- acc$history()
h[1] <- "tampered"
cat("balance: ", acc$balance(), "\n", sep = "")
cat("history: ", paste(acc$history(), collapse = ", "), "\n", sep = "")
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор R