Как вы на самом деле используете динамически изменяемые типы в Rust?
Теоретически приземлились динамические типы (DST), и теперь мы должны иметь возможность использовать экземпляры динамического размера. Практически, я не могу ни заставить это работать, ни понять тесты вокруг этого.
Кажется, все вращается вокруг Sized?
ключевое слово... но как именно вы его используете?
Я могу соединить несколько типов:
// Note that this code example predates Rust 1.0
// and is no longer syntactically valid
trait Foo for Sized? {
fn foo(&self) -> u32;
}
struct Bar;
struct Bar2;
impl Foo for Bar { fn foo(&self) -> u32 { return 9u32; }}
impl Foo for Bar2 { fn foo(&self) -> u32 { return 10u32; }}
struct HasFoo<Sized? X> {
pub f:X
}
... но как мне создать экземпляр HasFoo
, который является DST, чтобы иметь либо Bar
или же Bar2
?
Попытка сделать это всегда, кажется, приводит к:
<anon>:28:17: 30:4 error: trying to initialise a dynamically sized struct
<anon>:28 let has_foo = &HasFoo {
В общем, я понимаю, что у вас не может быть голого динамического типа; Вы можете взаимодействовать только с одним указателем, но я не могу понять, как это сделать.
4 ответа
Отказ от ответственности: это только результаты нескольких экспериментов, которые я провел в сочетании с чтением блога Нико Мацакиса.
DST - это типы, размер которых не обязательно известен во время компиляции.
До ДСТ
Ломтик как [i32]
или голая черта, как IntoIterator
не были допустимыми типами объектов, потому что они не имеют известного размера.
Структура может выглядеть так:
// [i32; 2] is a fixed-sized vector with 2 i32 elements
struct Foo {
f: [i32; 2],
}
или вот так:
// & is basically a pointer.
// The compiler always knows the size of a
// pointer on a specific architecture, so whatever
// size the [i32] has, its address (the pointer) is
// a statically-sized type too
struct Foo2<'a> {
f: &'a [i32],
}
но не так:
// f is (statically) unsized, so Foo is unsized too
struct Foo {
f: [i32],
}
Это было верно и для перечислений и кортежей.
С ДСТ
Вы можете объявить структуру (или перечисление или кортеж) как Foo
выше, содержащий нестандартный тип. Тип, содержащий тип без размера, также будет иметь размер.
При определении Foo
было легко, создавая экземпляр Foo
все еще трудно и может быть изменено. Поскольку технически вы не можете создать тип без размера по определению, вы должны создать аналог размера Foo
, Например, Foo { f: [1, 2, 3] }
, Foo<[i32; 3]>
, который имеет статически известный размер и код некоторого слежения, чтобы компилятор знал, как он может привести это в свой статически нестандартный аналог Foo<[i32]>
, Способ сделать это в безопасном и стабильном Rust все еще разрабатывается, начиная с Rust 1.5 (здесь приведен RFC для приведений DST для получения дополнительной информации).
К счастью, определение нового DST - это не то, что вы, вероятно, будете делать, если только вы не создаете новый тип интеллектуального указателя (например, Rc
), что должно быть достаточно редким явлением.
Представить Rc
определяется как наш Foo
выше. Поскольку у него есть все необходимое для приведения от размера к размеру, его можно использовать для этого:
use std::rc::Rc;
trait Foo {
fn foo(&self) {
println!("foo")
}
}
struct Bar;
impl Foo for Bar {}
fn main() {
let data: Rc<Foo> = Rc::new(Bar);
// we're creating a statically typed version of Bar
// and coercing it (the :Rc<Foo> on the left-end side)
// to as unsized bare trait counterpart.
// Rc<Foo> is a trait object, so it has no statically
// known size
data.foo();
}
?Sized
связанный
Поскольку вы вряд ли создадите новый DST, для чего нужны DST в повседневном кодировании Rust? Чаще всего они позволяют вам писать общий код, который работает как с типоразмерными типами, так и с их существующими нестандартными аналогами. Чаще всего это будет Vec
/ []
ломтики или String
/ str
,
То, как вы выражаете это через ?Sized
"Граница". ?Sized
в некотором смысле противоположность границы; это на самом деле говорит, что T
может иметь размер или размер, поэтому он расширяет возможные типы, которые мы можем использовать, вместо того, чтобы ограничивать их, как это обычно делают границы.
Придуманный пример времени! Допустим, у нас есть FooSized
структура, которая просто оборачивает ссылку и простой Print
черта, которую мы хотим реализовать для этого.
struct FooSized<'a, T>(&'a T)
where
T: 'a;
trait Print {
fn print(&self);
}
Мы хотим определить общее одеяние для всех завернутых T
это реализовать Display
,
impl<'a, T> Print for FooSized<'a, T>
where
T: 'a + fmt::Display,
{
fn print(&self) {
println!("{}", self.0)
}
}
Давайте попробуем заставить это работать:
// Does not compile. "hello" is a &'static str, so self print is str
// (which is not sized)
let h_s = FooSized("hello");
h_s.print();
// to make it work we need a &&str or a &String
let s = "hello"; // &'static str
let h_s = &s; // & &str
h_s.print(); // now self is a &str
Эх... это неловко... К счастью, у нас есть способ обобщить структуру для работы непосредственно с str
(и нестандартные типы в целом): ?Sized
//same as before, only added the ?Sized bound
struct Foo<'a, T: ?Sized>(&'a T)
where
T: 'a;
impl<'a, T: ?Sized> Print for Foo<'a, T>
where
T: 'a + fmt::Display,
{
fn print(&self) {
println!("{}", self.0)
}
}
теперь это работает:
let h = Foo("hello");
h.print();
Для менее надуманного (но простого) фактического примера вы можете посмотреть на Borrow
черта в стандартной библиотеке.
Вернуться к вашему вопросу
trait Foo for ?Sized {
fn foo(&self) -> i32;
}
for ?Sized
синтаксис теперь устарел. Раньше для обозначения типа Self
, объявив, что `Foo может быть реализован типом без размера, но теперь это значение по умолчанию. Любая черта теперь может быть реализована для нестандартного типа, то есть теперь вы можете иметь:
trait Foo {
fn foo(&self) -> i32;
}
//[i32] is unsized, but the compiler does not complain for this impl
impl Foo for [i32] {
fn foo(&self) -> i32 {
5
}
}
Если вы не хотите, чтобы ваша черта была реализована для нестандартных типов, вы можете использовать Sized
обязательность:
// now the impl Foo for [i32] is illegal
trait Foo: Sized {
fn foo(&self) -> i32;
}
Чтобы исправить пример, приведенный Паоло Фалабеллой, мы рассмотрим его по-другому с использованием свойства.
struct Foo<'a, T>
where
T: 'a + ?Sized,
{
printable_object: &'a T,
}
impl<'a, T> Print for Foo<'a, T>
where
T: 'a + ?Sized + fmt::Display,
{
fn print(&self) {
println!("{}", self.printable_object);
}
}
fn main() {
let h = Foo {
printable_object: "hello",
};
h.print();
}
Вот полный пример, основанный на ответе huon . Важная хитрость заключается в том, чтобы сделать тип, который вы хотите содержать DST, универсальным типом, где размер универсального не требуется (через
?Sized
). Затем вы можете построить конкретное значение, используя
Bar1
или же
Bar2
а затем сразу конвертировать его.
struct HasFoo<F: ?Sized = dyn Foo>(F);
impl HasFoo<dyn Foo> {
fn use_it(&self) {
println!("{}", self.0.foo())
}
}
fn main() {
// Could likewise use `&HasFoo` or `Rc<HasFoo>`, etc.
let ex1: Box<HasFoo> = Box::new(HasFoo(Bar1));
let ex2: Box<HasFoo> = Box::new(HasFoo(Bar2));
ex1.use_it();
ex2.use_it();
}
trait Foo {
fn foo(&self) -> u32;
}
struct Bar1;
impl Foo for Bar1 {
fn foo(&self) -> u32 {
9
}
}
struct Bar2;
impl Foo for Bar2 {
fn foo(&self) -> u32 {
10
}
}
На данный момент, чтобы создать HasFoo
хранение стертых типов Foo
вам нужно сначала создать один с фиксированным конкретным типом, а затем привести указатель на него к форме DST, то есть
let has_too: &HasFoo<Foo> = &HasFoo { f: Bar };
призвание has_foo.f.foo()
затем делает то, что вы ожидаете.
В будущем эти DST-броски почти наверняка будут возможны с as
, но на данный момент приведение с помощью явной подсказки типа не требуется.