В предыдущем посте (англ.) мы затронули два столпа дизайна Rust (поскольку во внутренней речи я постоянно склоняю название языка, дальше я буду использовать русскоязычное название «раст», что мне кажется более органичным — прим. перев.):
  • безопасное использование памяти без сборщика мусора,
  • многопоточность без гонок данных.

Этот пост начинает рассказ о третьем столпе:
  • абстракции без накладных расходов.

Одна из мантр C++, которая делает его таким подходящим для системного программирования — принцип абстракции с нулевой стоимостью:
Реализации C++ подчиняются принципу нулевой стоимости: ты не платишь за то, что не используешь [Страуструп, 1994]. Более того: то, что ты используешь, кодируется наилучшим образом.

– Бьёрн Страуструп

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

Центральное понятие абстракции в расте — типажи (traits).

  • Типажи в расте играют роль интерфейса. Типаж могут реализовывать несколько типов, а новые типажи могут предоставлять реализации для существующих типов. С другой стороны, если вы хотите абстрагироваться от неизвестного типа, типажи дают возможность указать конкретные требования к этому типу, определяющие ожидаемое от него поведение.
  • К типажам может применяться статическая диспетчеризация. Как и с шаблонами в C++, компилятор может сгенерировать реализацию абстракции для каждого случая её использования. Возвращаясь к мантре C++ — «то, что ты используешь, кодируется наилучшим образом» — абстракция полностью удаляется из скомпилированного кода.
  • К типажам может применяться и динамическая диспетчеризация. Иногда вам действительно нужна косвенная адресация, так что нет смысла «удалять» абстракцию из скомпилированного кода. То же самое представление интерфейса — типаж — может быть использовано для диспетчеризации во время выполнения программы.
  • Типажи решают множество разнообразных проблем помимо простого абстрагирования. Они используются как «маркеры» для типов, например маркер Send, описанный в предыдущем посте. Они же используются для объявления «дополнительных методов», то есть, чтобы добавить новые методы к уже определённому где-то типу. Они заменяют собой традиционную перегрузку методов. А ещё они предоставляют простой способ перегрузки операторов.

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

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

Основа: методы в расте


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

В расте есть как методы, так и самостоятельные функции, и они тесно связаны друг с другом:

struct Point {
    x: f64,
    y: f64,
}

// a free-standing function that converts a (borrowed) point to a string
fn point_to_string(point: &Point) -> String { ... }

// an "inherent impl" block defines the methods available directly on a type
impl Point {
    // this method is available on any Point, and automatically borrows the
    // Point value
    fn to_string(&self) -> String { ... }
}

Методы, наподобие to_string, называются «собственными» потому, что они:

  • привязаны к конкретному типу «self» (указанному в заголовке блока impl),
  • автоматически доступны для всех значений этого типа, то есть, в отличие от функций, собственные методы всегда «в области видимости».

Первый параметр метода всегда явно указан в виде «self», и может быть self, &mut self, либо &self — в зависимости от требуемого уровня владения (может ещё быть mut self, но по отношению к владению это то же самое, что и self — прим. перев.). Методы вызываются с использованием точки (.), как в обычном ООП, а параметр self неявно заимствуется, если того требует сигнатура метода:

let p = Point { x: 1.2, y: -3.7 };
let s1 = point_to_string(&p);  // calling a free function, explicit borrow
let s2 = p.to_string();        // calling a method, implicit borrow as &p

Методы и авто-заимствование — важные аспекты эргономичности раста, поддерживающие простоту API, например интерфейса создания процесса:

let child = Command::new("/bin/cat")
    .arg("rusty-ideas.txt")
    .current_dir("/Users/aturon")
    .stdout(Stdio::piped())
    .spawn();

Типажи как интерфейс


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

Возьмём, например, следующий простой типаж, описывающий хеширование:

trait Hash {
    fn hash(&self) -> u64;
}

Чтобы реализовать этот типаж для какого-либо типа, мы должны написать метод hash с соответствующей сигнатурой:

impl Hash for bool {
    fn hash(&self) -> u64 {
        if *self { 0 } else { 1 }
    }
}

