Содержание


Rust - новый язык программирования: Часть 9. Работа с памятью - заимствованные указатели (продолжение)

Comments

В предыдущих статьях цикла был сделан общий обзор языка программирования Rust, обсуждались основы синтаксиса: были описаны простейшие компоненты (переменные, простые типы данных, операторы и расширения), управляющие конструкции и составные структуры данных, функции и деструкторы. Особое внимание было уделено модели памяти в целом, концепции владения объектами, общим принципам управления памятью, использованию собственных и управляемых блоков общей памяти, а также было начато рассмотрение весьма важных компонентов Rust - заимствованных указателей. В данной статье продолжается обсуждение заимствованных указателей.

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

1. Заимствованные указатели и перечисления

В последнем примере из предыдущей статьи было показано, что система типов запрещает любое заимствование собственных блоков памяти, расположенных в изменяемой общей памяти (heap). Это ограничение введено для того, чтобы исключить саму возможность указания на освобождённую память. Но есть ещё один, отличающийся от рассмотренных выше, случай, когда компилятор должен быть чрезвычайно внимательным, чтобы гарантировать сохранение корректности указателей: указатели на внутренние элементы перечисления enum.

Например, можно рассмотреть тип Shape, определённый для представления прямоугольников и окружностей, а также функцию вычисления площади этих фигур. Эта функция принимает заимствованный указатель на фигуру типа shape, чтобы избежать прямого копирования всего объекта, как показано в листинге 1.

Листинг 1. Перечисление, определяющее геометрические фигуры, и функция вычисления их площади
struct Point { x: float, y: float };
struct Size { w: float, h: float };
enum Shape {
  Circle( Point, float ),   // центр, радиус
  Rectangle( Point, Size )  // верхний левый угол, размеры
}

fn compute_area( shape: &Shape ) -> float {
  match *shape {
    Circle( _, radius ) => pi * radius * radius,
    Rectangle( _, ref size ) => size.w * size.h
  }
}

Первая ветвь поиска совпадения по образцу предназначена для окружностей. Здесь образец извлекает значение радиуса из текущего рассматриваемого варианта перечисления shape и использует его в части действия для вычисления площади круга.

Во второй ветви поиска всё не так просто. Совпадение ищется для прямоугольника Rectangle, и извлекается его размер, но вместо копирования всей структуры size используется связывание по ссылке для создания указателя на эту структуру. Другими словами, связывание в образце, подобное ref size, связывает имя size с указателем типа &size на внутренний элемент рассматриваемого перечисления.

Для наглядности лучше воспользоваться схематичным изображением памяти в том случае, когда shape указывает на прямоугольник, как показано на рис.1.

Рис.1. Схема памяти при вычислении площади прямоугольника
Схема памяти при вычислении площади прямоугольника
Схема памяти при вычислении площади прямоугольника

На схеме можно видеть, что фигуры типа "прямоугольник" состоят из пяти слов памяти. Первое слово - это тэг, обозначающий текущий вариант, выбранный из перечисления (в данном случае - rectangle). Следующие два слова - это поля x и y для точки (структура Point), а последние два слова представляют поля w и h структуры размера (тип Size). Таким образом, переменная size является указателем на внутренний элемент данного перечисления типа Shape.

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

Рис.2. Схема памяти при динамической замене значения shape на тип Circle
Схема памяти при динамической замене значения shape на тип Circle
Схема памяти при динамической замене значения shape на тип Circle

По схеме видно, что после предположенной выше замены указатель size должен будет указывать на значение типа float вместо структуры. Сразу возникает мысль о некорректности такой ситуации: ведь разыменование второго поля, имеющего тип float, таким образом, как если бы оно было структурой с двумя полями, неизбежно приведёт к нарушению защиты памяти.

Но на самом деле для каждого связывания ref компилятор будет применять те же правила, которые были описаны ранее при заимствовании внутреннего элемента собственного блока памяти: компилятор должен иметь возможность гарантировать, что объект перечисления enum не будет переназначен или заменён на протяжении всего интервала времени заимствования. На практике компилятор должен принять пример, приведённый выше в листинге 1, как вполне корректный. Код в примере считается безопасным, потому что указатель shape имеет тип &Shape, что означает буквально "заимствованный указатель на неизменяемый блок памяти, содержащий shape". Но если бы типом этого указателя был &mut Shape, то связывание ref нельзя было бы считать корректным по типу. Точно так же, как для собственных блоков памяти, компилятор будет разрешать ref-связывания, с указанием на данные, которыми владеет фрейм стека, даже если эти данные изменяемые, но в противном случае становится обязательным требование, чтобы данные размещались в неизменяемой памяти.

2. Возврат заимствованных указателей из функций

