В этой статье я хотел бы поговорить о том, почему вы могли бы предпочесть использование Arc<[T]> вместо Vec<T> в качестве варианта по умолчанию в вашем Rust-коде.

Используйте Arc<[T]> вместо Vec<T>

Arc<[T]> может быть очень хорошей заменой Vec<T> для иммутабельных данных. Так, если вы формируете большую последовательность данных, которые потом никогда не будете менять в последствии, вы можете подумать в сторону Arc<[T]>. Также он действительно хорош если вы планируете хранить большое множество таких объектов; или планируете просто массово перемещать или копировать их в процессе работы программы.

Я сейчас не говорю о Vec<T>, который вы сформировали в качестве локальной переменной внутри функции или который будет использоваться как что-то временное. Речь скорее о таких случаях, когда вы планируете хранить данные в течение долгого промежутка времени, особенно если данные реализуют Clone.

Это не должно быть большим открытием, так как Clone — что-то вроде супер-способности Arc, о которой мы сейчас вкратце поговорим. Если вы собираетесь постоянно копировать огромные последовательности иммутабельных данных, Arc может серьезно ускорить эту операцию в сравнении с Vec<T>. Вы даже можете пойти дальше и использовать Box<[T]>, но об этом мы поговорим в самом конце.

Примечание переводчика

Далее по тексту часто будет употребляться термин "копирование", но в этой статье под ним подразумевается исключительно операция Clone::clone() — не перепутайте с трейтом Copy!

Почему Arc<[T]>?

Итак, почему я рекомендую Arc<[T]>? Для этого есть три веских причины. Первую мы уже немного успели затронуть. У Arc дешевое копирование, выполняемое за константное время. То есть неважно, насколько у вас большие данные, на которые указывает Arc, их копирование всегда займет одинаковое время — время инкремента целочисленной переменной и копирования самого Arc-указателя. И это выполняется очень, очень быстро и не задействует никакого выделения памяти, которое обычно происходит при копировании Vec. Это весомая оптимизация, которую можно получить просто перейдя на Arc<[T]>.

Другая причина — Arc<[T]> занимает всего 16 байт; он хранит только указатель и размер данных, в отличие от Vec<T>, которому нужно хранить указатель, size и capacity, что в общей сложности составляет 24 байта. Разница всего в восемь байт — это немного — но если вы храните огромное количество таких контейнеров, особенно в структуре или в массиве, то в случае с Vec<T> это дополнительное место может суммироваться и ухудшить ваш cache locality, и проход по вашим данным будет чуточку труднее и медленнее.

Наконец, третья причина — Arc<[T]> реализует трейт Deref<[T]>, так же как это делает Vec<T>. То есть, вы можете совершать все те же read-only операции c Arc<[T]>, какие вы делали с Vec<T>. Вы можете взять его длину, проитерироваться или обратиться по индексу. Это важно, поскольку первые два довода хороши, но я бы, пожалуй, не стал из-за них рекомендовать переход на Arc<[T]>, если бы его было куда труднее использовать, нежели Vec<T>, но это не так благодаря Deref<[T]>.

Arc так же реализует большое количество других трейтов, которые могли бы вам понадобиться, и все это делает Arc<[T]> взаимозаменяемым с Vec<T> в большинстве обстоятельств.

Таким образом, первые два преимущества — скорее про некоторый прирост производительности; третье же про то, что использовать Arc<[T]> не труднее, чем Vec<T>, так что вам будет достаточно просто сделать быстрый рефакторинг и перейти на Arc<[T]>.

Arc<str> vs String

Итак, мы поговорили про Arc<[T]> в сравнении с Vec<T>. Оставшуюся же часть статьи я буду говорить про Arc<str> и его преимущество в сравнении со String, ведь эта пара разделяет схожую дихотомию. Но на ее примере немного проще показать суть и использовать более жизненные примеры. А в целом, все, что я скажу про Arc<str> vs String будет относиться и непосредственно к Arc<[T]> vs Vec<T>.

Также заслуживает упоминания, что когда вы планируете использовать Arc, сперва попробуйте использовать Rc. Например, если вам не нужна потокобезопасность, которую вам дает Arc. Повествование отдает все лавры Arc только потому, что это более общая версия подхода, но если вам подойдет и Rc, вы определенно должны использовать именно его, так как он имеет меньший оверхед в сравнении с Arc.