impl Hash for i64 {
    fn hash(&self) -> u64 {
        *self as u64
    }
}

В отличие от интерфейсов в таких языках, как Java, C# или Scala, новые типажи могут быть реализованы для уже существующих типов (как в случае с Hash в последнем примере). То есть абстракции могут быть созданы по необходимости, а затем применены к уже существующим библиотекам.

В отличие от собственных методов, методы типажей находятся в области видимости только тогда, когда их типаж в области видимости. Но если предположить, что типаж Hash уже находится в нашей области видимости, вы можете написать true.hash(). Таким образом, реализация типажа расширяет набор методов, доступный для данного типа.

Ну и… это всё! Определение и реализация типажа — не более чем абстрагирование общего интерфейса, которому удовлетворяют несколько типов.

Статическая диспетчеризация


Всё становится интереснее с другой стороны — для пользователей типажей. Самый частый способ использования типажей ­— через использование типового параметризма:

fn print_hash<T: Hash>(t: &T) {
    println!("The hash is {}", t.hash())
}

Функция print_hash параметризована неизвестным типом T, но требует, чтобы этот тип реализовал типаж Hash. Что означает, что мы можем использовать её для значений типов bool и i64:

print_hash(&true);      // instantiates T = bool
print_hash(&12_i64);    // instantiates T = i64

Параметризованные типами функции после компиляции разворачиваются в конкретные реализации, в результате получаем статическую диспетчеризацию. Здесь, как и с шаблонами C++, компилятор сгенерирует две копии функции print_hash: по версии для каждого используемого вместо типового аргумента типа. В свою очередь, это означает, что внутренний вызов к t.hash() — то место, где используется абстракция — имеет нулевую стоимость, так как он будет скомпилирован в прямой статический вызов к соответствующей реализации метода hash:

// The compiled code:
__print_hash_bool(&true);  // invoke specialized bool version directly
__print_hash_i64(&12_i64);   // invoke specialized i64 version directly

Такая модель компиляции не очень полезна для функции вроде print_hash, но весьма удобна для более реалистического использования хеширования. Предположим, что у нас так же есть типаж для сравнение на равенство:

trait Eq {
    fn eq(&self, other: &Self) -> bool;
}

(Тип Self здесь будет заменён на тип, для которого реализован данный типаж; в случае impl Eq for bool он будет соответствовать типу bool.)

Мы можем определить тип-словарь, параметризованный типом T, для которого должны быть реализованы типажи Eq и Hash:

struct HashMap<Key: Hash + Eq, Value> { ... }

Тогда модель статической компиляции для параметрических типов даст несколько преимуществ:

Каждое использование HashMap с конкретными типами Key и Value приведёт к созданию отдельного конкретного типа HashMap, что означает, что HashMap может содержать ключи и значения непосредственно в своих бакетах, без использования косвенной адресации. А это экономит место, уменьшает количество разименований указателей и позволяет более полно использовать память кеша.

Каждый метод HashMap также скомпилируется в специализированный для заданных типов код. Так что нет дополнительных расходов на диспетчеризацию при вызовах методов hash и eq. Это так же означает, что оптимизатор сможет работать с полностью конкретным кодом — то есть с точки зрения оптимизатора абстракций нет. В частности статическая диспетчеризация позволяет инлайнить параметризованные типами методы.

Вместе с тем, как и в случае шаблонов C++, эти свойства параметрических типов означают, что вы можете писать достаточно высокоуровневые абстракции, которые компилируются в полностью конкретный машинный код, «закодированный наилучшим образом».

Однако, в отличие от шаблонов C++, использование типажей полностью проверяется на корректность типов. То есть когда вы компилируете HashMap сам по себе, его код проверяется на типы только один раз, на корректное использование абстрактных типажей Hash и Eq, а не каждый раз при применении конкретных типов. Что означает как более ясные и ранние ошибки компиляции для авторов библиотек, так и меньшие затраты на проверку типов для пользователей языка (читать «более быстрая компиляция»).

Динамическая диспетчеризация


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

Например, GUI фреймворки часто используют колбеки для реакции на события, вроде клика мышкой:

