Разработка / ROBLOX
table.clone в Luau: почему изменение копии затрагивает исходный инвентарь
Сравните присваивание, поверхностную копию и отдельные копии записей на маленьком вымышленном инвентаре. Проверенный пример покажет, как менять черновик, сохраняя исходное количество, и где заканчиваются возможности двухуровневой модели.
Задайте ожидаемое поведение черновика #
Представьте учебный инвентарь с двумя материалами: copper содержит count=3, wood содержит count=1. Нужно открыть черновик для примерки изменения и поставить в нём количество меди 9, сохранив в исходных данных 3. Здесь важны два одновременно проверяемых значения, а не только надпись в одной панели. Сначала запишите ожидаемую пару: original=3, draft=9. Затем выбирайте способ копирования, который должен обеспечить именно это разделение.
Материалы, поля label и equipped придуманы для упражнения. Это обычные таблицы в памяти, а не настоящий инвентарь игрока Roblox. Мы проверили автономную логику в Luau, но не подключали GUI, сервер, RemoteEvent или сохранения. Такая граница позволяет честно подтвердить поведение примера, не выдавая его за готовую систему предметов. Она также объясняет, почему успешная проверка копии не доказывает выдачу ресурсов в опубликованной игре.
Отличите новое имя от новой таблицы #
Если написать draft=original, появляется второе имя для той же таблицы. В официальном руководстве таблицы описываются как значения, на которые переменные ссылаются; простое присваивание не создаёт независимый снимок. Изменение draft.copper.count в таком случае доступно и через original.copper.count. Проблема не в отрисовке текста: обе переменные ведут к одним данным, поэтому ожидаемое разделение отсутствует ещё до интерфейса.
Для первой проверки используйте отдельный свежий набор данных и установите через такое второе имя количество 7. В нашем автономном тесте сравнение alias==source истинно, а source.copper.count становится 7. Это проверенный пример общего объекта. Не называйте его копией только потому, что имя переменной другое. Перед следующей проверкой создайте новый набор с исходным количеством 3, чтобы результат предыдущего изменения не испортил сравнение.
Поймите границу table.clone #
table.clone создаёт новую внешнюю таблицу, но копирует её содержимое поверхностно. Если значением элемента была другая таблица, ссылка на неё остаётся общей. Поэтому draft~=original может быть истинным одновременно с draft.copper==original.copper. Это не противоречие: внешние контейнеры различны, а одна вложенная запись у них общая. Для редактора количества нужно проверять именно ту глубину, на которой происходит изменение.
Во второй автономной проверке меняем вложенное количество через поверхностную копию на 8: исходное тоже становится 8. При этом удаление wood из внешней копии не удаляет wood из исходного контейнера. Два действия дают разный результат, потому что затрагивают разные уровни. Не объединяйте их в утверждение «копирование работает» или «не работает». Укажите конкретное поле и уровень, который проверили, сохраняя оба наблюдения.
| Вариант | Общая запись copper? |
|---|---|
| Присваивание | Да |
| table.clone | Да |
| Копии записей | Нет |
Сравнивайте ссылки и значения отдельно #
Значение count отвечает на вопрос, какое количество записано. Сравнение таблиц через == отвечает на другой вопрос: общая ли это таблица в текущем примере. Одинаковые числа не доказывают независимость. Две разные записи могут обе содержать 3, пока вы ещё ничего не изменили. Поэтому сначала проверяйте структуру ссылок, затем действие и оба итоговых значения; иначе дефект обнаружится только после реального редактирования.
Составьте короткий журнал: внешние таблицы различны, запись copper различна, изменение черновика не меняет исходное значение. Для обратного направления добавьте изменение wood в исходных данных и сравните черновик. Проверка в двух направлениях полезна, если один объект обновляется во время работы с другим. Наш тест выполняет такие сравнения, но не измеряет одновременную сетевую работу и не доказывает отсутствие всех возможных конфликтов в игре.
Копируйте записи по явно выбранной форме данных #
Оригинальная функция copyInventoryDraft рассчитана на словарь с непустыми строковыми идентификаторами предметов. Каждое значение — простая таблица записи, а её именованные поля содержат только строки, числа или логические значения. Функция сначала копирует внешний контейнер, затем отдельно копирует каждую запись. Для этой формы количество, подпись и false в equipped сохраняются как значения, а таблицы записей уже не разделяются с оригиналом.
Контракт намеренно ограничен. Метатаблицы, вложенная metadata-таблица, функции и числовые ключи полей не входят в эту модель и приводят к явному отказу. Это не универсальная глубокая копия произвольных данных. Такое ограничение делает пример понятным и проверяемым: он не обещает обработку циклов или сохранение сложного графа ссылок. Если ваша структура другая, сначала опишите её и выберите правила копирования, а не просто уберите проверки ради успешного запуска.
-- Teaching model only: string item IDs -> plain records -> scalar fields.
-- This is not a universal deep-copy function or a server validation layer.
local function copyInventoryDraft(original)
assert(type(original) == "table", "Expected an inventory table")
assert(getmetatable(original) == nil, "Root metatables are outside this model")
local draft = table.clone(original)
for id, record in pairs(original) do
assert(type(id) == "string" and id ~= "", "Expected a nonempty item ID")
assert(type(record) == "table", "Expected an item record")
assert(getmetatable(record) == nil, "Record metatables are outside this model")
for field, value in pairs(record) do
assert(type(field) == "string", "Expected named record fields")
local valueType = type(value)
assert(valueType == "string" or valueType == "number" or valueType == "boolean",
"Only scalar record fields are supported")
end
draft[id] = table.clone(record)
end
return draft
end
return copyInventoryDraftЗапустите небольшой пример использования #
Для автономного Luau сохраните функцию в table-copy-example.luau, а пример использования — в table-copy-usage.luau, в одной папке. Второй файл получает функцию через require с относительным путём, создаёт две учебные записи и меняет draft.copper.count на 9. Именно этот вариант запуска проверен. Если переносите идею в ModuleScript Roblox, отдельно адаптируйте способ получения модуля и окружение; файл с относительным путём не является готовой настройкой Studio.
Проверенный вывод примера: GUIDEBOOK_TABLE_COPY_USAGE original=3 draft=9. Assertions дополнительно сравнивают оба количества и сохраняют equipped=false. В статье показаны полный ограниченный помощник и его короткое применение. Они ничего не рисуют на экране и не выдают предметов. Если у вас вывод отличается, проверьте, какой файл запущен, откуда взята функция и не использовали ли перед ним уже изменённый исходный набор.
local copyInventoryDraft = require("./table-copy-example")
local original = {
copper = {count = 3, label = "Copper", equipped = false},
wood = {count = 1, label = "Wood", equipped = true},
}
local draft = copyInventoryDraft(original)
draft.copper.count = 9
assert(original.copper.count == 3)
assert(draft.copper.count == 9)
assert(draft.copper.equipped == false)
print("GUIDEBOOK_TABLE_COPY_USAGE original=" .. original.copper.count .. " draft=" .. draft.copper.count)Проверьте изменения обеих сторон #
После отдельного копирования записей редактирование количества меди в черновике оставляет исходное значение 3. Затем изменение количества wood в оригинале не меняет прежнее значение wood в черновике. Внешние вставка и удаление в черновике также не добавляют и не удаляют записи оригинала. Эти наблюдения относятся к проверенной форме данных. Они полезнее одного сообщения об успешном вызове функции, которое ничего не говорит о независимости редактирования.
Если два разных идентификатора исходного словаря ссылаются на одну запись, помощник делает отдельную копию для каждого идентификатора. Наш тест подтверждает, что эти новые записи различны и изменение одной не меняет другую. Значит, исходное разделяемое состояние не сохраняется. Это сознательная особенность данного помощника. Для проекта, где общая запись имеет смысл, потребуется другая политика; не называйте результат точной копией любого графа объектов.
Не принимайте freeze за глубокую независимость #
Официальная библиотека объясняет, что table.freeze изменяет состояние самой переданной таблицы на месте, а вложенные значения автоматически не замораживаются. Наш тест замораживает внешний инвентарь и проверяет, что вложенная copper остаётся изменяемой. Изменение её count возможно. Поэтому неизменяемый внешний контейнер не означает, что все содержащиеся в нём данные защищены от редактирования или уже отделены от других ссылок.
table.clone замороженной таблицы возвращает незамороженную внешнюю копию, но вложенная запись по-прежнему разделяется при поверхностном копировании. В тесте это проверяется отдельно, затем ограниченный помощник создаёт независимые записи. Не используйте одну отметку frozen как доказательство всех свойств результата. Сначала выберите, должны ли данные копироваться, сохранять общие ссылки или запрещать изменение. Это разные требования, и их нужно проверять различными действиями.
Сохраните границы автономной проверки #
Набор проверок охватывает присваивание, поверхностную копию, независимость двухуровневых записей, false, пустой словарь, общий исходный объект и явно неподдерживаемые формы. Отдельно проверено поведение замороженного внешнего контейнера. Не переносите вывод на любую рекурсивную структуру: помощник не реализует обход такого графа. Ожидаемый отказ для глубокой записи — часть его контракта, а не универсальная ошибка Luau или Roblox.
Копирование в памяти не подтверждает право игрока изменить настоящий инвентарь и не заменяет серверную проверку. Оно также не сохраняет прогресс между сеансами. Для интерфейса понадобятся собственные проверки обновления, отмены черновика и получения новых данных; для сохранений — отдельный протокол. В этом материале они не выполнены. Перед передачей кода другому чату перечислите эти границы вместе с проверенным выводом, чтобы помощник не выглядел готовой игровой системой.
| Случай | Вывод |
|---|---|
| Изменение черновика | Оригинал остаётся 3 |
| Изменение оригинала | Черновик не меняется |
| Более глубокий объект | Отказ по контракту |
| GUI и сохранения | Ещё не проверены |
Выберите копирование под конкретное действие #
Итоговый журнал должен показывать, какой уровень копируется, какие ссылки остаются общими и какие изменения действительно изолированы. Для нашего упражнения результат original=3, draft=9 соответствует выбранной задаче редактора. Но если нужен только новый список ссылок на существующие объекты, поверхностная копия может быть намеренным решением. Сначала сформулируйте действие и ожидаемый эффект, а не выбирайте самое сложное копирование для всех данных подряд.
Оригинальные записи, помощник и проверки доступны для изучения и адаптации. Сохраните вместе с ними форму данных и ограничения; при добавлении нового вложенного поля пересмотрите контракт и повторите соответствующие проверки. Не обещайте ускорение, экономию памяти или исправление реального инвентаря без измерений и интеграционного теста. Актуальные правила table.clone и table.freeze сверяйте с официальными источниками, а вывод конкретной реализации подтверждайте собственным запуском.
Первоисточники
Luau — Standard LibraryRoblox Creator Hub — Tables