Стоит прояснить еще кое-что. Я хочу уточнить, что говорю об Arc<str> — не об Arc<String>. Arc<String> на самом деле имеет некоторые преимущества над Arc<str>, но с другой стороны имеет и очень существенные недостаток: вам нужна косвенность в целых два указателя чтобы добраться до данных, которые вам нужны — до символьных данных строки. Я покажу визуализацию этой ситуации в конце, но Arc<String> в общем целом — это просто громоздко и неэффективно по памяти, а вышеупомянутая двойная косвенность серьезно ударяет по производительности, поэтому я не рекомендую использовать Arc<String>.

Поэтому мы будем говорить конкретно про Arc<str>. Сила "толстых" указателей (wide pointers) в Rust заключается в том, что Arc имеет возможность указывать непосредственно на динамически выделенный на куче текстовый буфер.

MonsterId

Представим, что я работаю над игрой, и у меня есть тип struct MonsterId(String). Под капотом он представлен как обычный текст, и я, как мы видим, использую String, чтобы хранить его.

Примечание переводчика

У меня сложилось стойкое впечатление, что автор оригинального видео скорее имел в виду MonsterType, нежели MonsterId, поскольку большинство дальнейших примеров и использований MonsterId намекают на то, что подразумевается не конкретный созданный монстр, а скорее его разновидность.

Тем не менее, продолжим использовать MonsterId в дальнейшем повествовании, чтобы отдать дань оригиналу, при этом держа в уме, что если у вас немного не складывается картина, то вы можете мысленно заменить MonsterId на MonsterType, и, возможно, вы почувствуете облегчение.

Первое, что я мог бы захотеть реализовать для моего MonsterId, это пачку типичных трейтов:

#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, /* ... */)]
struct MonsterId(String);

Как видим, я хочу иметь возможность копировать наш id, выводить на экран его дебажное представление, сравнивать его, хэшировать. Также я хочу использовать serde для сериализации и десериализации, возможно из конфига или из каких-то save-данных. В общем, я хочу отнаследовать все эти трейты для моего MonsterId, и чтобы это все работало со String.

Далее, я хочу метод для MonsterId, который просто выдает внутреннее текстовое представление в качестве &str. Возможно, я хочу логгировать его в консоль, возможно, я хочу использовать его для аналитики в каком-то другом месте:

fn as_str(&self) -> &str {
	&self.0
}

То есть я просто хочу достучаться до внутреннего представления данных в виде &str, и это достаточно легко сделать, имея String.

Далее, мне понадобится конфиг с данными в виде HashMap с MonsterId в качестве ключа и некоторыми статами в качестве данных — ну, вы знаете: урон монстра, его здоровье и прочее подобное:

enemy_data: HashMap<MonsterId, EnemyStats>

Ключ — MonsterId, именно поэтому мне нужна была реализация Eq и Hash, и все это отлично работает со String в качестве внутреннего представления MondterId.

Следующее, что нам могло бы понадобиться — хранить в списке идентификаторы всех врагов, которые когда-либо спавнились за одну игровую сессию:

enemies_spawned: Vec<MonsterId>

Заметьте, что я здесь использую Vec<T>, поскольку предполагается, что монстры будут добавляться сюда в процессе игры, и мне, вероятно, придется копировать сюда MonsterId. Список может быть ощутимо бóльшим.

Далее, нужна какая-то функциональность для создания реального монстра по MonsterId:

fn create(id: MonsterId) -> EnemyInstance {
	...
}

Вероятно, я собираюсь копировать MonsterId в эту функцию, возможно, копировать MonsterId внутрь инстанса монстра, чтобы каждый монстр знал, кто он такой.

И, наконец, скажем, я буду хранить какую-то статистику внутри BTreeMap — например, сколько раз определенный MonsterId был убит в процессе игровой сессии.

total_destroyed: BTreeMap<MonsterId, u64>

Я использую здесь BTreeMap просто, чтобы он отличался от уже используемого нами HashMap, и у нас есть возможность использовать MonsterId в качестве ключа в BTreeMap потому что он реализует Ord.

В общем, тут достаточное количество разнообразных кейсов; ну и конечно их может быть куда больше. Это просто пример того, как такой базовый тип как MonsterId может начать пронизывать всю вашу систему, и вам придется повсюду его копировать в процессе работы программы. В какой-то момент вам может понадобиться использовать все эти вышеперечисленные структуры разом, и даже больше, и в какой-то момент все эти операции по копированию MonsterId начнут сказываться на производительности, и все это станет расти, как снежный ком.

