2010-09-20 15 views
16

In Clojure, dato un nome di classe come stringa, ho bisogno di creare una nuova istanza della classe. In altre parole, come faccio a implementare nuova istanza-da-class-name inClojure: creazione di una nuova istanza dal nome della classe String

(def my-class-name "org.myorg.pkg.Foo") 
; calls constructor of org.myorg.pkg.Foo with arguments 1, 2 and 3 
(new-instance-from-class-name my-class-name 1 2 3) 

Sto cercando una soluzione più elegante rispetto

  • chiamando il metodo Java newInstance su un costruttore dal classe
  • usando eval, carico-string, ...

In pratica, userò su classi create utilizzando defrecord. Quindi se c'è qualche sintassi speciale per quello scenario, sarei piuttosto interessato.

risposta

23

Ci sono due buoni modi per farlo. Quale è il migliore dipende dalla circostanza specifica.

Il primo è riflessione:

 
(clojure.lang.Reflector/invokeConstructor 
    (resolve (symbol "Integer")) 
    (to-array ["16"])) 

Ecco come chiamare (new Integer "16") ... includere altri argomenti ctor necessari nel vettore a-array. Questo è facile, ma più lento in fase di esecuzione rispetto all'utilizzo di new con suggerimenti di tipo sufficienti.

La seconda opzione è il più velocemente possibile, ma un po 'più complicato, e utilizza eval:

 
(defn make-factory [classname & types] 
    (let [args (map #(with-meta (symbol (str "x" %2)) {:tag %1}) types (range))] 
    (eval `(fn [[email protected]] (new ~(symbol classname) [email protected]))))) 

(def int-factory (make-factory "Integer" 'String)) 

(int-factory "42") 

Il punto chiave è il codice eval che definisce una funzione anonima, come make-factory fa. Questo è lento - più lento rispetto all'esempio di riflessione sopra, quindi eseguirlo solo raramente, ad esempio una volta per classe. Ma avendo fatto ciò hai una normale funzione Clojure che puoi memorizzare da qualche parte, in una var come int-factory in questo esempio, o in una mappa hash o in un vettore a seconda di come la userai. Indipendentemente da ciò, questa funzione di fabbrica funzionerà a piena velocità compilata, può essere sottolineata da HotSpot, ecc. E sarà sempre più veloce più veloce di rispetto all'esempio di riflessione.

Quando si trattano specificamente con le classi generate da deftype o defrecord, è possibile saltare la lista tipo dal momento che tali classi hanno sempre esattamente due ctors ognuna con diverse arietà. Questo permette qualcosa di simile:

 
(defn record-factory [recordname] 
    (let [recordclass ^Class (resolve (symbol recordname)) 
     max-arg-count (apply max (map #(count (.getParameterTypes %)) 
             (.getConstructors recordclass))) 
     args (map #(symbol (str "x" %)) (range (- max-arg-count 2)))] 
    (eval `(fn [[email protected]] (new ~(symbol recordname) [email protected]))))) 


(defrecord ExampleRecord [a b c]) 

(def example-record-factory (record-factory "ExampleRecord")) 

(example-record-factory "F." "Scott" 'Fitzgerald) 
+0

Eccellente! La seconda opzione è ovviamente una tecnica molto generale. L'ho già usato in un altro modo. – chris

4

Poiché "nuovo" è un modulo speciale, non sono sicuro che sia possibile farlo senza una macro. Ecco un modo per farlo usando una macro:

user=> (defmacro str-new [s & args] `(new ~(symbol s) [email protected])) 
#'user/str-new 
user=> (str-new "String" "LOL") 
"LOL" 

Controlla il commento di Michal sui limiti di questa macro.

+2

Si noti che questo funziona solo se il 'S' ricevuto dalla macro è una (letterale) stringa e non un'espressione arbitraria che valuta una stringa. In quest'ultimo caso, non è possibile evitare la costruzione di un'istanza riflettente o "eval". –

+0

Grazie per averlo indicato. Non ho pensato di dirlo. – Rayne

+0

Ho paura che non sia una stringa letterale. Ho modificato la domanda per riflettere questo. – chris

2

In Clojure 1.3, defrecord sarà defn automaticamente una funzione di fabbrica utilizzando il nome record con "->" anteposto. Allo stesso modo, una variante che prende una mappa sarà il nome del record preceduto da "map->".

user=> (defrecord MyRec [a b]) 
user.MyRec 
user=> (->MyRec 1 "one") 
#user.MyRec{:a 1, :b "one"} 
user=> (map->MyRec {:a 2}) 
#user.MyRec{:a 2, :b nil} 

Una macro come questo dovrebbe funzionare per creare un'istanza dal nome di stringa del tipo di record:

(defmacro newbie [recname & args] `(~(symbol (str "->" recname)) [email protected])) 
Problemi correlati