Generics - Wie vermeide ich redundante Methode?

28

Nehmen wir an, ich habe zwei Klassen, die so aussehen (der erste Codeblock und das allgemeine Problem beziehen sich auf C #):

class A 
{
    public int IntProperty { get; set; }
}

class B 
{
    public int IntProperty { get; set; }
}

Diese Klassen können in keiner Weise geändert werden (sie sind Teil einer Assembly eines Drittanbieters). Daher kann ich sie nicht dazu bringen, dieselbe Schnittstelle zu implementieren oder dieselbe Klasse zu erben, die dann IntProperty enthalten würde.

Ich möchte einige Logik auf die IntPropertyEigenschaft beider Klassen anwenden , und in C ++ könnte ich eine Template-Klasse verwenden, um dies ganz einfach zu tun:

template <class T>
class LogicToBeApplied
{
    public:
        void T CreateElement();

};

template <class T>
T LogicToBeApplied<T>::CreateElement()
{
    T retVal;
    retVal.IntProperty = 50;
    return retVal;
}

Und dann könnte ich so etwas machen:

LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();   

Auf diese Weise konnte ich eine einzelne generische Factory-Klasse erstellen, die sowohl für ClassA als auch für ClassB funktionieren würde.

In C # muss ich jedoch zwei Klassen mit zwei verschiedenen whereKlauseln schreiben , obwohl der Code für die Logik genau gleich ist:

public class LogicAToBeApplied<T> where T : ClassA, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

public class LogicBToBeApplied<T> where T : ClassB, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

Ich weiß, dass, wenn ich verschiedene Klassen in der whereKlausel haben möchte , sie verwandt sein müssen, dh dieselbe Klasse erben, wenn ich denselben Code in dem oben beschriebenen Sinne auf sie anwenden möchte. Es ist nur sehr ärgerlich, zwei völlig identische Methoden zu haben. Wegen der Leistungsprobleme möchte ich auch keine Reflexion verwenden.

Kann jemand einen Ansatz vorschlagen, der eleganter formuliert werden kann?

Vladimir Stokic
quelle
3
Warum verwenden Sie dafür überhaupt Generika? Diese beiden Funktionen sind nicht generisch.
Luaan
1
@Luaan Dies ist ein vereinfachtes Beispiel für eine Variation des abstrakten Factory-Musters. Stellen Sie sich vor, es gibt Dutzende von Klassen, die ClassA oder ClassB erben, und ClassA und ClassB sind abstrakte Klassen. Geerbte Klassen enthalten keine zusätzlichen Informationen und müssen instanziiert werden. Anstatt für jeden von ihnen eine Fabrik zu schreiben, entschied ich mich für die Verwendung von Generika.
Vladimir Stokic
6
Nun, Sie könnten Reflection oder Dynamics verwenden, wenn Sie sicher sind, dass sie in zukünftigen Releases nicht kaputt gehen werden.
Casey
Dies ist eigentlich meine größte Beschwerde über Generika ist, dass es das nicht tun kann.
Joshua
1
@Joshua, ich betrachte es eher als ein Problem mit Schnittstellen, die "duck typing" nicht unterstützen.
Ian

Antworten:

49

Fügen Sie eine Proxy-Schnittstelle hinzu (manchmal Adapter genannt , gelegentlich mit geringfügigen Unterschieden), implementieren Sie sie LogicToBeAppliedim Hinblick auf den Proxy und fügen Sie dann eine Möglichkeit hinzu, eine Instanz dieses Proxys aus zwei Lambdas zu erstellen: eine für die Eigenschaft get und eine für die Menge.

interface IProxy
{
    int Property { get; set; }
}
class LambdaProxy : IProxy
{
    private Function<int> getFunction;
    private Action<int> setFunction;
    int Property
    {
        get { return getFunction(); }
        set { setFunction(value); }
    }
    public LambdaProxy(Function<int> getter, Action<int> setter)
    {
        getFunction = getter;
        setFunction = setter;
    }
}

Wenn Sie nun ein IProxy übergeben müssen, aber eine Instanz der Klassen von Drittanbietern haben, können Sie einfach einige Lambdas übergeben:

A a = new A();
B b = new B();
IProxy proxyA = new LambdaProxy(() => a.Property, (val) => a.Property = val);
IProxy proxyB = new LambdaProxy(() => b.Property, (val) => b.Property = val);
proxyA.Property = 12; // mutates the proxied `a` as well

Darüber hinaus können Sie einfache Hilfsprogramme schreiben, um LamdaProxy-Instanzen aus Instanzen von A oder B zu erstellen. Dies können sogar Erweiterungsmethoden sein, um Ihnen einen "fließenden" Stil zu verleihen:

public static class ProxyExtension
{
    public static IProxy Proxied(this A a)
    {
      return new LambdaProxy(() => a.Property, (val) => a.Property = val);
    }

    public static IProxy Proxied(this B b)
    {
      return new LambdaProxy(() => b.Property, (val) => b.Property = val);
    }
}

Und jetzt sieht die Konstruktion von Proxies so aus:

IProxy proxyA = new A().Proxied();
IProxy proxyB = new B().Proxied();

Was Ihre Fabrik betrifft, würde ich sehen, ob Sie sie in eine "Haupt" -Fabrikmethode umgestalten können, die ein IProxy akzeptiert und alle Logik darauf und andere Methoden ausführt, die nur übergeben werden, new A().Proxied()oder new B().Proxied():

public class LogicToBeApplied
{
    public A CreateA() {
      A a = new A();
      InitializeProxy(a.Proxied());
      return a; // or maybe return the proxy if you'd rather use that
    }

    public B CreateB() {
      B b = new B();
      InitializeProxy(b.Proxied());
      return b;
    }

    private void InitializeProxy(IProxy proxy)
    {
        proxy.IntProperty = 50;
    }
}

Es gibt keine Möglichkeit, das Äquivalent Ihres C ++ - Codes in C # zu erstellen, da C ++ - Vorlagen auf struktureller Typisierung beruhen . Solange zwei Klassen denselben Methodennamen und dieselbe Signatur haben, können Sie diese Methode in C ++ für beide generisch aufrufen. C # hat eine nominelle Typisierung - der Name einer Klasse oder Schnittstelle ist Teil ihres Typs. Daher können die Klassen Aund Bnicht in irgendeiner Form gleich behandelt werden, es sei denn, eine explizite "is a" -Beziehung wird durch Vererbung oder Schnittstellenimplementierung definiert.

Wenn das Boilerplate für die Implementierung dieser Methoden pro Klasse zu groß ist, können Sie eine Funktion schreiben, die ein Objekt übernimmt und reflektierend eine erstellt, LambdaProxyindem Sie nach einem bestimmten Eigenschaftsnamen suchen:

public class ReflectiveProxier 
{
    public object proxyReflectively(object proxied)
    {
        PropertyInfo prop = proxied.GetType().GetProperty("Property");
        return new LambdaProxy(
            () => prop.GetValue(proxied),
            (val) => prop.SetValue(proxied, val));
     }
}

Dies scheitert abgrundtief, wenn Objekte eines falschen Typs gegeben werden. Reflexion führt inhärent zu der Möglichkeit von Fehlern, die das C # -System nicht verhindern kann. Glücklicherweise können Sie Reflexionen vermeiden, bis der Wartungsaufwand der Helfer zu groß wird, da Sie die IProxy-Schnittstelle oder die LambdaProxy-Implementierung nicht ändern müssen, um den reflektierenden Zucker hinzuzufügen.

Ein Grund dafür ist, dass LambdaProxyes "maximal generisch" ist. Es kann jeden Wert anpassen, der den "Geist" des IProxy-Vertrags implementiert, da die Implementierung von LambdaProxy vollständig durch die angegebenen Get- und Setter-Funktionen definiert ist. Es funktioniert sogar, wenn die Klassen unterschiedliche Namen für die Eigenschaft haben oder unterschiedliche Typen, die sinnvoll und sicher als ints darstellbar sind, oder wenn es eine Möglichkeit gibt, das Konzept, Propertydas dargestellt werden soll, auf andere Merkmale der Klasse abzubilden . Die Indirektion durch die Funktionen bietet Ihnen maximale Flexibilität.

Jack
quelle
Ein sehr interessanter Ansatz, der definitiv zum Aufrufen der Funktionen verwendet werden kann. Kann er jedoch auch für die Factory verwendet werden, in der ich tatsächlich Objekte der Klassen A und B erstellen muss?
Vladimir Stokic
@VladimirStokic Siehe Änderungen, ich habe dies ein bisschen erweitert
Jack
Bei dieser Methode müssen Sie die Eigenschaft für jeden Typ mit der zusätzlichen Möglichkeit eines Laufzeitfehlers explizit zuordnen, wenn Ihre Zuordnungsfunktion fehlerhaft ist
Ewan,
Könnten Sie alternativ zum ReflectiveProxiereinen einen Proxy mit dem dynamicSchlüsselwort erstellen ? Mir scheint, Sie hätten dieselben grundlegenden Probleme (dh Fehler, die nur zur Laufzeit auftreten), aber die Syntax und Wartbarkeit wären viel einfacher.
Bobson
1
@ Jack - Fair genug. Ich habe meine eigene Antwort hinzugefügt, um es zu demonstrieren. Es ist eine sehr nützliche Funktion, unter bestimmten seltenen Umständen (wie dieser).
Bobson
12

Im Folgenden finden Sie einen Überblick über die Verwendung von Adaptern ohne Vererbung von A und / oder B mit der Möglichkeit, sie für vorhandene A- und B-Objekte zu verwenden:

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : IAdapter
{
    A _a;

    public AAdapter()  // use this if you want to have the "logic" part create new objects
    {
        _a=new A();
    }

    public AAdapter(A a) // if you need an adapter for an existing object afterwards
    {
       _a=a;
    }

    public int Property
    {
        get { return _a.Property; }
        set { _a.Property = value; }
    }

    public A {get{return _a; } } // to provide access for non-generic code
}

class BAdapter 
{
     // analogously
}

Normalerweise würde ich diese Art von Objektadapter den Klassenproxys vorziehen. Sie vermeiden hässliche Probleme, die bei der Vererbung auftreten können. Diese Lösung funktioniert beispielsweise auch, wenn A und B versiegelte Klassen sind.

Doc Brown
quelle
Warum new int Property? Sie beschatten nichts.
Pinkfloydx33
@ pinkfloydx33: nur ein Tippfehler, geändert, danke.
Doc Brown
9

Sie könnten ClassAund ClassBüber eine gemeinsame Schnittstelle anpassen . Auf diese Weise LogicAToBeAppliedbleibt Ihr Code in unverändert. Nicht viel anders als das, was du hast.

class A
{
    public int Property { get; set; }
}
class B
{
    public int Property { get; set; }
}

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }
devnull
quelle
1
+1 unter Verwendung des Adaptermusters ist hier die traditionelle OOP-Lösung. Es ist eher ein Adapter als ein Proxy, da wir die A, B-Typen an eine gemeinsame Schnittstelle anpassen . Der große Vorteil ist, dass wir die gemeinsame Logik nicht duplizieren müssen. Der Nachteil ist, dass die Logik jetzt den Wrapper / Proxy anstelle des tatsächlichen Typs instanziiert.
Am
5
Das Problem bei dieser Lösung ist, dass Sie nicht einfach zwei Objekte vom Typ A und B nehmen, sie irgendwie in AProxy und BProxy konvertieren und dann LogicToBeApplied auf sie anwenden können. Dieses Problem kann durch Verwendung von Aggregation anstelle von Vererbung gelöst werden (bzw. Implementieren der Proxy-Objekte nicht durch Ableiten von A und B, sondern durch Verwenden eines Verweises auf A- und B-Objekte). Wieder ein Beispiel dafür, wie die falsche Verwendung der Vererbung Probleme verursacht.
Doc Brown
@ DocBrown Wie würde das in diesem speziellen Fall gehen?
Vladimir Stokic
1
@Jack: Diese Art von Lösung ist sinnvoll, wenn LogicToBeAppliedsie eine bestimmte Komplexität aufweist und sollte unter keinen Umständen an zwei Stellen in der Codebasis wiederholt werden. Dann ist der zusätzliche Kesselschildcode oft vernachlässigbar.
Doc Brown
1
@ Jack Wo ist die Redundanz? Die beiden Klassen haben keine gemeinsame Schnittstelle. Sie erstellen Wrapper , die Sie eine gemeinsame Schnittstelle haben. Sie verwenden diese gemeinsame Schnittstelle, um Ihre Logik zu implementieren. Es ist nicht so, als gäbe es im C ++ - Code nicht dieselbe Redundanz - sie steckt nur hinter ein wenig Code-Generierung. Wenn Sie der Meinung sind, dass Dinge gleich aussehen , obwohl sie nicht gleich sind, können Sie immer T4s oder ein anderes Template-System verwenden.
Luaan
8