String

Итак, давайте посмотрим на стоимость копирования MonsterId и его общее потребление памяти при использовании String в качестве внутреннего представления. Вот как String представлен в памяти:

Реальные текстовые данные в String представлены как последовательность символов на куче, и String выделяет достаточное количество памяти под ваш текст, что очевидно, но так же он выделяет еще немного места, чтобы строка могла расти до какого-то предела без повторного выделения памяти.

У нас есть строка "Goblin"

с дополнительными незанятыми четырьмя байтами памяти для случая, если String станет длиннее, хотя, конечно, в конкретном случае мы знаем, что этого не произойдет, поскольку Goblin — это окончательное имя монстра.

Далее, сам объект String состоит из трех восьми-байтовых полей:

  • указателя на данные ptr

  • длины данных len

  • вместимости cap

ptr указывает непосредственно на выделенную для нашей строки память, len хранит размер текста "g-o-b-l-i-n", то есть равен шести, а cap включает в себя размер всей выделенной для объекта памяти и в нашем случае равен десяти.

Теперь давайте посмотрим, как выглядит копирование String. Сначала нам нужно целиком продублировать массив символов. Для этого будет выделен новый участок памяти на куче, а затем все символы будут скопированы в него, что займет линейное время выполнения — другими словами, это займет тем больше времени, чем длиннее ваша строка. Потом будет создан новый объект String на стеке, и он будет ссылаться на новый массив символов на куче.

Заметьте, что в этот раз резервное место для строки было отброшено, поскольку дубликат строки скорее всего не будет расти, и у нас получается полностью заполненный буфер с данными. Поэтому в данном случае мы получаем cap, равный шести — и он избыточен, поскольку теперь он идентичен с len.

Если мы хотим сделать еще одну копию, нам придется повторить все то же самое: выделить память под новый буфер, скопировать в него все символы строки, а затем создать на стеке объект String, который будет ссылаться на него. И у нас снова будет избыточное, не нужное нам поле cap.

Полагаю, вы понимаете, что процесс выделения памяти — достаточно дорогая операция, а объекты String занимают больше места, чем хотелось бы для нашей простой задачи по хранению гоблинов.

Arc<str>

Теперь, давайте посмотрим на этот же случай, но с использованием Arc<str> вместо String. С Arc<str> данные на куче выглядят немного иначе, чем в случае со String:

Итак, у нас присутствуют два восьми-байтных поля: одно для подсчета сильных ссылок, другое — для подсчета слабых, затем идут реальные текстовые данные. На данном этапе это выглядит длинно и странновато, но не волнуйтесь, станет лучше.

Наш объект Arc на стеке состоит всего лишь из указателя и длины данных:

Это всего 16 байт, поскольку отсутствует дополнительное поле capacity, которое было у String. Давайте посмотрим, что произойдет при копировании:

Все, что нам пришлось сделать — это скопировать структуру на стеке и инкрементировать счетчик ссылок, который теперь равен двум. Заметьте, мы не произвели никакого выделения новой памяти; мы не делали глубокого копирования текстовых данных, как в String. Мы просто ссылаемся на все те же текстовые данные в двух разных местах.

То есть мы теперь можем очень и очень дешево делать копии:

И тот факт, что текстовые данные делятся между разными Arc-объектами также увеличивает шансы, что они будут в кэше, когда мы их запросим, потому что они загружаются в кэш каждый раз, когда я использую любой из четырех Arc-объектов с изображения выше. В отличие от String, где память, на которую мы указываем, была бы загружена в кэш только если мы недавно читали из этого конкретного объекта String.

Видно, что этот подход в целом легковеснее. Arc-указатели сами по себе меньше, то есть в идентичное количество памяти может уместиться большее их количество, и, честно говоря, я считаю, что это просто более грамотное использование ресурсов для иммутабельных строк, которые копируются по всему нашему коду.

Также, эти два дополнительных поля — strong и weak — за которые мы платим на куче, изолированы от каждого отдельного имеющегося у нас Arc-объекта, поэтому их присутствие как бы амортизируется, тогда как в случае со String мы вынуждены платить за каждое такое поле, потому что оно лежит на стеке. Это два дополнительных поля на куче — а два, конечно, больше, чем один — но они все-таки делятся между каждым инстансом Arc-объекта, ссылающегося на нашу строку. Так что в конечном счете это намного меньший расход памяти в сравнении со String.

