2016-12-15 35 views
6

Pisząc wywołania zwrotne dla interfejsów ogólnych, może być przydatne dla nich zdefiniowanie własnych danych lokalnych, które są odpowiedzialne za tworzenie i uzyskiwanie dostępu.Wygodny "Opcja <Box<Any>>" dostęp, gdy sukces jest zapewniony?

W CI będzie po prostu użyć void wskaźnik, C-podobny przykład:

struct SomeTool { 
    int type; 
    void *custom_data; 
}; 

void invoke(SomeTool *tool) { 
    StructOnlyForThisTool *data = malloc(sizeof(*data)); 
    /* ... fill in the data ... */ 
    tool.custom_data = custom_data; 
} 
void execute(SomeTool *tool) { 
    StructOnlyForThisTool *data = tool.custom_data; 
    if (data.foo_bar) { /* do something */ } 
} 

Pisząc coś podobnego w Rust, zastępując void * z Option<Box<Any>>, jednak jestem stwierdzenia, że ​​dostęp do danych jest nieracjonalnie gadatliwy, np:

struct SomeTool { 
    type: i32, 
    custom_data: Option<Box<Any>>, 
}; 

fn invoke(tool: &mut SomeTool) { 
    let data = StructOnlyForThisTool { /* my custom data */ } 
    /* ... fill in the data ... */ 
    tool.custom_data = Some(Box::new(custom_data)); 
} 
fn execute(tool: &mut SomeTool) { 
    let data = tool.custom_data.as_ref().unwrap().downcast_ref::<StructOnlyForThisTool>().unwrap(); 
    if data.foo_bar { /* do something */ } 
} 

jest jedna linia o której chciałbym, aby móc pisać w sposób bardziej zwarty:

  • tool.custom_data.as_ref().unwrap().downcast_ref::<StructOnlyForThisTool>().unwrap()
  • tool.custom_data.as_ref().unwrap().downcast_mut::<StructOnlyForThisTool>().unwrap()

Chociaż każda metoda ma sens, na własną rękę, w praktyce to nie jest coś, co chcę napisać w całym kodzie bazy, a nie coś, co zamierzam chcesz często pisać lub łatwo zapamiętać.

Umownie, zastosowania rozpakować tutaj nie są niebezpieczne, ponieważ:

  • Chociaż tylko niektóre narzędzia definiowania niestandardowych danych, te, które zawsze zdefiniować.
  • Gdy dane są ustawione, zgodnie z konwencją narzędzie zawsze ustawia własne dane. Nie ma więc szansy na zło danych.
  • Za każdym razem, gdy konwencje te nie są przestrzegane, jest to błąd i powinien panikować.

Biorąc pod uwagę te konwencje i zakładając, że dostęp do danych niestandardowych z narzędzia jest często wykonywany - jaki byłby dobry sposób na uproszczenie tego wyrażenia?


Możliwe opcje:

  • Zdjąć Option, wystarczy użyć Box<Any> z Box::new(()) reprezentujący None więc dostęp można uprościć trochę.
  • Użyj makra lub funkcji, aby ukryć szczegółowość - przejście w trybie Option<Box<Any>>: zadziała oczywiście, ale wolisz - nie będzie używane jako ostateczność.
  • Dodaj cechę do Option<Box<Any>>, która udostępnia taką metodę, jak tool.custom_data.unwrap_box::<StructOnlyForThisTool>() z dopasowaniem unwrap_box_mut.

Update 1): ponieważ tym pytaniem punkt nie obejmuje wydaje się istotne. Może istnieć wiele funkcji zwrotnych, takich jak execute, które muszą mieć dostęp do custom_data. W tamtym czasie nie sądziłem, że to ważne, aby zwrócić uwagę.

Aktualizacja 2): Opakowanie to w zależności, która zabierze tool nie jest możliwe, ponieważ kontroler pożyczyć następnie zapobiega dalszemu dostęp do członków tool dopóki zmienna obsada wykracza poza zakres, znalazłem tylko niezawodny sposób do zrobienia to było napisanie makra.

+5

Czy jest jakiś powód, dla którego nie używa się tylko zamknięć? Zamknięcia są tam, aby powstrzymać nas od konieczności użycia wskaźnika funkcji + pary wskaźników danych. Tak jak plasterki są po to, aby powstrzymać nas przed potrzebą wskaźnika danych + pary długości. –

+1