Die C ++ - Version funktioniert nur, weil die Vorlagen "Static Duck Typing" verwenden - alles wird kompiliert, solange der Typ die richtigen Namen enthält. Es ist eher ein Makrosystem. Das generische System von C # und anderen Sprachen funktioniert sehr unterschiedlich.

Die Antworten von devnull und Doc Brown zeigen, wie das Adaptermuster verwendet werden kann, um Ihren Algorithmus allgemein zu halten und trotzdem mit beliebigen Typen zu arbeiten ... mit ein paar Einschränkungen. Insbesondere erstellen Sie jetzt einen anderen Typ, als Sie tatsächlich möchten.

Mit ein wenig Trick ist es möglich, genau den beabsichtigten Typ ohne Änderungen zu verwenden. Jetzt müssen wir jedoch alle Interaktionen mit dem Zieltyp in eine separate Schnittstelle extrahieren. Hier sind diese Wechselwirkungen Konstruktion und Eigentumszuweisung:

interface IInteractions<T> {
  T Instantiate();
  void AssignProperty(T target, int value);
}

In einer OOP-Interpretation wäre dies ein Beispiel für das Strategiemuster , obwohl es mit Generika gemischt ist.

Wir können dann Ihre Logik umschreiben, um diese Interaktionen zu verwenden:

public class LogicBToBeApplied<T>
{
    public T CreateElement(IInteractions<T> interactions)
    {
        T retVal = interactions.Instantiate();
        interactions.AssignProperty(retVal, 50);
        return retVal;
    }
}

Die Interaktionsdefinitionen sehen folgendermaßen aus:

class Interactions_ClassA : IInteractions<ClassA> {
  public override ClassA Instantiate() { return new ClassA(); }
  public override void AssignProperty(ClassA target, int value) { target.IntProperty = value; }
}

Der große Nachteil dieses Ansatzes besteht darin, dass der Programmierer beim Aufrufen der Logik eine Interaktionsinstanz schreiben und übergeben muss. Dies ähnelt weitgehend Lösungen auf der Basis von Adaptermustern, ist jedoch etwas allgemeiner.

Meiner Erfahrung nach können Sie dies am ehesten mit Vorlagenfunktionen in anderen Sprachen erreichen. Ähnliche Techniken werden in Haskell, Scala, Go und Rust verwendet, um Schnittstellen außerhalb einer Typdefinition zu implementieren. In diesen Sprachen greift der Compiler jedoch implizit ein und wählt die richtige Interaktionsinstanz aus, sodass das zusätzliche Argument nicht angezeigt wird. Dies ähnelt auch den Erweiterungsmethoden von C #, ist jedoch nicht auf statische Methoden beschränkt.

