Створення функції на Rust, яка повертає String або - str, SavePearlHarbor
Ще одна копія хабора
Створення функції Rust, яка повертає String або &str
Від перекладача
Ми дізналися як створити функцію, яка приймає String або &strstr (англ.) як аргумент. Тепер я хочу показати вам, як створити функцію, яка повертає String або &str . Ще хочу обговорити, чому це може знадобитися. Для початку давайте напишемо функцію, яка видаляє всі прогалини із заданого рядка. Наша функція може виглядати приблизно так:
Ця функція виділяє пам'ять для рядкового буфера, проходить по всіх символах у рядку input і додає не пробільні символи в буфер buf . Тепер питання: що якщо на вході немає жодної прогалини? Тоді значення input буде таким самим, як і buf . У такому разі було б ефективніше взагалі не створювати buf. Натомість ми хотіли б просто повернути заданий input назад користувачу функції. Тип input - &str, але наша функція повертає String. Ми могли б змінити тип input на String :
Але тут виникають дві проблеми. По-перше, якщо input стане String , користувачеві функції доведетьсяпереміщатиправо володіння input в нашу функцію, так що він не зможе працювати з тими самими даними в майбутньому. Нам слід брати володіння input тільки якщо воно нам дійсно потрібне. По-друге, на вході вже може бути &str, і тоді ми змушуємо користувача перетворювати рядок в String, зводячи нанівець нашу спробу уникнути виділення пам'яті для buf.
Клонування під час запису
Наша функція перевіряє, чи містить вихідний аргумент input хоча б один пропуск, і тільки потім виділяє пам'ять під новий буфер. Якщо в пробілах немає, то він просто повертається як є. Ми додаємо трохи складності під часвиконання, щоб оптимізувати роботу з пам'яттю. Зверніть увагу, що наш тип Cow той самий час життя, що й у &str . Як ми вже говорили раніше, компілятор потрібно відстежувати використання посилання &str , щоб знати, коли можна безпечно звільнити пам'ять (або викликати метод-деструктор, якщо тип реалізує Drop ).
Краса Cow в тому, що він реалізує типаж Deref , так що ви можете викликати для нього методи, що не змінюють, навіть не знаючи, чи виділено для результату новий буфер. Наприклад:
Якщо мені потрібно змінити s, то я можу перетворити її на змінну, що володіє , за допомогою методу into_owned(). Якщо Cow містить запозичені дані (вибраний варіант Borrowed), то відбудеться виділення пам'яті. Такий підхід дозволяє нам клонувати (тобто виділяти пам'ять) ліниво, тільки коли нам дійсно потрібно записати (або змінити) змінну.
Приклад із змінним Cow::Borrowed :
Приклад із змінним Cow::Owned :
Ідея Cow у наступному:
- Відкласти виділення пам'яті якомога довгий термін. У найкращому разі ми ніколи не виділимо нову пам'ять.
- Дозволити користувачеві нашої функції remove_spaces не хвилюватися про виділення пам'яті. Використання Cow буде однаковим у будь-якому випадку (буде виділена нова пам'ять, чи ні).
Використання типажу Into
Раніше ми говорили про використання типажу Into (англ.) для перетворення &strstr на String . Так само ми можемо використовувати його для конвертації &str або String в потрібний варіант Cow. Виклик .into() змусить компілятор автоматично вибрати правильний варіант конвертації. Використання .into() анітрохи не сповільнить наш код, це просто спосіб позбутися від явної вказівки варіанта Cow::Owned або Cow::Borrowed .
Ну і насамкінець ми можемо трохи спростити наш приклад з використанням ітераторів:
Реальне використання Cow
Мій приклад із видаленням прогалин здається трохи надуманим, але в реальному коді така стратегія теж знаходить застосування. У ядрі Rust є функція, яка перетворює байти в UTF-8 рядок із втратою невалідних поєднань байт, і функція, яка переводить кінці рядків із CRLF на LF. Для обох цих функцій є випадок, при якому можна повернути amp в оптимальному випадку, і менш оптимальний випадок, що вимагає виділення пам'яті під String . Інші приклади, які мені спадають на думку: кодування рядка у валідний XML/HTML або коректне екранування спецсимволів у SQL запиті. У багатьох випадках вхідні дані вже правильно закодовані або екрановані, і тоді краще просто повернути вхідний рядок назад, як є. Якщо ж дані потрібно змінювати, нам доведеться виділити пам'ять для рядкового буфера і повернути його.
Навіщо використовувати String::with_capacity()?
Поки ми говоримо про ефективне управління пам'яттю, зверніть увагу, що я використовував String::with_capacity() замість String::new() під час створення рядкового буфера. Ви можете використовувати String::new() замість String::with_capacity() , але набагато ефективніше виділяти пам'ять для буфера відразу всю необхідну пам'ять, замість того, щоб перевиділяти її в міру того, як ми додаємо в буфер нові символи.
String — насправді вектор Vec із кодових позицій (code points) 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++ може бути дуже повільним через те, що потрібно викликати конструктори переміщення індивідуально для кожного елемента, а вони можуть викинути виняток.