Итак, давайте посмотрим, как выглядит и ощущается переход со String на Arc<str> для MonsterId:

#[derive(Clone, Debug, PartialEq, Eq, Hash, Ord, PartialOrd, Serialize, Deserialize, /* ... */)]
struct MonsterId(Arc<str>);

Во-первых, все наши имплементированные трейты работают так же, как и работали ранее. Вы можете копировать Arc<str>, выводить дебажный принт Arc<str>, сравнивать его, хешировать. Вы также можете сериализовать и десериализовать его. Все это работает точно так же, как работало раньше.

Что по поводу доступа непосредственно к текстовым данным? Метод as_str() не придется менять, потому что Arc<str> реализует Deref<str> так же, как и String. Мы можем просто взять ссылку на него и она автоматически превратится в &str. То есть, ничего в этой функции тоже не нужно менять — она работает идеально.

Далее, что с нашим словарем HashMap<MonsterId, EnemyStats>? Ну, наш объект MonsterId все еще реализует Eq и Hash, потому что Arc<str> тоже реализует их, поэтому и здесь ничего не надо менять.

Фактически, я бы поспорил, что из-за все этого в данном случае использование Arc<str> более оправдано, чем использование String, поскольку String дает вам обширное API для модификации внутреннего текстового представления, но вы не можете модифицировать ключ HashMap, потому что вы можете сломать инвариантность данных. Представление вашего ключа как иммутабельного типа вроде Arc<str>, является более подходящим в данной ситуации, нежели String.

Двигаемся дальше. Мой Vec<MonsterId> станет теперь более cache-friendly, потому что я убрал целое лишнее поле для каждого MonsterId, а это две трети от старого размера MonsterId. Моя функция создания инстанса врага по MonsterId, вероятно, теперь более эффективна, поскольку, я так полагаю, она вовлекает в той или иной степени копирование, которое теперь делается эффективнее с Arc.

И, наконец, наша BTreeMap — с ней по сути все тоже самое, что и с HashMap; использование иммутабельных данных, таких, как наш Arc<str>, просто более подходяще для нашего ключа, поскольку ключи BTreeMap в любом случае нельзя модифицировать. BTreeMap теперь будет эффективнее, поскольку хранит меньшее количество данных и не должна платить за риски быть мутабельной тогда, когда мутабельность не имеет смысла. Таким образом, Arc<str> побеждает во всех случаях. Он эффективнее, и на него легко перейти.

Vec<T> and String

Почему же не использовать Vec<T> или String? Здесь стоит оговориться — я уже намекал, но сейчас скажу явно, что Vec<T> и String — для модификации. Для добавления и удаления, расширения и укорачивания данных. И если вам это не требуется, не используйте их, потому что они имеют дополнительную плату за то, что дают:

fn push(&mut self, ch: char)
fn extend<I>(&mut self, iter: I)
fn pop(&mut self) -> Option<char>
fn retain<F>(&mut self, f: F)
fn truncate(&mut self, new_len: usize)
fn reserve(&mut self, additional: usize)
fn resize(&mut self, new_len: usize, value: T)
fn drain<R>(&mut self, range: R) -> Drain<'_, T, A>
fn clear(&mut self)
fn shrink_to_fit(&mut self)

Все эти методы Vec<T> и String интересны тем, что все они принимают &mut self, то есть модифицируют свои данные. Если вам не нужны эти фичи, не используйте Vec<T> и String.

Если вам просто нужно посмотреть на какие-то данные, вычислить их размер, узнать, не пуст ли буфер с данными, взять данные по индексу, проитерироваться по нему, разделить его, произвести в нем поиск — все эти вещи предоставляются непосредственно str и [T], оба из которых вы можете легко взять через Arc:

fn len(&self) -> usize
fn is_empty(&self) -> bool
fn get(&self, index: usize) -> Option<&T>
fn iter(&self) -> Iter<'_, T>
fn split_at(&self, mid: usize) -> (&[T], &[T])
fn strip_prefix<P>(&self, prefix: P) -> Option<&[T]>
fn contains(&self, x: &T) -> bool
fn binary_search(&self, x: &T) -> Result<usize, usize>
fn to_vec(&self) -> Vec<T>
fn repeat(&self, n: usize) -> Vec<T>
impl<T, I> Index<I> for [T]
impl<T> ToOwned for [T]
impl<T> Eq for [T]