Co powiesz na tworzenie metod dostępu do 'SomeTool'? 'custom_data()' i 'custom_data_mut()'. –

+0

@ JorgeIsraelPeña: Ta sama reakcja tutaj; Zastanawiam się, czy czegoś brakuje, bo wydaje się oczywiste, że należy utworzyć funkcję, a nie kopiować/wklejać :( –

Odpowiedz

1

Jeśli implementacja rzeczywiście ma tylko jedną metodę o nazwie takiej jak execute, jest to mocna wskazówka, aby rozważyć użycie zamknięcia w celu przechwycenia danych implementacji. SomeTool może zawierać arbitralne wywoływanie w sposób wymazany typem przy użyciu boxed FnMut, jak pokazano in this answer. execute() następnie sprowadza się do wywołania zamknięcia przechowywanego w zamknięciu implementacji pola strukturalnego przy użyciu (self.impl_)(). Aby uzyskać bardziej ogólne podejście, będzie również działać, gdy masz więcej metod na wdrożenie, czytaj dalej.

Idiomatyczny i bezpieczny dla typu odpowiednik wzorca typu + dataptr C polega na przechowywaniu typu realizacji i wskaźnika na danych łącznie jako trait object. Struktura SomeTool może zawierać pojedyncze pole, obiekt o cechach pudełkowych SomeToolImpl, gdzie cecha określa metody specyficzne dla narzędzia, takie jak execute. To ma następujące cechy:

  • Nie trzeba już wyraźne type pole ponieważ rtti jest włączona w obiekcie cechy.

  • Wdrożenie metod cech poszczególnych narzędzi pozwala na dostęp do własnych danych w bezpieczny sposób bez rzutowania i rozpakowywania. Dzieje się tak dlatego, że vtable obiektu cechy automatycznie wywołuje prawidłową funkcję dla prawidłowej implementacji cechy i jest to błąd kompilacji podczas próby wywołania innego.

  • W „wskaźnik tłuszczu” reprezentacja obiektu cecha ma te same właściwości użytkowe jak rodzaj + dataptr para - na przykład wielkość SomeTool będą dwa wskaźniki, a dostęp do danych wdrożeniowych nadal obejmie jeden wskaźnik dereferencja.

Oto przykład realizacja:

struct SomeTool { 
    impl_: Box<SomeToolImpl>, 
} 

impl SomeTool { 
    fn execute(&mut self) { 
     self.impl_.execute(); 
    } 
} 

trait SomeToolImpl { 
    fn execute(&mut self); 
} 

struct SpecificTool1 { 
    foo_bar: bool 
} 

impl SpecificTool1 { 
    pub fn new(foo_bar: bool) -> SomeTool { 
     let my_data = SpecificTool1 { foo_bar: foo_bar }; 
     SomeTool { impl_: Box::new(my_data) } 
    } 
} 

impl SomeToolImpl for SpecificTool1 { 
    fn execute(&mut self) { 
     println!("I am {}", self.foo_bar); 
    } 
} 

struct SpecificTool2 { 
    num: u64 
} 

impl SpecificTool2 { 
    pub fn new(num: u64) -> SomeTool { 
     let my_data = SpecificTool2 { num: num }; 
     SomeTool { impl_: Box::new(my_data) } 
    } 
} 

impl SomeToolImpl for SpecificTool2 { 
    fn execute(&mut self) { 
     println!("I am {}", self.num); 
    } 
} 

pub fn main() { 
    let mut tool1: SomeTool = SpecificTool1::new(true); 
    let mut tool2: SomeTool = SpecificTool2::new(42); 
    tool1.execute(); 
    tool2.execute(); 
} 

zauważyć, że w tym projekcie, to nie ma sensu, aby wdrożenie Option bo zawsze kojarzy się z narzędziem typ z realizacją . Chociaż jest całkowicie uzasadnione, że posiada implementację bez danych, musi zawsze mieć typ skojarzony z nią.

+0

Czy to działa, gdy potrzebne są różne metody, takie jak 'execute'? – ideasman42

+0

@ ideasman42 To robi. W rzeczywistości jest to cały ten aspekt zawracania sobie głowy definiowaniem cechy; w przeciwnym razie wystarczyłby prosty "FnMut". Spróbuj, [przykład jest kompilowany] (https://play.rust-lang.org/?gist=976b179960d0322f5811af1825648f3f&version=stable&backtrace=0). – user4815162342