Главная страница
Навигация по странице:

  • Листинг 8-15: Добавление среза строки к String используя метод

  • Листинг 8-16: Использование фрагмента строки после его добавления в состав другого

  • Листинг 8-17: Добавление одного символа в String значение используя

  • Объединение строк с помощью оператора + или макроса

  • Листинг 8-18: Использование оператора + для объединения двух значений String в новое Stringзначение

  • Индексирование в строках

  • Листинг 8-19: Попытка использовать синтаксис индекса со строкой

  • Внутреннее представление

  • Байты, скалярные значения и кластеры графем! Боже мой!

  • Методы для перебора строк

  • Хранение ключей со связанными значениями в HashMap

  • Создание новой хеш-карты

  • Листинг 8-20: Создание новой хеш-карты и вставка в неё пары ключей и значений

  • Доступ к данным в HashMap

  • Язык программирования Rust


    Скачать 7.02 Mb.
    НазваниеЯзык программирования Rust
    Дата12.04.2023
    Размер7.02 Mb.
    Формат файлаpdf
    Имя файлаThe Rust Programming Language_ru.pdf
    ТипУчебник
    #1056301
    страница18 из 62
    1   ...   14   15   16   17   18   19   20   21   ...   62
    Присоединение к строке с помощью push_str и push
    Мы можем нарастить
    String используя метод push_str который добавит в исходное значение новый строковый срез, как показано в листинге 8-15.
    Листинг 8-15: Добавление среза строки к
    String
    используя метод
    push_str
    После этих двух строк кода s
    будет содержать foobar
    . Метод push_str принимает строковый срез, потому что мы не всегда хотим владеть входным параметром.
    Например, код в листинге 8-16 показывает вариант, когда будет не желательно поведение, при котором мы не сможем использовать s2
    после его добавления к содержимому значения переменной s1
    Листинг 8-16: Использование фрагмента строки после его добавления в состав другого
    String
    Если метод push_str стал бы владельцем переменной s2
    , мы не смогли бы напечатать его значение в последней строке. Однако этот код работает так, как мы ожидали!
    Метод push принимает один символ в качестве параметра и добавляет его к
    String
    . В
    листинге 8-17 показан код, добавляющий букву “l” к
    String используя метод push
    Листинг 8-17: Добавление одного символа в
    String
    значение используя
    push
    В результате s
    будет содержать lol let mut s =
    String
    ::from(
    "foo"
    ); s.push_str(
    "bar"
    ); let mut s1 =
    String
    ::from(
    "foo"
    ); let s2 =
    "bar"
    ; s1.push_str(s2); println!
    (
    "s2 is {}"
    , s2); let mut s =
    String
    ::from(
    "lo"
    ); s.push(
    'l'
    );

    Объединение строк с помощью оператора + или макроса format!
    Часто хочется объединять две существующие строки. Один из возможных способов —
    это использование оператора
    +
    из листинга 8-18:
    Листинг 8-18: Использование оператора
    +
    для объединения двух значений
    String
    в новое
    String
    значение
    Строка s3
    будет содержать
    Hello, world!
    . Причина того, что s1
    после добавления больше недействительна и причина, по которой мы использовали ссылку на s2
    имеют отношение к сигнатуре вызываемого метода при использовании оператора
    +
    . Оператор
    +
    использует метод add
    , чья сигнатура выглядит примерно так:
    В стандартной библиотеке вы увидите метод add определённым с использованием обобщённых и связанных типов. Здесь мы видим сигнатуру с конкретными типами,
    заменяющими обобщённый, что происходит когда вызывается данный метод со значениями
    String
    . Мы обсудим обобщённые типы в Главе 10. Эта сигнатура даёт нам ключ для понимания особенностей оператора
    +
    Во-первых, перед s2
    мы видим
    &
    , что означает что мы складываем ссылку на вторую строку с первой строкой. Это происходит из-за параметра s
    в функции add
    : мы можем добавить только
    &str к
    String
    ; мы не можем сложить два значения
    String
    . Но подождите — тип
    &s2
    это
    &String
    , а не
    &str
    , как определён второй параметр в add
    Так почему код в листинге 8-18 компилируется?
    Причина, по которой мы можем использовать
    &s2
    в вызове add заключается в том, что компилятор может принудительно привести (coerce) аргумент типа
    &String к типу
    &str
    Когда мы вызываем метод add в Rust используется принудительное приведение (deref coercion), которое превращает
    &s2
    в
    &s2[..]
    . Мы подробно обсудим принудительное приведение в Главе 15. Так как add не забирает во владение параметр s
    , s2
    по прежнему будет действительной строкой
    String после применения операции.
    Во-вторых, как можно видеть в сигнатуре, add забирает во владение self
    , потому что self
    не имеет
    &
    . Это означает, что s1
    в листинге 8-18 будет перемещён в вызов add и
    больше не будет действителен после этого вызова. Не смотря на то, что код let s3 = s1
    + &s2;
    выглядит как будто он скопирует обе строки и создаёт новую, это выражение фактически забирает во владение переменную s1
    , присоединяет к ней копию содержимого s2
    , а затем возвращает владение результатом. Другими словами, это выглядит как будто код создаёт множество копий, но это не так; данная реализация более эффективна чем копирование.
    let s1 =
    String
    ::from(
    "Hello, "
    ); let s2 =
    String
    ::from(
    "world!"
    ); let s3 = s1 + &s2;
    // note s1 has been moved here and can no longer be used fn add
    (
    self
    , s: &
    str
    ) ->
    String
    {

    Если нужно объединить несколько строк, поведение оператора
    +
    становится громоздким:
    Здесь переменная s
    будет содержать tic-tac-toe
    . С множеством символов
    +
    и "
    становится трудно понять, что происходит. Для более сложного комбинирования строк можно использовать макрос format!
    :
    Этот код также устанавливает переменную s
    в значение tic-tac-toe
    . Макрос format!
    работает тем же способом что макрос println!
    , , но вместо вывода на экран возвращает тип
    String с содержимым. Версия кода с использованием format!
    значительно легче читается, а также код, сгенерированный макросом format!
    , использует ссылки, а значит не забирает во владение ни один из его параметров.
    Индексирование в строках
    Доступ к отдельным символам в строке, при помощи ссылки на них по индексу, является допустимой и распространённой операцией во многих других языках программирования. Тем не менее, если вы попытаетесь получить доступ к частям
    String
    , используя синтаксис индексации в Rust, то вы получите ошибку. Рассмотрим неверный код в листинге 8-19.
    Листинг 8-19: Попытка использовать синтаксис индекса со строкой
    Этот код приведёт к следующей ошибке:
    let s1 =
    String
    ::from(
    "tic"
    ); let s2 =
    String
    ::from(
    "tac"
    ); let s3 =
    String
    ::from(
    "toe"
    ); let s = s1 +
    "-"
    + &s2 +
    "-"
    + &s3; let s1 =
    String
    ::from(
    "tic"
    ); let s2 =
    String
    ::from(
    "tac"
    ); let s3 =
    String
    ::from(
    "toe"
    ); let s = format!
    (
    "{}-{}-{}"
    , s1, s2, s3); let s1 =
    String
    ::from(
    "hello"
    ); let h = s1[
    0
    ];

    Ошибка и примечание говорит, что в Rust строки не поддерживают индексацию. Но почему так? Чтобы ответить на этот вопрос, нужно обсудить то, как Rust хранит строки в памяти.
    Внутреннее представление
    Тип
    String является оболочкой над типом
    Vec
    . Давайте посмотрим на несколько закодированных корректным образом в UTF-8 строк из примера листинга 8-14. Начнём с этой:
    В этом случае len будет 4, что означает вектор, хранит строку "Hola" длиной 4 байта.
    Каждая из этих букв занимает 1 байт при кодировании в UTF-8. Но как насчёт следующей строки? (Обратите внимание, что эта строка начинается с заглавной кириллической "З", а не арабской цифры 3.)
    Отвечая на вопрос, какова длина строки, вы можете ответить 12. Однако ответ Rust - 24,
    что равно числу байт, необходимых для кодирования «Здравствуйте» в UTF-8, так происходит, потому что каждое скалярное значение Unicode символа в этой строке занимает 2 байта памяти. Следовательно, индекс по байтам строки не всегда бы соответствовал действительному скалярному Unicode значению. Для демонстрации рассмотрим этот недопустимый код Rust:
    $
    cargo run
    Compiling collections v0.1.0 (file:///projects/collections) error[E0277]: the type `String` cannot be indexed by `{integer}`
    -->
    src/main.rs:3:13
    |
    3 | let h = s1[0];
    | ^^^^^ `String` cannot be indexed by `{integer}`
    |
    = help: the trait `Index<{integer}>` is not implemented for `String`
    = help: the following other types implement trait `Index`:
    >>
    >
    >>
    >>
    >>
    >>
    >
    For more information about this error, try `rustc --explain E0277`. error: could not compile `collections` due to previous error let hello =
    String
    ::from(
    "Hola"
    ); let hello =
    String
    ::from(
    "Здравствуйте"
    ); let hello =
    "Здравствуйте"
    ; let answer = &hello[
    0
    ];

    Каким должно быть значение переменной answer
    ? Должно ли оно быть значением первой буквы
    З
    ? При кодировке в UTF-8, первый байт значения
    З
    равен
    208
    , а второй -
    151
    , поэтому значение в answer на самом деле должно быть
    208
    , но само по себе
    208
    не является действительным символом. Возвращение
    208
    , скорее всего не то, что хотел бы получить пользователь: ведь он ожидает первую букву этой строки; тем не менее, это единственный байт данных, который в Rust доступен по индексу 0. Пользователи обычно не хотят получить значение байта, даже если строка содержит только латинские буквы:
    если
    &"hello"[0] было бы допустимым кодом, который вернул значение байта, то он вернул бы
    104
    , а не h
    Таким образом, чтобы предотвратить возврат непредвиденного значения, вызывающего ошибки которые не могут быть сразу обнаружены, Rust просто не компилирует такой код и предотвращает недопонимание на ранних этапах процесса разработки.
    Байты, скалярные значения и кластеры графем! Боже мой!
    Ещё один момент, касающийся UTF-8, заключается в том, что на самом деле существует три способа рассмотрения строк с точки зрения Rust: как байты, как скалярные значения и как кластеры графем (самая близкая вещь к тому, что мы назвали бы буквами).
    Если посмотреть на слово языка хинди «नम े», написанное в транскрипции Devanagari, то оно хранится как вектор значений u8
    который выглядит следующим образом:
    Эти 18 байт являются именно тем, как компьютеры в конечном итоге сохранят в памяти эту строку. Если мы посмотрим на 18 байт как на скалярные Unicode значения, которые являются Rust типом char
    , то байты будут выглядеть так:
    Здесь есть шесть значений типа char
    , но четвёртый и шестой являются не буквами: они диакритики, специальные обозначения которые не имеют смысла сами по себе.
    Наконец, если мы посмотрим на байты как на кластеры графем, то получим то, что человек назвал бы словом на хинди состоящем из четырёх букв:
    Rust предоставляет различные способы интерпретации необработанных строковых данных, которые компьютеры хранят так, чтобы каждой программе можно было выбрать необходимую интерпретацию, независимо от того, на каком человеческом языке представлены эти данные.
    Последняя причина, по которой Rust не позволяет нам индексировать
    String для получения символов является то, что программисты ожидают, что операции
    [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
    224, 165, 135]
    ['न', 'म', 'स', '◌्', 'त', '◌े']
    ["न", "म", "स्", "ते"]
    индексирования всегда имеют постоянное время (O(1)) выполнения. Но невозможно гарантировать такую производительность для
    String
    , потому что Rust понадобилось бы пройтись по содержимому от начала до индекса, чтобы определить, сколько было действительных символов.
    Срезы строк
    Индексирование строк часто является плохой идеей, потому что не ясно каким должен быть возвращаемый тип такой операции: байтовым значением, символом, кластером графем или срезом строки. Поэтому Rust просит вас быть более конкретным, если действительно требуется использовать индексы для создания срезов строк.
    Вместо индексации с помощью числового индекса
    []
    , вы можете использовать оператор диапазона
    []
    при создании среза строки в котором содержится указание на то,
    срез каких байтов надо делать:
    Здесь переменная s
    будет типа
    &str который содержит первые 4 байта строки. Ранее мы упоминали, что каждый из этих символов был по 2 байта, что означает, что s
    будет содержать
    Зд
    Что бы произошло, если бы мы использовали
    &hello[0..1]
    ? Ответ: Rust бы запаниковал во время выполнения точно так же, как если бы обращались к недействительному индексу в векторе:
    Вы должны использовать диапазоны для создания срезов строк с осторожностью, потому что это может привести к сбою вашей программы.
    Методы для перебора строк
    Лучший способ работать с фрагментами строк - чётко указать, нужны ли вам символы или байты. Для отдельных скалярных значений в Юникоде используйте метод chars
    Вызов chars у “Зд” выделяет и возвращает два значения типа char
    , и вы можете выполнить итерацию по результату для доступа к каждому элементу:
    let hello =
    "Здравствуйте"
    ; let s = &hello[
    0 4
    ];
    $
    cargo run
    Compiling collections v0.1.0 (file:///projects/collections)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
    Running `target/debug/collections` thread 'main' panicked at 'byte index 1 is not a char boundary; it is inside 'З'
    (bytes 0..2) of `Здравствуйте`', library/core/src/str/mod.rs:127:5 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

    Код напечатает следующее:
    Метод bytes возвращает каждый байт, который может быть подходящим в другой предметной области:
    Этот код выведет четыре байта, составляющих эту строку:
    Но делая так, обязательно помните, что валидные скалярные Unicode значения могут состоять более чем из одного байта.
    Извлечение кластеров графем из строк, как в случае с языком хинди, является сложным,
    поэтому эта функциональность не предусмотрена стандартной библиотекой. На crates.io есть доступные библиотеки, если Вам нужен данный функционал.
    Строки не так просты
    Подводя итог, становится ясно, что строки сложны. Различные языки программирования реализуют различные варианты того, как представить эту сложность для программиста.
    В Rust решили сделать правильную обработку данных
    String поведением по умолчанию для всех программ Rust, что означает, что программисты должны заранее продумать обработку UTF-8 данных. Этот компромисс раскрывает большую сложность строк, чем в других языках программирования, но это предотвращает от необходимости обрабатывать ошибки, связанные с не-ASCII символами которые могут появиться в ходе разработки позже.
    Хорошая новость состоит в том что стандартная библиотека предлагает множество функциональных возможностей, построенных на основе типов
    String и
    &str
    , чтобы помочь правильно обрабатывать эти сложные ситуации. Обязательно ознакомьтесь с for c in
    "Зд"
    .chars() { println!
    (
    "{}"
    , c);
    }
    З д for b in
    "Зд"
    .bytes() { println!
    (
    "{}"
    , b);
    }
    208 151 208 180
    документацией для полезных методов, таких как contains для поиска в строке и replace для замены частей строки другой строкой.
    Давайте переключимся на что-то немного менее сложное: HashMap!

    Хранение ключей со связанными значениями в
    HashMap
    Последняя коллекция, которую мы рассмотрим, будет hash map (хеш-карта). Тип
    HashMap
    хранит ключи типа
    K
    на значения типа
    V
    . Данная структура организует и хранит данные с помощью функции хеширования. Во множестве языков программирования реализована данная структура, но часто с разными наименованиями: такими как hash, map, object, hash table, dictionary или ассоциативный массив.
    Хеш-карты полезны, когда нужно искать данные не используя индекс, как это например делается в векторах, а с помощью ключа, который может быть любого типа. Например, в игре вы можете отслеживать счёт каждой команды в хеш-карте, в которой каждый ключ - это название команды, а значение - счёт команды. Имея имя команды, вы можете получить её счёт из хеш-карты.
    В этом разделе мы рассмотрим базовый API хеш-карт. Остальной набор полезных функций скрывается в объявлении типа
    HashMap
    . Как и прежде, советуем обратиться к документации по стандартной библиотеке для получения дополнительной информации.
    Создание новой хеш-карты
    Создать пустую хеш-карту можно с помощью new
    , а добавить в неё элементы - с помощью insert
    . В листинге 8-20 мы отслеживаем счёт двух команд, синей Blue и жёлтой Yellow. Синяя команда набрала 10 очков, а жёлтая команда - 50.
    Листинг 8-20: Создание новой хеш-карты и вставка в неё пары ключей и значений
    Обратите внимание, что нужно сначала указать строку use std::collections::HashMap;
    для её подключения из коллекций стандартной библиотеки. Из трёх коллекций данная является наименее используемой, поэтому она не подключается в область видимости функцией автоматического импорта (prelude). Хеш-карты также имеют меньшую поддержку со стороны стандартной библиотеки; например, нет встроенного макроса для их конструирования.
    Подобно векторам, хеш-карты хранят свои данные в куче. Здесь тип
    HashMap имеет в качестве типа ключей
    String
    , а в качестве типа значений тип i32
    . Как и векторы,
    use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
    String
    ::from(
    "Blue"
    ),
    10
    ); scores.insert(
    String
    ::from(
    "Yellow"
    ),
    50
    );

    HashMap однородны: все ключи должны иметь одинаковый тип и все значения должны иметь тоже одинаковый тип.
    Доступ к данным в HashMap
    Мы можем получить значение из HashMap по ключу, с помощью метода get
    , как показано в листинге 8-21.
    Листинг 8-21: Доступ к очкам команды "Blue", которые хранятся в хеш-карте
    Здесь score будет иметь количество очков, связанное с командой "Blue", результат будет
    10
    . Метод get возвращает
    Option<&V>
    ; если для какого-то ключа нет значения в
    HashMap, get вернёт
    None
    . Из-за такого подхода программе следует обрабатывать
    Option
    , вызывая copied для получения
    Option
    вместо
    Option<&i32>
    , затем unwrap_or для установки score в ноль, если scores не содержит данных по этому ключу.
    Мы можем перебирать каждую пару ключ/значение в HashMap таким же образом, как мы делали с векторами, используя цикл for
    :
    Этот код будет печатать каждую пару в произвольном порядке:
    Хеш-карты и владение
    use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
    String
    ::from(
    "Blue"
    ),
    10
    ); scores.insert(
    String
    ::from(
    "Yellow"
    ),
    50
    ); let team_name =
    String
    ::from(
    "Blue"
    ); let score = scores.get(&team_name).copied().unwrap_or(
    0
    ); use std::collections::HashMap; let mut scores = HashMap::new(); scores.insert(
    String
    ::from(
    "Blue"
    ),
    10
    ); scores.insert(
    String
    ::from(
    "Yellow"
    ),
    50
    ); for
    (key, value) in
    &scores { println!
    (
    "{}: {}"
    , key, value);
    }
    Yellow: 50
    Blue: 10

    Для типов, которые реализуют типаж
    Copy
    , например i32
    , значения копируются в
    HashMap. Для значений со владением, таких как
    String
    , значения будут перемещены в хеш-карту и она станет владельцем этих значений, как показано в листинге 8-22.
    1   ...   14   15   16   17   18   19   20   21   ...   62


    написать администратору сайта