
Size: a a a
#[cfg(...)]
) в Rust позволяет использовать условные выражения, но не в очень эргономичном виде: через all
, any
и not
. Библиотека efg позволяет использовать более привычный синтаксис, похожий на то, что пишут в коде на самом Rust. Пример:#[efg(feature "proc-macro" && !(target_arch "wasm32" && target_os "unknown"))]
extern crate proc_macro;
Кстати, написана Толяном.struct OptionA;
struct OptionB;
trait Param {
fn op();
}
impl Param for OptionA { ... }
impl Param for OptionB { ... }
struct Parametrised<P> {
data: Data,
_param: PhantomData<P>,
}
impl<P: Param> Parametrised<P> {
fn func(&mut self) {
self.data.process_foo();
P::op();
self.data.process_bar();
}
}
Вроде выглядит нормально. Однако тут есть подвох: авто-трейты, в отличие от дерайвов для большинства трейтов, смотрят не на типовые параметры, а на типы самих полей. В число авто-трейтов входят также Send
и Sync
. PhantomData (не) имеет те же реализации Send
/Sync
, что и тИповый параметр, которым параметризован. К чему это приводит? Да к тому, что если Parametrised<P>
будет использован в контексте, когда выполнение этих трейтов имеет значение, нам придётся вешать ограничение P: Send/P: Sync
, даже не смотря на то, что значения этих типов не используются вообще. Хуже, это придётся писать даже в том случае, если Data
реализует Send
/Sync
. Что мы можем с этим сделать?_param
:struct Parametrised<P> {
data: Data,
_param: PhantomData<fn(P)>, // <--
}
Таким образом, мы всё ещё включаем тип P
в описание типа Parametrised
, но так как все функциональные указателя безусловно удовлетворяют Send
и Sync
, паразитные ограничения более не возникают.T
, но не включает его напрямую. Использование PhantomData<T>
может быть неправильным в том смысле, что оно показывает, что наш тип логически владеет значением типа T
, что не обязательно является правдой. Более того, так как из-за лайфтаймов на типах в Rust есть отношение субтипизации, в полный рост встают проблемы вариантности.PhantomData
, параметризованных *const T
и *mut T
. Этот подход опирается на тот факт, что *const T
ковариантен по T
, а *mut T
контравариантен по T
, и оба типа не требуют лайфтаймы. Однако тут легко упустить тот момент, что оба типа примитивных указателей безусловно не реализуют ни Send
, ни Sync
, что вкупе с протеканием автотрейтов через PhantomData
может привести к тем же паразитным ограничениям. Причём этот случай это даже хуже, поскольку требует написания реализаций Send
и Sync
вручную, а эти имплы очень легко могут стать неправильными при внесении изменений в код.Send
/Sync
только на релевантные поля, мы опять воспользуемся тем фактом, что функциональные указатели реализуют Send
и Sync
безусловно, а так же тем фактом, что функциональные типы ковариантны по типу результата и контрвариантны по типу аргумента. Комбинируя эти типы с PhantomData
, мы можем получить нужную нам вариантность без головной боли, связанной с потокобезопасностью. Проиллюстрирую это псевдонимами типов:type Covariant<T> = PhantomData<fn() -> T>;
type Contravariant<T> = PhantomData<fn(T)>;
// ко- и контрвариантность — это про наложение ограничений,
// поэтому совмещение этих требований приводит к
// более строгому отношению: инвариантности (в смысле вариантности)
type Invariant<T> = PhantomData<fn(T) -> T>;