amon
quelle
Interessanter Ansatz. Nicht das, was meine erste Wahl wäre, aber ich denke, es kann einige Vorteile haben, wenn ich ein Framework oder ähnliches schreibe.
Doc Brown
8

Wenn Sie wirklich Vorsicht walten lassen möchten, können Sie den Compiler mit "dynamic" veranlassen, sich um die gesamte Überlegungsfiesheit zu kümmern. Dies führt zu einem Laufzeitfehler, wenn Sie ein Objekt an SetSomeProperty übergeben, das keine Eigenschaft namens SomeProperty hat.

using System;

namespace ConsoleApplication3
{
    class A
    {
        public int SomeProperty { get; set; }
    }

    class B
    {
        public int SomeProperty { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            var b = new B();

            SetSomeProperty(a, 7);
            SetSomeProperty(b, 12);

            Console.WriteLine($"a.SomeProperty = {a.SomeProperty}, b.SomeProperty = {b.SomeProperty}");
        }

        static void SetSomeProperty(dynamic obj, int value)
        {
            obj.SomeProperty = value;
        }
    }
}
Alter Furz
quelle
4

Die anderen Antworten identifizieren das Problem korrekt und bieten praktikable Lösungen. C # unterstützt (im Allgemeinen) nicht "Enten-Typisierung" ("Wenn es wie eine Ente läuft ..."), daher gibt es keine Möglichkeit, Ihre zu erzwingen ClassAund ClassBaustauschbar zu sein, wenn sie nicht auf diese Weise entworfen wurden.

