2010-09-20 18 views
16

In Clojure muss ich einen Klassennamen als String angeben und muss eine neue Instanz der Klasse erstellen. Mit anderen Worten, wie würde ich neue Instanz-von-class-name inClojure: Erstellen einer neuen Instanz von String Klassenname

(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) 

Suche nach einer Lösung eleganter als

  • Aufruf der Java newInstance Methode auf einem Konstruktor aus dem Gerät Klasse
  • eval, Last-String verwenden, ...

In der Praxis wird ich habe es auf Klassen defrecord werden. Wenn es also eine spezielle Syntax für dieses Szenario gibt, wäre ich sehr interessiert.

Antwort

23

Es gibt zwei gute Möglichkeiten, dies zu tun. Was am besten ist, hängt von den spezifischen Umständen ab.

Die erste ist die Reflexion:

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

, die wie (new Integer "16") Aufruf ist ... schließen alle anderen Ctor Argumente, die Sie in der To-Array Vektor benötigen. Dies ist einfach, aber zur Laufzeit langsamer als die Verwendung von new mit ausreichenden Typhinweisen.

Die zweite Option ist, so schnell wie möglich, aber ein bisschen komplizierter und verwendet 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") 

Der entscheidende Punkt ist zu eval-Code, der eine anonyme Funktion definiert, wie make-factory tut. Dies ist langsam - langsamer als das obige Beispiel der Reflexion, also nur so selten wie möglich wie einmal pro Klasse. Aber nachdem Sie das getan haben, haben Sie eine normale Clojure-Funktion, die Sie irgendwo speichern können, in einer Var wie int-factory in diesem Beispiel, oder in einer Hash-Map oder einem Vektor, je nachdem, wie Sie es verwenden werden. Ungeachtet dessen wird diese Factory-Funktion mit voller kompilierter Geschwindigkeit ausgeführt, kann von HotSpot usw. inline ausgeführt werden und wird immer viel schneller laufen als das Reflektionsbeispiel .

Wenn Sie speziell sind mit Klassen, die durch deftype oder defrecord tun haben, können Sie die Typenliste überspringen, da immer diese Klassen haben genau zwei ctors mit jeweils unterschiedlichen arities. Dies ermöglicht es so etwas wie:

 
(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

Ausgezeichnet! Die zweite Option ist offensichtlich eine sehr allgemeine Technik. Ich habe es schon anders benutzt. – chris

4

Da 'neu' ist eine spezielle Form, ich bin mir nicht sicher, dass Sie dies ohne ein Makro tun können. Hier ist ein Weg, um es mit einem Makro zu tun:

Überprüfen Sie Michals Kommentar zu den Einschränkungen dieses Makros.

+2

Beachten Sie, dass dies nur funktioniert, wenn die 'vom Makro empfängt s' ist ein (Literal) string und nicht ein beliebiger Ausdruck, der eine Zeichenkette auswertet. Im letzteren Fall gibt es keine Vermeidung von "eval" oder reflektorischer Instanzkonstruktion. –

+0

Vielen Dank, dass Sie darauf hingewiesen haben. Dachte nicht, das zu erwähnen. – Rayne

+0

Ich habe Angst, dass s keine wörtliche Zeichenfolge sein wird. Ich habe die Frage bearbeitet, um dies zu reflektieren. – chris

2

In Clojure 1.3 wird defrecord DEFN automatisch eine Fabrik-Funktion den Datensatz Namen mit "->" vorangestellt. In ähnlicher Weise wird eine Variante, die eine Karte aufnimmt, der Datensatzname sein, dem "map->" vorangestellt ist.

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} 

Ein Makro wie dies sollte eine Instanz aus dem String Namen des Datensatztypen erstellen arbeiten:

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