2013-01-02 5 views
10

Sto lavorando su un sistema di template scritto in Go, il che significa che richiede un uso liberale del pacchetto reflect. In questa circostanza specifica, devo essere in grado di chiamare dinamicamente un metodo su un interface{}. La stranezza è che la mia logica di riflessione funziona bene finché i miei dati sono di tipo noto, ma non se i dati sono di tipo interface{}.Chiamare dinamicamente metodo sull'interfaccia {} indipendentemente dal tipo di ricevitore

Nell'esempio seguente è possibile vedere che la logica in main() e Pass() è identica. L'unica differenza è se i dati sono un tipo conosciuto o un tipo noto all'interno di un gioco interface{}

Go: http://play.golang.org/p/FTP3wgc0sZ

package main 

import (
    "fmt" 
    "reflect" 
) 

type Test struct { 
    Start string 
} 

func (t *Test) Finish() string { 
    return t.Start + "finish" 
} 

func Pass(i interface{}) { 
    _, ok := reflect.TypeOf(&i).MethodByName("Finish") 
    if ok { 
     fmt.Println(reflect.ValueOf(&i).MethodByName("Finish").Call([]reflect.Value{})[0]) 
    } else { 
     fmt.Println("Pass() fail") 
    } 
} 

func main() { 
    i := Test{Start: "start"} 

    Pass(i) 
    _, ok := reflect.TypeOf(&i).MethodByName("Finish") 
    if ok { 
     fmt.Println(reflect.ValueOf(&i).MethodByName("Finish").Call([]reflect.Value{})[0]) 
    } else { 
     fmt.Println("main() fail") 
    } 
} 

Al momento l'esecuzione di questo codice si ottiene il seguente risultato

Pass() fail 
startfinish 

Quale significa che la mia metodologia per chiamare dinamicamente un metodo funziona bene tranne in uno scenario in cui il mio oggetto è attualmente in un interface{}.

Se invece non utilizzo un ricevitore puntatore e passaggio i allora funziona come previsto.

andare a giocare: http://play.golang.org/p/myM0UXVYzX

Questo mi porta a credere che il mio problema è che non riesco a accedere all'indirizzo di i (&i) quando si tratta di un interface{}. Ho setacciato il pacchetto di riflessione e testato cose come reflect.Value.Addr() e reflect.PtrTo() ma non riuscivo a far funzionare il modo in cui mi serviva. La mia impressione è che abbia qualcosa a che fare con il fatto che uno interface{} è per definizione un oggetto di riferimento.

risposta

16

Grazie a @Jeremy parete Credo di essere stato in grado di risolvere il mio problema. Il problema di base è chiamare un metodo denominato dinamicamente su un interface{}. Ci sono 4 casi.

  1. interface{} sottostante dati è il valore e il ricevitore è valore
  2. interface{} dati sottostanti puntatore e ricevitore è valore
  3. interface{} sottostante dati valore e il ricevitore è puntatore
  4. interface{} dati sottostante è puntatore e ricevitore pointer

Utilizzando la riflessione possiamo determinare il valore di underthe del nostro int erface. Quindi, usando un'ulteriore riflessione, possiamo generare il tipo di dati alternativo al nostro tipo corrente.Se i dati passati in è stato un valore abbiamo bisogno di generare un puntatore ad esso

value := reflect.ValueOf(data) 
if value.Type().Kind() == reflect.Ptr { 
    ptr = value 
    value = ptr.Elem() // acquire value referenced by pointer 
} else { 
    ptr = reflect.New(reflect.TypeOf(i)) // create new pointer 
    temp := ptr.Elem() // create variable to value of pointer 
    temp.Set(value) // set value of variable to our passed in value 
} 

Ora che abbiamo entrambi i tipi di dati possiamo semplicemente usare ciascuno per verificare la presenza di un metodo esistente

var finalMethod reflect.Value 
method := value.MethodByName(methodName) 
if method.IsValid() { 
    finalMethod = method 
} 
// check for method on pointer 
method = ptr.MethodByName(methodName) 
if method.IsValid() { 
    finalMethod = method 
} 

if (finalMethod.IsValid()) { 
    return finalMethod.Call([]reflect.Value{})[0].String() 
} 

Quindi con questo in mente possiamo chiamare efficacemente qualsiasi metodo, in modo dinamico, sia dichiarato come *receiver o receiver.

Proof

Pieno di concetto: http://play.golang.org/p/AU-Km5VjZs

package main 

import (
    "fmt" 
    "reflect" 
) 