trait ClickCallback {
    fn on_click(&self, x: i64, y: i64);
}

Для элементов GUI часто характерна поддержка регистрации нескольких колбеков для одного события. С помощью параметрических типов вы могли бы написать что-то такое:

struct Button<T: ClickCallback> {
    listeners: Vec<T>,
    ...
}

Но тут возникает очевидная проблема: каждая кнопка будет специализирована только для одного типа, реализующего ClickCallback, и это отражается на конкретном типе кнопки. Это совсем не то, что нам нужно! Мы хотим один конкретный тип кнопки Button с набором разнородных получателей события, каждый из которых может быть произвольного конкретного типа, который реализует типаж ClickCallback.

Одна из сложностей при этом состоит в том, что мы имеем дело с группой разнородных типов, каждый из которых может иметь разный размер. Так как же нам их расположить в векторе? Решение стандартно: с помощью косвенной адресации. Мы сохраним в векторе указатели на колбеки.

struct Button {
    listeners: Vec<Box<ClickCallback>>,
    ...
}

Здесь мы используем типаж так, как будто это тип. Вообще-то в расте типажи — это «безразмерные» типы, что примерно означает, что их можно использовать только через указатели, например с помощью Box (указатель на кучу) или & (любой указатель куда угодно).

В расте тип &ClickCallback или Box<ClickCallback> называется «объект-типаж» и включает в себя указатель на экземпляр типа T, который реализует заданный типаж (ClickCallback), и указатель на таблицу виртуальных методов с указателями на все методы типажа, реализованные для типа T (в нашем случае только метод on_click). Этой информации достаточно, чтобы корректно определить вызываемый метод во время выполнения программы, при этом сохранить единое представление для всех возможных T. Так что тип Button будет скомпилирован только один раз, а абстракции будут существовать и во время выполнения.

Статическая и динамическая диспетчеризация — дополняющие друг друга инструменты, каждый из которых подходит для своих случаев. Типажи в расте дают единую простую систему интерфейсов, которая может быть использована в обоих случаях с минимальной предсказуемой ценой. Объекты-типажи удовлетворяют принципу Страуструпа «плати по необходимости»: у вас есть таблицы виртуальных методов тогда, когда они нужны, но тот же самый типаж может быть статически развёрнут и убран во время компиляции, когда эта сложность не нужна.

Множество способов использовать типажи


Мы увидели то, как могут работать типажи, и их основные способы применения, но они играют и другие важные роли в расте. Несколько примеров:

  • Замыкания. Как и типаж ClickCallback, замыкания в расте просто отдельные типажи. Подробнее о том, как они устроены, можно почитать в блоге Хуона Вилсона (Huon Wilson) в этом подробном посте.
  • Условные API. Параметрические типы дают возможность реализовать типажи по условию:

      struct Pair<A, B> { first: A, second: B }
      impl<A: Hash, B: Hash> Hash for Pair<A, B> {
          fn hash(&self) -> u64 {
              self.first.hash() ^ self.second.hash()
          }
      }
    

    Здесь тип Pair реализует типаж Hash тогда, и только тогда, когда его компоненты тоже реализуют этот типаж. Это позволяет использовать один и тот же тип в разных контекстах, при этом поддерживая наиболее широкое API, возможное в каждом контексте. Это настолько обычный для раста подход, что есть даже поддержка для автоматического создания некоторых типажей:

      #[derive(Hash)]
      struct Pair<A, B> { .. }
    

  • Дополнительные методы. Можно использовать типажи для добавления новых методов к существующим типам (которые определены в других местах), подобно расширенным методам (extension methods) в C#. Это свойство происходит напрямую из правил видимости методов типажа: просто добавляете новые методы в типаж, реализуете этот типаж для нужного типа, и всё, методы готовы к использованию!
  • Маркеры. В расте есть несколько «маркеров», которые классифицируют типы: Send, Sync, Copy, Sized. Эти маркеры — просто типажи с пустыми телами, которые затем могут быть использованы как в параметрических типах, так и в типажах-объектах. Маркеры могут быть определены в библиотеках, и для них автоматически доступны реализации с помощью #[derive]. Например, если все компоненты типа реализуют Send, то и сам тип может автоматически реализовать Send. Как мы видели раньше, маркеры могут быть очень полезными — с помощью того же маркера Send раст гарантирует потокобезопасность.
  • Перегрузка методов. Раст не поддерживает традиционную перегрузку методов, когда один и тот же метод может быть объявлен с разными сигнатурами. Но типажи дают те же преимущества, что и перегрузка: если метод объявлен с типовым параметром, реализующим какой-либо типаж, его можно вызвать с аргументом любого типа, который реализует этот типаж. По сравнению с традиционной перегрузкой методов, у этого подхода есть два преимущества. Во-первых, перегрузка менее ad hoc: как только вы поймёте требуемые типажи, вы сразу же поймёте и способ перегрузки любого API, который их использует. Во-вторых, этот подход очень гибкий: вы можете добавить новые варианты перегрузки методов, просто реализовав нужные типажи для своих типов.
  • Перегрузка операторов. Раст позволяет перегружать операторы вроде + для ваших собственных типов. Каждый оператор определяется соответствующим типажом из стандартной библиотеки, и любой тип, реализующий такой типаж, автоматически будет поддерживать и заданные операторы.