Wenn Sie jedoch bereits bereit sind, das Risiko eines Laufzeitfehlers in Kauf zu nehmen, gibt es eine einfachere Antwort als die Verwendung von Reflection.

C # hat das dynamicSchlüsselwort, das für Situationen wie diese perfekt ist. Es sagt dem Compiler "Ich werde erst zur Laufzeit (und vielleicht auch dann nicht) wissen, welcher Typ dies ist, also erlaube mir, überhaupt etwas dagegen zu tun ".

Auf diese Weise können Sie genau die gewünschte Funktion erstellen:

public class LogicToBeApplied<T> where T : new()
{
    public static T CreateElement()
    {
        dynamic retVal = new T(); // This doesn't care what type T is.
        retVal.IntProperty = 50;  // This will fail at runtime if there is no "IntProperty" 
                                  // or it doesn't accept an int.
        return retVal;            // Once again, we don't care what it is.
    }
}

Beachten Sie auch die Verwendung des staticSchlüsselworts. Damit können Sie dies verwenden als:

A classAElement = LogicToBeApplied<A>.CreateElement();
B classBElement = LogicToBeApplied<B>.CreateElement();

Die dynamicVerwendung von Reflection hat keine Auswirkungen auf die Gesamtleistung , ebenso wenig wie die einmalige Auswirkung (und die zusätzliche Komplexität) der Verwendung von Reflection. Wenn Ihr Code zum ersten Mal auf einen dynamischen Anruf mit einem bestimmten Typ trifft, entsteht ein geringer Overhead . Wiederholte Anrufe sind jedoch genauso schnell wie der Standardcode. Aber Sie werden erhalten ein , RuntimeBinderExceptionwenn Sie versuchen , in etwas zu übergeben, die diese Eigenschaft nicht hat, und es gibt keine gute Möglichkeit , dass vor der Zeit zu überprüfen. Möglicherweise möchten Sie diesen Fehler auf nützliche Weise behandeln.

Bobson
quelle
Dies kann langsam sein, aber langsamer Code ist oft kein Problem.
Ian
@ Ian - Guter Punkt. Ich habe ein bisschen mehr über die Leistung hinzugefügt. Es ist nicht so schlimm, wie man denkt, vorausgesetzt, Sie verwenden dieselben Klassen an denselben Stellen.
Bobson
Denken Sie daran, in C ++ - Vorlagen haben Sie nicht einmal den Overhead von virtuellen Methoden!
Ian
2

Sie können Reflection verwenden, um die Eigenschaft nach Namen abzurufen.

public class logic 
{
    public object getNew<T>() where T : new()
    {
        T ret = new T();
        try
        {
            var property = typeof(T).GetProperty("IntProperty");
            if (property != null && property.PropertyType == typeof(int))
            {
                property.SetValue(ret, 50);
            }
        }
        catch (AmbiguousMatchException)
        {
            //hmm..
        }
        return ret;
    }
}

Offensichtlich riskieren Sie mit dieser Methode einen Laufzeitfehler. Welches ist, was C # versucht, Sie zu stoppen.