Итак, вам не нужна вся мощь String и Vec<T>, если вам нужны только read-only операции, и вы можете выиграть в производительности, просто не платя за String и Vec<T>. Именно поэтому вы могли бы рассмотреть использование Arc<[T]> вместо Vec<T> или Arc<str> вместо String.

Arc<String>

Теперь, я хочу показать вам Arc<String>, поскольку я упоминал, что он плох, и вот почему. Arc<String> начинается точно так же, как и String, с буфера с текстом и некоторого дополнительного резервного места:

но потом мы так же должны положить саму String внутрь Arc:

Итак, вы видите, что в начале у нас есть два поля для подсчета ссылок, потом идут данные объекта String. Объект Arc будет указывать на эти данные, а копирование будет копировать ссылку на String. Но если нам захочется достучаться до текстовых данных "Goblin," нам придется прыгнуть на String, а затем прыгнуть со String на "Goblin", и все эти манипуляции просто громоздки и неудобны, поэтому Arc<String> — это плохая идея, когда мы можем просто использовать Arc<str>.

Box<str>

Наконец, я упоминал, что если вам не нужно копирование, вы можете пойти еще дальше и использовать Box<str> вместо Arc<str>, и это, в сущности, будет настолько эффективно, насколько это вообще возможно, если исходить из предположения, что вам не нужно копирование объекта.

Тут нет лишней зарезервированной памяти, Box просто выделяет необходимые данные в куче, ссылается на них и знает, какого они размера. Вероятно, нельзя сделать еще лучше, чем этот вариант, однако, когда вы будете копировать такой объект, вы будете делать deep clone всех данных на куче.

Но если ваши данные не поддерживают Clone, то это лучший вариант в плане эффективности по занимаемой памяти. Так что рассмотрите Box<str>, если это ваш случай.

Послесловие от переводчика

В тот момент, когда я решил переводить данное видео от Logan Smith, алгоритмы YouTube подкинули мне реакцию ThePrimeagen на это самое видео. Эта реакция оказалась примечательна тем, что ThePrimeagen задался справедливым и, возможно, всплывшим у некоторых читателей вопросом: "А как вообще создать или инициализировать Arc<str>?".

И действительно, мы привыкли к тому, что имеем дело с типом &str — не str. Что такое в сущности strи как его создать, как с ним работать? В документации про тип str написано следующее:

The str type, also called a ‘string slice’, is the most primitive string type. It is usually seen in its borrowed form, &str. It is also the type of string literals, &'static str.

Окей, str — это "срез строки", и в чистом виде его достаточно трудно уловить, поскольку при объявлении голой не-owned строки она будет иметь тип &str, не str:

let hello_world = "Hello, World!"; // тип &str

Это сбило с толку и ThePrimeagen в том числе, и он ошибочно предположил, что Arc<str> можно создать только из строк, известных на этапе компиляции, например:

let s: &'static str = "Hello, World!";
let arc_str: Arc<str> = Arc::from(s);

Но к его удивлению, точно таким же образом сработал и код формирующий Arc<str> из String:

let s = String::from("Hello, World!");
let arc_str: Arc<str> = Arc::from(s);

Все потому, что для Arc реализован ряд трейтов, который под капотом делает всю магию за нас. В частности, это трейты:

Их имплементация позволяет удобно создавать Arc со всеми типами данных, упомянутыми в статье:

// from String
let unique: String = "eggplant".to_owned();
let shared: Arc<str> = Arc::from(unique);
assert_eq!("eggplant", &shared[..]);
// from &str
let shared: Arc<str> = Arc::from("eggplant");
assert_eq!("eggplant", &shared[..]);
// from Vec<i32>
let unique: Vec<i32> = vec![1, 2, 3];
let shared: Arc<[i32]> = Arc::from(unique);
assert_eq!(&[1, 2, 3], &shared[..]);