Основная мысль: не смотря на кажущуюся простоту, типажи объединяют в себе множество шаблонов проектирования и способов использования, при этом не добавляя в язык никаких дополнительных фич.

Будущее


Один из основных путей эволюции языка — через существующие в нём абстракции, и раст — не исключение: многие из наших приоритетов после версии 1.0 включают расширение системы типажей тем или иным способом. Вот некоторые примеры:

  • Статическая диспетчеризация по выходным типам. Сейчас можно использовать типовые параметры для входных аргументов методов, но не для выходного типа: нельзя сказать «эта функция возвращает значение какого-то типа, реализующего типаж Iterator», и при этом получить развёртывание абстракции во время компиляции. Это особенно становится проблемой, когда вы хотите вернуть замыкание, и получить для него статическую диспетчеризацию. Это невозможно в современном расте. Мы хотим сделать это возможным, и у нас уже есть некоторые идеи по этому поводу.
  • Специализация. Раст не позволяет перекрываться реализациям типажей, так что двусмысленности по поводу вызываемого метода не возникает. С другой стороны, есть некоторые случаи, когда вы можете написать более общую реализацию, покрывающую множество типов, но потом захотеть сделать более конкретную реализацию для некоторых случаев (что часто требуется, например, при оптимизациях). Мы надеемся, что сможем предложить способы реализовать такое поведение в ближайшем будущем.
  • Типы высшего порядка (ТВП, Higher-kinded types, HKT). Типажи сейчас могут быть применены только к типам, а не конструкторам типов (то есть для Vec<u8> реализовать типаж можно, а просто для Vec — нет). Это ограничение делает очень сложным предоставить хороший набор типажей для контейнеров, которые поэтому и отсутствуют в стандартной библиотеке. ТВП — очень большая и важная фича, которая даст огромный толчок к развитию абстракций в расте.
  • Эффективное повторное использование кода. Наконец, хотя типажи предоставляют механизмы для переиспользования кода (что не было упомянуто с этом посте), всё ещё есть подходы к повторному использованию, для которых не вполне подходят средства нынешнего раста, в частности объектно-ориентированные структуры (типа DOM), GUI-фреймворки, многие игры. Поиск подходов к этим проблемам без лишнего дублирования средств и увеличения сложности — довольно интересная проблема проектирования, про которую Нико Матсакис (Niko Matsakis) начал писать отдельную серию постов в своём блоге. Всё ещё не до конца ясно, можно ли сделать всё это с помощью типажей, или требуются какие-то новые ингредиенты.