Ich habe irgendwo gelesen, dass Sie mit einer zukünftigen Version von C # Objekte als Schnittstelle übergeben können, die sie nicht erben, aber zusammenpassen. Welches würde auch Ihr Problem lösen.

(Ich werde versuchen, den Artikel auszugraben)

Eine andere Methode, obwohl ich nicht sicher bin, ob sie Ihnen Code erspart, besteht darin, sowohl A als auch B zu unterklassifizieren und auch eine Schnittstelle mit IntProperty zu erben.

public interface IIntProp {
    public int IntProperty {get, set}
}

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}
Ewan
quelle
Die Möglichkeit von Laufzeitfehlern und Leistungsproblemen sind die Gründe, warum ich mich nicht mit Reflektion beschäftigen wollte. Ich bin jedoch sehr daran interessiert, den Artikel zu lesen, den Sie in Ihrer Antwort erwähnt haben. Ich freue mich darauf, es zu lesen.
Vladimir Stokic
1
Gehen Sie mit Ihrer c ++ - Lösung doch das gleiche Risiko ein?
Ewan
4
@Ewan nein, c ++ sucht beim Kompilieren nach dem Mitglied
Caleth
Reflexion bedeutet Optimierungsprobleme und (noch wichtiger) schwer zu debuggende Laufzeitfehler. Vererbung und eine gemeinsame Schnittstelle bedeuten, dass für jede einzelne dieser Klassen eine Unterklasse im Voraus deklariert wird (keine Möglichkeit, eine anonym vor Ort zu erstellen). Sie funktionieren nicht, wenn nicht jedes Mal derselbe Eigenschaftsname verwendet wird.
Jack
1
@Jack es gibt Nachteile, aber bedenken Sie, dass Reflektion in Mappern, Serialisierern, Abhängigkeitsinjektions-Frameworks usw. ausgiebig verwendet wird und dass das Ziel darin besteht, es mit der geringsten Menge an Code-Duplizierung zu tun
Ewan
0

Ich wollte nur implicit operatorConversions zusammen mit dem Delegate / Lambda-Ansatz von Jacks Antwort verwenden. Aund Bsind wie angenommen:

// A and B are mutable reference types

class A
{
  public int IntProperty { get; set; }
}

class B
{
  public int IntProperty { get; set; }
}

Dann ist es einfach, eine nette Syntax mit impliziten benutzerdefinierten Konvertierungen zu erhalten (keine Erweiterungsmethoden oder ähnliches erforderlich):

// Adapter is an immutable type. However, the delegate instances have a captured reference to an A or a B (closure semantics)
struct Adapter
{
  readonly Func<int> getter;
  readonly Action<int> setter;

  Adapter(Func<int> getter, Action<int> setter)
  {
    this.getter = getter;
    this.setter = setter;
  }

  public int IntProperty
  {
    get { return getter(); }
    set { setter(value); }
  }

  public static implicit operator Adapter(A a) => new Adapter(() => a.IntProperty, x => a.IntProperty = x);
  public static implicit operator Adapter(B b) => new Adapter(() => b.IntProperty, x => b.IntProperty = x);

  public A CloneToA() => new A { IntProperty = getter(), };
  public B CloneToB() => new B { IntProperty = getter(), };
}

Anwendungsbeispiel:

class LogicToBeApplied
{
  public static A CreateA()
  {
    var a = new A();
    Initialize(a);
    return a;
  }
  public static B CreateB()
  {
    var b = new B();
    Initialize(b);
    return b;
  }

  static void Initialize(Adapter a)
  {
    a.IntProperty = 50;
  }
}

Die InitializeMethode zeigt, wie Sie damit arbeiten können, Adapterohne sich darum zu kümmern, ob es sich um ein Aoder ein Banderes Objekt handelt. Die invokations der InitializeMethode zeigt , dass wir brauchen keine (sichtbaren) gegossen oder .AsProxy()oder ähnlich zu behandeln Beton Aoder Bals ein Adapter.

Überlegen Sie, ob Sie eine ArgumentNullExceptionbenutzerdefinierte Konvertierung durchführen möchten, wenn das übergebene Argument eine Nullreferenz ist oder nicht.

Jeppe Stig Nielsen
quelle