До сих пор во всех рассматриваемых примерах заимствованные указатели использовались в "нисходящем" направлении, то есть, метод или блок кода создавал заимствованный указатель, затем использовал его в определённой области видимости. Но возможно ещё и такое действие, как возврат заимствованного указателя в качестве результата функции, хотя это и потребует некоторого явного квалифицирования, о чём пойдёт речь ниже.

Например, можно написать некоторую подпрограмму, показанную в листинге 2.

Листинг 2. Пример подпрограммы, возвращающей заимствованный указатель
struct Point { x: float, y: float }
fn get_x<'r>( p: &'r Point ) -> &'r float { &p.x }

Функция get_x() возвращает указатель на внутренний элемент переданной ей структуры. И тип параметра (&'r Point), и тип возвращаемого значения (&'r float) используют новую синтаксическую форму, которая до настоящего момента ещё не встречалась. В данном случае идентификатор r в явной форме именует (обозначает) время существования данного указателя. По сути эта функция объявляет, что она принимает указатель с временем существования r и возвращает указатель с тем же самым временем существования.

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

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

Именованные времена существования, появляющиеся в сигнатурах функций, теоретически представляют собой то же самое, что и времена существования объектов, рассматриваемые ранее, но всё же именованные времена существования немного более абстрактны: они не ссылаются (не указывают) на конкретное выражение внутри функции get_x(), вместо этого они ссылаются (указывают) на некоторое выражение внутри функции, вызвавшей get_x(). В действительности время существования r - это разновидность параметра типа "время существования" (lifetime parameter). Он определяется в том коде, который вызывает функцию get_x(), точно так же, как значение обычного параметра p определяется вызывающей стороной.

В любом случае, каким бы ни было время существования r, указатель, производный от &p.x, всегда будет иметь то же время существования, что и сам объект p: указатель на поле структуры корректен только в том интервале времени, когда корректна сама эта структура. Следовательно, компилятор принимает функцию get_x() как корректную.

Чтобы подчеркнуть важность этого момента, следует рассмотреть противоположный пример, который отвергается компилятором (см. листинг 3).

Листинг 3. Некорректное использование заимствованного указателя при возврате из функции
struct Point { x: float, y: float }
fn get_x_sh( p: @Point ) -> &float {
  &p.x  // здесь компилятор обнаружит ошибку
}

В этом примере функция get_x_sh() принимает в качестве парметра указатель на управляемый блок памяти, а возвращает заимствованный указатель. Как и в предыдущем примере, время существования этого заимствованного указателя, который должен быть возвращён, как результат данной функции, является специальным параметром, определяемым вызывающей стороной. Это означает, что get_x_sh() "обещает" вернуть заимствованный указатель, корректный в том интервале времени существования, который предполагается вызывающей стороной: это слегка отличается от первого примера, где корректность возвращаемого указателя обеспечивалась зависимостью его времени существования от времени существования соответствующего указателя-аргумента.

В функции get_x_sh() имеется выражение &p.x, которое даёт адрес поля управляемого блока памяти. Наличие этого выражения предполагает, что компилятор непременно должен обеспечить такое положение дел, при котором сохраняется корректность возвращаемого указателя, следовательно, управляемый блок памяти не должен быть удалён механизмом сборки мусора. Но вполне возможен повторный вызов, при котором get_x_sh() также "обещает" вернуть указатель, корректный в течение времени существования, требуемого вызывающей стороной. Очевидно, что get_x_sh() не в состоянии обеспечить оба подобных требования; фактически, нельзя дать никаких гарантий того, что рассматриваемый указатель будет оставаться корректным во всех случаях, когда он возвращается, так как параметр p в вызывающем коде может продолжать существование, но также может быть и удалён. Именно поэтому компилятор не может гарантировать безопасность такого кода, а следовательно сообщает об ошибке.

Вообще говоря, при заимствовании управляемого (или собственного) блока памяти для создания заимствованного указателя этот указатель будет корректным только внутри текущей функции и не может быть возвращён. Поэтому обычным способом возврата заимствованных указателей является приём тех же заимствованных указателей, как входных данных (параметров). Пожалуй, единственное исключение, в котором может быть корректным возврат заимствованного указателя, - это случай, когда указатель указывает на статическую константу.

3. Именованное время существования

В предыдущем разделе выяснилась важность применения именованного времени существования, поэтому стоит рассмотреть этот специальный параметр более подробно. Именованное время существования позволяет группировать параметры по времени существования. Для наглядной демонстрации сначала потребуется функция, приведённая в листинге 4.

Листинг 4. Функция с применением именованного времени существования нескольких параметров
fn select<'r, T>( shape: &'r Shape, threshold: float,
                  a: &'r T, b: &'r T ) -> &'r T {
  if compute_area( shape ) > threshold { a } else { b }
}