Конечно, мы ещё только подходим к релизу 1.0, и потребуется какое-те время, пока не осядет пыль, а сообщество накопит достаточно опыта, чтобы начать добавлять все эти расширения в язык. Но именно поэтому сейчас замечательное время присоединиться к разработке — начиная с участия в проектировании языка на этом раннем этапе, работе по реализации всех этих фич, и вплоть до испытания различных подходов в своём собственном коде. Мы будем рады вашей помощи!

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


  1. tgz
    13.05.2015 20:20
    +3

    Верной дорогой идете, товарищи!!!


  1. namespace
    14.05.2015 09:30
    -3

    Сейчас я расскажу вам, что произойдет. Сегодня Rust — молодой, мочный язык, с n типами указателей, все потокобезопасно, дизайн языка from the ground up просто шикарен, не прикопаться. Он конечно же лучше старых злобных С++, которые ко всему прочему не всегда потокобезопасные, так еще и протекают!

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


    1. kstep Автор
      14.05.2015 09:48

      Однако согласитесь, что по крайней мере сейчас он лучше, чем C++. Кроме того, меня уже немного достаёт, что его только с плюсами и сравнивают, это не совсем справедливо, ведь язык волне сопоставим и с питоном (по уровню абстракций), и со скалой/явой, он вполне может конкурировать со многими высокоуровневыми языками.

      И не мне, и не вам судить, что будет через н лет, мы не провидцы.

      А если его никак не продвигать, то прогресса не будет, не будет ничего лучшего на замену плюсам.


      1. namespace
        14.05.2015 10:56
        -5

        Я не соглашусь, что сейчас он лучше, чем С++. Стандратная библиотека в плюсах дает фору растовской, гигантскую фору, тысячекилометровую фору. Да, в Rust есть классные штуки, но говорить, что Rust лучше плюсов — это глупости. Давайте я еще до#$усь до произношения «ява», потому что трясет. Кстати говоря, сравнивать его с Python/Java нет смысла, это языки из совершенно другой оперы и области применения.

        И не мне, и не вам судить, что будет через н лет, мы не провидцы.

        Я никого тут не засуживаю, я всего лишь говорю, что когда Rust окрепнет, он превратится в тот же самый С++, только сбоку. Ну да, еще с thread-safe штуками и двадцати пятью типами указателей.

        А если его никак не продвигать, то прогресса не будет, не будет ничего лучшего на замену плюсам.

        Быть может, вместо того, чтобы заменять плюсы, можно просто их сделать лучше? Посмотрите на скачок, который мы сделали (ну, еще не до конца) с С++11 до С++17. Щас и корутины появятся и все-все-все.


        1. tzlom
          14.05.2015 11:53
          +4

          Так чем вам Rust не угодил-то? Нравятся плюсы и хватает их — используйте и дальше, я вот использую и ничего, жив-здоров. Но давайте честно — как бы не улучшали С++ некоторые вещи в нём если и поменяются, то не в ближайшие 20 лет (т.к. ломать обратную совместимость не будут однозначно), а раз так, то есть незанятая ниша, которую может себе забрать Rust. Я вот пробую писать, не сказал бы что мне прям ВСЁ нравится, написание API поверх СИшных библиотек несколько затруднено, да и никакого хорошего примера пока нету, приходится самому пробовать все варианты и выбирать лучший, не есть гуд когда нет опыта в языке. Но некоторые вещи Rust для C++ недоступны, поэтому перекладывать опыт C++ на Rust несколько некорректно, скажем так, возможность управлять AST на этапе компиляции теоретически позволит исправить многие проблемы без изменения синтаксиса и некрасивых трюков.
          По поводу стандартной библиотеки — вам не кажется что вы переоцениваете важность оной? Тот же Qt позволяет писать программы не прибегая к стандартной библиотеке вовсе(хоть и совместим с нею), помоему это признак того что сила плюсов в самом языке, а не библиотеке.


        1. k12th
          15.05.2015 00:45
          +1

          Кому нужна стандартная библиотека, когда есть Cargo:)


        1. stack_trace
          24.05.2015 11:35

          А можно конкретнее о том, где стандартная библиотека плюсов лучше растовской?


  1. leventov
    14.05.2015 18:29

    1. Специализация / динамическая диспетчеризация зависит только от того, ссылаюсь я в коде на тип «по ссылке» или «по значению»? Могу ли я попросить не плодить специализации для плоских типов, какого-нибудь i64, например, чтобы сохранить маленький размер бинарника, или, наоборот, попросить специализировать вызовы eq() для какого-нибудь развесистого типа, который я хочу хранить в Vec по ссылке, а не по значению?

    2. Могу ли я сделать несколько реализаций типажа для одного и того же типа (в разных модулях), и, в зависимости от модуля, который я заимпортировал, получать по разному скомпилированный код?

    3. Могу ли я делать реализации методов типажа по-умолчанию, опираясь на другие методы в том же типаже, как, например, в Хаскеле, или интерфейсах в Java 8?


    1. kstep Автор
      14.05.2015 19:01

      1. Вид диспетчеризации зависит от использования типажа. Если типаж используется как баундинг на типовый параметр, то будет специализация и статическая диспетчеризация (это в расте называется «мономорфизация»), если используется «ссылка на типаж», то будет создан типаж-объект и будет динамическая диспетчеризация.

      trait DoIt {
        fn do_it(&self);
      }
      
      fn do_it_statically<T: DoIt>(x: T) { // static dispatch
        // ...
      }
      
      fn do_it_dynamically(x: &DoIt) { // dynamic dispatch
        // ...
      }
      


      Всё зависит только от того, как типаж используется, а не от того, как объявляется.
      Другое дело, что некоторые типажи не являются «объектнобезопасными», и использовать их как типажи-объекты нельзя.
      Но это отдельная тема.

      Надеюсь, я ответил на вопрос.

      2. Не совсем понял вопрос, попробую ответить развёрнуто.

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

      б) В пределах одного крейта, если имеется в виду один и тот же типаж для одного и того же типа, то может
      быть только одна такая реализация, иначе будет конфликт реализаций: is.gd/S1FCmO.

      в) Если трейт объявлен с одним и тем же именем и одними и теми же методами в разных модулях одного крейта,
      то это де факте два разных типажа, и да, их можно оба реализовать для одного и того же типа, и подключать какой-то
      из них выборочно: is.gd/QJWLkc.

      Но оба таких типажа из разных модулей подключить просто так нельзя, т.к. будет конфликт имён.
      Можно заюзать форму use modb::A as A1, но тогда всё равно нельзя будет использовать
      метод типажа обычным образом, т.к. не ясно, от какого типажа метод использовать (error: multiple applicable methods in scope).
      Надо будет вызывать метод через UFCS: is.gd/LTfXcZ

      3. Да, конечно можете.


      1. leventov
        14.05.2015 19:17

        1. Я имел ввиду не это.

        В расте тип &ClickCallback или Box<ClickCallback> называется «объект-типаж» и включает в себя указатель на экземпляр типа T, который реализует заданный типаж (ClickCallback), и указатель на таблицу виртуальных методов.


        Могу я ссылаться на экземпляр, при статическом вызове методов (не через таблицу)? Могу я не ссылаться на тип (то есть работать с ним «по значению»), но вызывать методы через таблицу?

        2. Я не очень понял, конкретно вопрос такой: вот, допустим, есть тип HashMap (стандартный, не в моем крейте). Он хочет, чтобы тип ключа имел реализацию типажа Hash (и Eq). Есть мой тип MyKey. Могу ли я в одном модуле написать impl Hash for MyKey {foo foo foo}, в другом — impl Hash for MyKey {bar bar bar}. Потом, когда я пишу: let map = HashMap<MyKey, i64>(); — компиляция бы зависела от того, какой модуль с реализацией impl Hash for MyKey я заимпортил?


        1. CONSTantius
          14.05.2015 19:44

          2. Внутри одного контейнера (crate) — нельзя, в разных — можно, но придётся импортировать типажи под разными и именами и вызывать не как методы, а как функции.


          1. leventov
            14.05.2015 19:47

            Так а если HashMap уже сам вызывает их определенным образом? Не я типаж использую, а HashMap.


            1. kstep Автор
              14.05.2015 19:49

              Так откуда HashMap узнает какую реализацию Hash использовать на MyKey, если таких реализаций несколько?
              В общем, нет, нельзя.


              1. leventov
                14.05.2015 20:28

                Узнает по тому, какую реализацию импортировали. Если импортировали обе — пусть будет конфликт, ошибка компиляции.


                1. kstep Автор
                  14.05.2015 20:48

                  Единица компиляции в расте — крейт, а не модуль. В пределах крейта для пары типаж—тип может быть только одна реализация.


                  1. leventov
                    14.05.2015 20:50

                    Ну а так как если я объявлю MyKey в одном крейте, в другом и Hash, и MyKey окажутся «внешними», поэтому определить другую реализацию я тоже не смогу. Ясно-понятно.


                    1. CONSTantius
                      14.05.2015 21:18

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


                      1. leventov
                        14.05.2015 22:14

                        Да, я это уже понял, то же самое написал комментарием выше. Просто подытожил, что абстрагирование определенного плана не поддерживается.


        1. kstep Автор
          14.05.2015 19:48

          1. При динамической диспетчеризации и идёт ссылка на экземпляр в самом методе, &self и есть такая ссылка.
          При статической диспетчеризации никаких «таблиц» нет в принципе. Я не понимаю вопроса. Варианта два: либо есть таблица виртуальных методов, и в вызванный метод отдаётся ссылка на экземпляр типа через &self, либо нет таблицы, есть статический вызов метода, но в него всё равно передаётся та же самая ссылка на экземпляр типа &self. Всё происходит прозрачно.

          Как ни вызывай, в &self будет ссылка на конкретный экземпляр типа, на котором вызывается метод.

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

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


          1. leventov
            14.05.2015 20:43

            Я совсем запутался с этими обозначениями Раста. Если я хочу динамический массив (Vec в Расте, vector в C++), который хранит ссылки на стуктуры/объекты/типы (называйте как хотите), которые лежат где-то еще (например, в куче, или аллоцированы с помощью malloc). При этом я хочу реализовать что-то типа vec.contains(elem), который бы проходился по всем элементам в векторе и вызывал их eq(). Я могу добиться того, чтобы в этом tight loop в скомпилированном коде не проверялась таблица виртуальных методов на каждой итерации? То есть хочу ссылки, но не «жирные», а «тонкие». Массив ссылок, но не абы на что, а на статически известный один и тот же тип.

            CONSTantius


            1. CONSTantius
              14.05.2015 21:16

              Вам типаж для этого вообще не нужен. Vec статически диспетчеризуется на прямой вызов .eq() у T.


              1. kstep Автор
                14.05.2015 21:21

                Да, всё верно. Пользуйтесь Vec<&MyType> и будьте счастливы!


              1. leventov
                14.05.2015 21:22

                Как не нужен. eq() это же часть типажа Eq.

                Почему статически. В статье Vec<Box<ClickCallback>> динамически.


                1. CONSTantius
                  14.05.2015 21:29

                  Типаж нужен, чтобы хранить в векторе структуры *разных* типов, все из которых реализуют этот типаж. Гипотетически, Vec<&Eq> — это вектор ссылок на структуры, все из которых умеют сравниваться, но имеют разные типы. На практике что там с чем может сравниться — непонятно, поэтому такой код может и не скомпилируется.


                  1. CONSTantius
                    14.05.2015 21:33

                    Проверил. Типаж Eq не является объекто-безопасным (насколько я понимаю, как раз потому, что это бред) — не компилируется это всё.


                  1. leventov
                    14.05.2015 21:34

                    То есть тип диспетчеризации определяется при инстанцировании типа. То есть Vec<&MyType>(10) будет занимать, скажем, какой-то заголовок + 40 байт (если ссылка 4-байтовая), а Vec<&ClickCallback>(10) — заголовок + 80 байт, если ссылка на таблицу виртуальных методов тоже 4-байтовая. Правильно?


                    1. CONSTantius
                      14.05.2015 21:37

                      Похоже на правду :)


                      1. leventov
                        14.05.2015 21:38
                        +1

                        Супер, разобрался.


        1. CONSTantius
          14.05.2015 19:50

          1. Нет, потому что, например, только &ClickCallback содержит указатель на таблицу (это жирный указатель). Насколько я понимаю, ClickCallback как типаж-значение во время исполнения вообще никак не представляется.

          А зачем это может понадобиться?