type Test struct { 
    Start string 
} 

// value receiver 
func (t Test) Finish() string { 
    return t.Start + "finish" 
} 

// pointer receiver 
func (t *Test) Another() string { 
    return t.Start + "another" 
} 

func CallMethod(i interface{}, methodName string) interface{} { 
    var ptr reflect.Value 
    var value reflect.Value 
    var finalMethod reflect.Value 

    value = reflect.ValueOf(i) 

    // if we start with a pointer, we need to get value pointed to 
    // if we start with a value, we need to get a pointer to that value 
    if value.Type().Kind() == reflect.Ptr { 
     ptr = value 
     value = ptr.Elem() 
    } else { 
     ptr = reflect.New(reflect.TypeOf(i)) 
     temp := ptr.Elem() 
     temp.Set(value) 
    } 

    // check for method on value 
    method := value.MethodByName(methodName) 
    if method.IsValid() { 
     finalMethod = method 
    } 
    // check for method on pointer 
    method = ptr.MethodByName(methodName) 
    if method.IsValid() { 
     finalMethod = method 
    } 

    if (finalMethod.IsValid()) { 
     return finalMethod.Call([]reflect.Value{})[0].Interface() 
    } 

    // return or panic, method not found of either type 
    return "" 
} 

func main() { 
    i := Test{Start: "start"} 
    j := Test{Start: "start2"} 

    fmt.Println(CallMethod(i, "Finish")) 
    fmt.Println(CallMethod(&i, "Finish")) 
    fmt.Println(CallMethod(i, "Another")) 
    fmt.Println(CallMethod(&i, "Another")) 
    fmt.Println(CallMethod(j, "Finish")) 
    fmt.Println(CallMethod(&j, "Finish")) 
    fmt.Println(CallMethod(j, "Another")) 
    fmt.Println(CallMethod(&j, "Another")) 
} 
+0

Grazie. Sebbene la mia domanda fosse diversa, la tua risposta mi ha aiutato a determinare se una struct ha un metodo su di essa, con 'IsValid()'. – Matt

+0

Mi è venuto in mente questo (http://play.golang.org/p/v7lC0swB3s) quando provo a rispondere alla mia stessa domanda (http://stackoverflow.com/questions/20167935/can-embedded-methods-access-parent -campi) – Brenden

4

Nell'esempio non si chiama pass con qualcosa che supporta il metodo Finish poiché Finish è definito solo sui puntatori alle strutture Test. MethodByName sta facendo esattamente quello che dovrebbe in quel caso. *Test != Test sono due tipi completamente diversi. Nessuna quantità di riflesso trasformerà un Test in un * Test. E davvero non dovrebbe neanche. Puoi usare la funzione PtrTo per scoprire se il metodo Finish è definito sul tipo di puntatore ma che non ti aiuterà a ottenere un puntatore al valore reale.

Chiamata Pass con un puntatore funziona: http://play.golang.org/p/fICI3cqT4t

+1

La sua risposta è stata molto utile per trovare la risposta al problema. La chiave era "Nessuna quantità di riflessione trasformerà un Test in un Test", il che mi ha fatto capire che risolvere il problema si basava sulla ricerca di un modo per fare esattamente questo. Ho aggiunto la mia risposta che ritengo provi che si possa creare un '* Test' da un' Test' usando la riflessione. Questo mi permette di chiamare qualsiasi metodo su qualsiasi 'interfaccia {}' dinamicamente per nome, indipendentemente dal tipo di ricevitore. – Nucleon

+0

In effetti è possibile utilizzare la reflect api per creare un nuovo tipo che è un puntatore al valore e che funzionerà. Ma sotto la filosofia che funziona sotto di te non stai più lavorando sullo stesso tipo in cui sei passato. In ogni caso, sono contento che tu sia stato in grado di risolvere il tuo problema. –

0

Questo è un buon esempio di golang per implementare una funzionalità simile:

package main 

import "fmt" 

type Parent struct { 
    Attr1 string 
} 

type Parenter interface { 
    GetParent() Parent 
} 

type Child struct { 
    Parent //embed 
    Attr string 
} 

func (c Child) GetParent() Parent { 
    return c.Parent 
} 

func setf(p Parenter) { 
    fmt.Println(p) 
} 

func main() { 
    var ch Child 
    ch.Attr = "1" 
    ch.Attr1 = "2" 

    setf(ch) 
} 

codice: https://groups.google.com/d/msg/golang-nuts/8lK0WsGqQ-0/HJgsYW_HCDoJ

Problemi correlati