Комментарии (16)


  1. OldFisher
    18.03.2024 19:14
    +3

    Несмотря на вдохновенное выступление, автору так и не удалось убедить меня в необходимости массового копирования иммутабельных данных. Использовать строки в качестве идентификаторов, копируя их по значению? Хорошо ли это, правильно ли? Зря что ли придуманы хэши и атомы?


    1. snuk182
      18.03.2024 19:14
      +2

      Может автор пришел из джавы, забрав все привычки с собой...


    1. domix32
      18.03.2024 19:14
      +2

      Автор забыл сказать в каких кейсах это имело бы смысл. А это в основном какие-нибудь парсинги в параллель. Какой-нибудь HTML парсить или чанки JSON или может у вас там LSP какой крутится.


  1. ItzShiney
    18.03.2024 19:14

    Атомарный Rc в однопоточном коде, отличный совет, а главное нулевой оверхед. Если автор не понимает даже этого и вообще никак не упоминает обычный Rc, такая огромная статья от него не заслуживает прочтения

    В целом совет хороший, но очевидный: если никак не модифицируете массив, то не делайте его расширяемым, чтоб не тратить ресурсы на capacity. Но так Rc тут ничем не особен: сравнение можно было проводить и с Box, а другие контейнеры упомянуть в одном пункте


    1. AskePit Автор
      18.03.2024 19:14
      +3

      Но ведь автор упоминает


      1. ItzShiney
        18.03.2024 19:14

        Подскажите, пожалуйста, в каком месте/пункте?


        1. amishaa
          18.03.2024 19:14
          +1

          В самом начале, в разделе "Arc<str> vs String" целый абзац посвящен как раз Rc.


          1. ItzShiney
            18.03.2024 19:14

            А, хорошо, спасибо, был неправ. Тогда моя претензия в том, что автор решил указать везде менее оптимальный, но работающий в большем числе случаев способ, хотя говорит о системном языке. Казалось бы, философия всего этого направления в том, чтобы так не делать: выжимать из кода всю производительность и понимать устройство того, чем работаешь. А об оптимальном варианте автор лишь оговорился в одном абзаце, который невозможно заметить при беглом просмотре, в пункте, который вообще не про это. Но абзац хороший, да, просто находится не там, где хотелось бы (а именно в самом начале)


            1. boldape
              18.03.2024 19:14
              +1

              Последний раз когда я писал одно поточную программу был примерно 15 лет назад это не значит, что их больше не пишут совсем, но это вполне значит, что на расте это совсем не дефолтный вариант. Дефолт как раз таки многопоток, а однопоток - экзотика.


  1. candyboah
    18.03.2024 19:14

    Следующая статья будет:

    ✅Option<&T>❎&Option<T>

    Также от Логана Смита


    1. AskePit Автор
      18.03.2024 19:14

      Это видео я тоже смотрел, но решил, что переведу только про Arc. Так что если у вас есть желание перевести видео про &Option<T>, то я не стою на вашем пути)


  1. DarkTranquillity
    18.03.2024 19:14

    А в сложном и небезопасном С++ ребята долго запрягали, и таки ввели move-семантику, чтобы не натягивать сову на глобус, и не использовать shared ptr как костыль в данном случае.


    1. AskePit Автор
      18.03.2024 19:14
      +1

      Но ведь в Rust move-семантика вшита в язык по дефолту. А статья вовсе не про то как замувить, а про то как дешево копировать. Ну и последняя глава как раз про Box<str> для тех, кто хочет мувить


  1. Dominux
    18.03.2024 19:14

    Далее по тексту часто будет употребляться термин "копирование", но в этой статье под ним подразумевается исключительно операция Clone::clone()

    В чем проблема использовать общепринятый термин "клонировать"?


    1. AskePit Автор
      18.03.2024 19:14

      Если честно, я было начал с "клонировать", но быстро стушевался, потому что когда увидел на русском много слов "клонировать, клонировать", мне показалось, что я сумасшедший ученый, клонирующий людей. Насчет общепринятости в русском языке тоже засомневался, "копировать" выглядело более приемлемым.

      В общем, если отвечать на вопрос "в чем проблема?", то скорее всего во мне)


      1. Dominux
        18.03.2024 19:14

        когда увидел на русском много слов "клонировать, клонировать", мне показалось, что я сумасшедший ученый, клонирующий людей

        В принципе, термин "клонировать" подходит идеально, ведь по сути он означает, что вы создаёте клон из другой материи, но с идентичными свойствами и характеристиками. Копирование, в данном случае, не очень подходит, т.к. это иной процесс.

        Да и клонировать можно не только людей, в основном клонируют не их) Также и в расте, т.к. он - объектный ЯП, то логично, что там можно клонировать объекты какого-то типа. И, если уж вам захочется, то можно и создать тип "Person" (или "Human", раз аналогия биологическая) и клонировать их, уж это не запрещено всеми странами мира)))