Эта функция в качестве параметров принимает три заимствованных указателя и присвивает каждому из них одно и то же время существования r. Это может оказаться и излишней, чрезмерной осторожностью, как показано в следующем примере в листинге 5.

Листинг 5. Использование функции select с явно задаваемым именованным временем существования
// Именованное время существования r существует и вне тела данной функции
fn select_based_on_unit_circle<'r, T>( threshold: float,
                                      a: &'r T, b: &'r T ) -> &'r T {
  let shape = Circle( Point { x: 0., y: 0. }, 1. );  // B - время 
                                       //существования тела функции
  select( shape, threshold, a, b )                   // |
}                                                  // <-+

В выполняемом здесь вызове select() временем существования передаваемого параметра shape является B, то есть время существования тела вызывающей функции. Следующие два параметра, a и b, имеют одно и то же время существования - r, которое является специальным параметром вызывающей функции select_based_on_unit_circle(). Эта вызывающая функция будет определять пересечение двух вышеперечисленных времён существования как время существования возвращаемого значения, и вследствие этого возвращаемому значению функции select() будет присвоено время существования B. Это в свою очередь приведёт к ошибке компиляции, потому что предполагается, что select_based_on_unit_circle() возвращает значение с временем существования r.

Для исправления этой ошибки можно изменить определение функции select() таким образом, чтобы время существования первого параметра отличалось от времени существования остальных двух. В конце концов первый параметр не будет возвращаться. В листинге 6 показан один из возможных вариантов альтернативного определения функции select().

Листинг 6. Другое определение функции select() с изменением первого параметра
fn select<'r, 'tmp, T>( shape &'tmp Shape, threshold: float,
                        a: &'r T, b: &'r T ) -> &'r T {
  if compute_area( shape ) > threshold { a } else { b }
}

Время существования для параметра shape теперь именуется tmp. Для параметров a и b, а также для возвращаемого значения имя времени существования осталось прежним - r. Но вполне логично предположить, что если время существования tmp не имеет никакого отношения к возвращаемому значению, то его можно просто не указывать для параметра shape, как показано в листинге 7.

Листинг 7. Более лаконичное определение функции select()
fn select<'r, T>( shape: &Shape, threshold: float, a: &'r T, b: &'r T ) -> &'r T {
  if compute_area( shape ) > threshold { a } else { b }
}

Это определение полностью равнозначно определению, приведённому в предыдущем листинге 6.

4. Подведение итогов описания системы управления памятью в языке rust

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

4.1. Чем отличается указатель на управляемый блок памяти (@) от указателя на собственный блок памяти (~)

Управляемые блоки памяти существуют в общей памяти (heap), локальной для текущей задачи, в которой работает локальный же механизм сборки мусора.

Собственные блоки памяти существуют в глобальной общей памяти с возможностью обмена (global exchange heap).

На управляемые блоки памяти могут указывать несколько указателей соответствующего типа (указатели на управляемые блоки памяти).

Собственные блоки памяти имеют единственного владельца, следовательно, в любой момент времени может существовать только один указатель на собственный блок памяти.

Управляемые блоки памяти не могут совместно использоваться несколькими задачами одновременно.

Собственные блоки памяти могут передаваться (точнее, перемещаться) между задачами.

4.2. Чем отличается заимствованный указатель (&) от указателей на управляемый и собственный блоки памяти

Заимствованный указатель указывает на внутренний элемент стека или на размещённый (выделенный) фрагмент общей памяти (heap).

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

Заимствованный указатель на управляемый блок памяти позволяет продлить время существования данного управляемого блока памяти.

Заимствованный указатель на собственный блок памяти делает невозможной передачу владения этим блоком памяти.

4.3. Какие указатели использовать на практике

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

  1. ~ поддерживает один и только один указатель на выделенный блок памяти. Если необходимо создать несколько указателей на одни и те же данные, то рекомендуется использовать @. Но @-указатели требуют работы механизма сборки мусора, что снижает производительность, и если это важно для приложения, то следует применять ~ во всех возможных случаях.
  2. Если нужно передавать данные между несколькими тредами, то @-указатели для этого неприменимы, так как их передача запрещена. Для этих целей используются ~-указатели.

И последнее замечание: в очередной раз следует отметить, что если эти правила накладывают слишком жёсткие ограничения на работу с памятью (например, нужно несколько указателей на данные, но нежелательны задержки, связанные с работой GC), то существуют более изощрённые решения: заимствованные указатели и даже незащищённые блоки кода (unsafe code). Тем не менее, простая система собственных (owned) и управляемых (managed) указателей на блоки памяти хорошо работает во многих программах и формирует фундамент rust-подхода к управлению памятью.

Заключение

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=951108
ArticleTitle=Rust - новый язык программирования: Часть 9. Работа с памятью - заимствованные указатели (продолжение)
publish-date=11012013