Създаване на функция на Rust, която връща String или - str, SavePearlHarbor

Още едно копие на пристанището

Създаване на Rust функция, която връща низ или &str

От преводача

Научихме как да създадем функция, която приема String или &str (на английски) като аргумент. Сега искам да ви покажа как да създадете функция, която връща String или &str. Също така искам да обсъдя защо може да имаме нужда от това. Първо, нека напишем функция, която премахва всички интервали от даден низ. Нашата функция може да изглежда така:

Тази функция разпределя памет за буфера на низа, преминава през всички знаци във входния низ и добавя всички знаци, които не са интервали, към буфера buf. Сега въпросът е: какво ще стане, ако във входа няма нито един интервал? Тогава стойността на input ще бъде точно същата като buf. В такъв случай би било по-ефективно изобщо да не създавате buf. Вместо това бихме искали просто да върнем дадения вход обратно на потребителя на функцията. Типът вход е &str, но нашата функция връща String. Можем да променим типа вход на String:

Но тук има два проблема. Първо, ако входът стане String, потребителят на функцията ще трябва дапреместисобствеността върху входа към нашата функция, така че да не може да работи със същите данни в бъдеще. Трябва да поемем собствеността върху входа само ако наистина имаме нужда от него. Второ, входът може вече да е &str, в който случай ние принуждаваме потребителя да преобразува низа в String, осуетявайки опита ни да избегнем разпределението на памет за buf.

Клониране при запис

Нашата функция проверява дали оригиналният входен аргумент съдържа поне едно място, преди да разпредели памет за новия буфер. Ако във входа няма интервали, той просто се връща такъв, какъвто е. Добавяме известна сложност по време наизпълнение за оптимизиране на използването на паметта. Имайте предвид, че нашият тип крава има същия живот като &str. Както казахме по-рано, компилаторът трябва да следи използването на препратката &str, така че да знае кога е безопасно да освободи паметта (или да извика метода на деструктора, ако типът имплементира Drop).

Красотата на Cow е, че прилага характеристиката Deref, така че можете да извиквате немодифициращи методи върху него, без дори да знаете дали е разпределен нов буфер за резултата. Например:

Ако трябва да променя s, мога да го конвертирам в променливаowningс помощта на метода into_owned(). Ако кравата съдържа заети данни (избрано е заето), ще се извърши разпределение на паметта. Този подход ни позволява да клонираме (т.е. да разпределяме памет) лениво само когато наистина трябва да напишем (или променим) в променлива.

Пример с променлив Cow::Borrowed:

Пример с променлив Cow::Owned:

Идеята на кравата е следната:

  • Отложете разпределението на паметта възможно най-дълго. В най-добрия случай никога няма да разпределим нова памет.
  • Нека потребителят на нашата функция remove_spaces не трябва да се притеснява за разпределянето на памет. Използването на Cow ще бъде същото и в двата случая (независимо дали е разпределена нова памет или не).

Използване на чертата Into

По-рано говорихме за използването на чертата Into за преобразуване на &str в String. По същия начин можем да го използваме, за да преобразуваме &str или String в желания вариант на крава. Извикването на .into() ще принуди компилатора автоматично да избере правилната опция за преобразуване. Използването на .into() изобщо няма да забави нашия код, това е просто начин да се отървете от изричната опция Cow::Owned или Cow::Borrowed.

И накрая, можем да опростим нашия пример, използвайки малко итератори:

Реално използване на Крава

Моят пример за премахване на интервали изглежда малко пресилен, но в реалния код тази стратегия също влиза в действие. Rust core има функция, която преобразува байтове в UTF-8 низ, пропускайки невалидни комбинации от байтове, и функция, която преобразува краища на редове от CRLF в LF. И за двете функции има случай, в който можете да върнете &str в оптималния случай, и по-малко оптимален случай, който изисква разпределяне на памет за String. Други примери, които ми идват на ум, са кодиране на низ във валиден XML/HTML или правилно екраниране на специални знаци в SQL заявка. В много случаи входът вече е правилно кодиран или екраниран, в който случай е по-добре просто да върнете входния низ обратно такъв, какъвто е. Ако данните трябва да бъдат променени, тогава ще трябва да заделим памет за низовия буфер и да го върнем вече.

Защо да използвате String::with_capacity()?

Докато говорим за ефективно управление на паметта, имайте предвид, че използвах String::with_capacity() вместо String::new() при създаването на буфера за низове. Можете също да използвате String::new() вместо String::with_capacity(), но е много по-ефективно да разпределите цялата необходима памет за буфера наведнъж, вместо да я разпределяте отново, докато добавяме нови знаци към буфера.

Низът всъщност е Vec вектор от UTF-8 кодови точки. Когато се извика String::new(), Rust създава вектор с нулева дължина. Когато поставим символа a в буфера на низа, например с input.push('a'), Rust трябва да увеличи капацитета на вектора. За да направи това, той ще разпредели 2 байта памет. При по-нататъшно поставяне на символи в буфера, когато превишаваме разпределения обемпамет, Rust удвоява размера на низа чрез преразпределяне на паметта. Той ще продължи да увеличава капацитета на вектора всеки път, когато бъде превишен. Последователността на разпределения капацитет е: 0, 2, 4, 8, 16, 32, …, 2^n, където n е броят пъти, в които Rust е открил, че разпределената памет е надвишена. Преразпределението е много бавно (корекция: kmc_v3 обясни, че може да не е толкова бавно, колкото си мислех). Rust не само трябва да поиска от ядрото да разпредели нова памет, но също така трябва да копира съдържанието на вектора от старата област на паметта в новата. Разгледайте изходния код за Vec::push, за да видите сами логиката зад преоразмеряването на вектора.

Преоразмеряването на std::vector в C++ може да бъде много бавно поради необходимостта да се извикват конструктори за преместване поотделно за всеки елемент и те могат да хвърлят изключение.