Beispiel für Kovarianz und Kontravarianz in der realen Welt

162

Ich habe ein wenig Probleme zu verstehen, wie ich Kovarianz und Kontravarianz in der realen Welt verwenden würde.

Bisher waren die einzigen Beispiele, die ich gesehen habe, das gleiche alte Array-Beispiel.

object[] objectArray = new string[] { "string 1", "string 2" };

Es wäre schön, ein Beispiel zu sehen, mit dem ich es während meiner Entwicklung verwenden könnte, wenn ich sehen könnte, dass es an anderer Stelle verwendet wird.

Rasierer
quelle
1
Ich untersuche die Kovarianz in dieser Antwort auf (meine eigene) Frage: Kovarianztypen: anhand eines Beispiels . Ich denke, Sie werden es interessant und hoffentlich lehrreich finden.
Cristian Diaconescu

Antworten:

109

Angenommen, Sie haben eine Klasse Person und eine Klasse, die sich daraus ableitet, Lehrer. Sie haben einige Operationen, die ein IEnumerable<Person>als Argument verwenden. In Ihrer Schulklasse haben Sie eine Methode, die eine zurückgibt IEnumerable<Teacher>. Mit der Kovarianz können Sie dieses Ergebnis direkt für die Methoden verwenden, die IEnumerable<Person>einen weniger abgeleiteten (allgemeineren) Typ durch einen abgeleiteten Typ ersetzen. Kontravarianz ermöglicht es Ihnen, entgegen der Intuition einen allgemeineren Typ zu verwenden, bei dem ein stärker abgeleiteter Typ angegeben wird.

Siehe auch Kovarianz und Kontravarianz in Generika auf MSDN .

Klassen :

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

Verwendung :

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());
Tvanfosson
quelle
14
@FilipBartuzi - wenn Sie, wie ich, als ich diese Antwort schrieb, an einer Universität beschäftigt waren, die ein Beispiel aus der Praxis ist.
Tvanfosson
5
Wie kann dies als Antwort markiert werden, wenn die Frage nicht beantwortet wird und kein Beispiel für die Verwendung der Co / Contra-Varianz in c # angegeben wird?
Barakcaf
@barakcaf hat ein Beispiel für Kontravarianz hinzugefügt. Ich bin mir nicht sicher, warum Sie das Beispiel der Kovarianz nicht gesehen haben - vielleicht mussten Sie den Code nach unten scrollen -, aber ich habe einige Kommentare dazu hinzugefügt.
Tvanfosson
@tvanfosson der Code verwendet co / contra, ich erwähne, dass es nicht zeigt, wie man es deklariert. Das Beispiel zeigt nicht die Verwendung von In / Out in der generischen Deklaration, während die andere Antwort dies tut.
Barakcaf
Wenn ich es richtig verstehe, ist Kovarianz das, was Liskovs Substitutionsprinzip in C # ermöglicht. Ist das richtig?
Miguel Veloso
136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

Zur Vollständigkeit…

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??
Marcelo Cantos
quelle
138
Ich mag dieses realistische Beispiel. Ich habe letzte Woche gerade einen Esel-Fress-Code geschrieben und war so froh, dass wir jetzt Kovarianz haben. :-)
Eric Lippert
4
Dieser Kommentar oben mit @javadba, der THE EricLippert erzählt, was Kovarianz und Kontravarianz ist, ist ein realistisches kovariantes Beispiel dafür, wie ich meiner Oma erzähle, wie man Eier saugt! : p
iAteABug_And_iLiked_it
1
Bei der Frage wurde nicht gefragt, was Kontravarianz und Kovarianz bewirken können , sondern warum Sie sie verwenden müssen . Ihr Beispiel ist alles andere als praktisch, da es auch nicht erforderlich ist. Ich kann einen QuadrupedGobbler erstellen und ihn als sich selbst behandeln (ihn IGobbler <Quadruped> zuweisen) und er kann immer noch Esel verschlingen (ich kann einen Esel an die Gobble-Methode übergeben, für die ein Quadruped erforderlich ist). Keine Kontravarianz erforderlich. Das ist cool , dass wir können eine QuadrupedGobbler als DonkeyGobbler behandeln, aber warum sollten wir müssen in diesem Fall, wenn ein QuadrupedGobbler bereits Donkeys verschlingen kann?
Wired_in
1
@wired_in Denn wenn Sie sich nur für Esel interessieren, kann es Ihnen im Weg stehen, allgemeiner zu sein. Wenn Sie beispielsweise eine Farm haben, die Esel zum Verschlingen liefert, können Sie dies als ausdrücken void feed(IGobbler<Donkey> dg). Wenn Sie stattdessen einen IGobbler <Quadruped> als Parameter verwenden, können Sie keinen Drachen übergeben, der nur Esel frisst.
Marcelo Cantos
1
Waaay spät zur Party, aber dies ist ungefähr das beste schriftliche Beispiel, das ich um SO gesehen habe. Macht Sinn, während man lächerlich ist. Ich werde mein Spiel mit Antworten verbessern müssen ...
Jesse Williams
120

Folgendes habe ich zusammengestellt, um den Unterschied zu verstehen

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant
CSharper
quelle
10
Dies ist das Beste, was ich bisher gesehen habe, das klar und prägnant ist. Großartiges Beispiel!
Rob L
6
Wie kann die Frucht (im ContravarianceBeispiel) auf Apfel niedergeschlagen werden, wenn Fruitder Elternteil von ist Apple?
Tobias Marschall
@TobiasMarschall bedeutet, dass Sie mehr über "Polymorphismus" lernen müssen
snr
56

Die Schlüsselwörter in und out steuern die Casting-Regeln des Compilers für Schnittstellen und Delegaten mit generischen Parametern:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class
Jack
quelle
Angenommen, Fisch ist ein Subtyp von Tier. Tolle Antwort übrigens.
Rajan Prasad
48

Hier ist ein einfaches Beispiel mit einer Vererbungshierarchie.

Angesichts der einfachen Klassenhierarchie:

Geben Sie hier die Bildbeschreibung ein

Und im Code:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

Invarianz (dh generische Typparameter * nicht * verziert mit inoder outSchlüsselwörter)

Scheinbar eine Methode wie diese

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

... sollte eine heterogene Sammlung akzeptieren: (was es tut)

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

Das Übergeben einer Sammlung eines abgeleiteten Typs schlägt jedoch fehl!

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

Warum? Da der generische Parameter IList<LifeForm>nicht kovariant ist - er IList<T>ist invariant, IList<LifeForm>akzeptiert er nur Sammlungen (die IList implementieren), in denen der parametrisierte Typ sein Tmuss LifeForm.

Wenn die Methodenimplementierung von PrintLifeFormsböswillig war (aber dieselbe Methodensignatur hat), wird der Grund, warum der Compiler das Übergeben verhindert, List<Giraffe>offensichtlich:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

Da IListdas Hinzufügen oder Entfernen von Elementen möglich ist, LifeFormkönnte dem Parameter eine beliebige Unterklasse von hinzugefügt werden lifeForms, die den Typ einer an die Methode übergebenen Sammlung abgeleiteter Typen verletzen würde. (Hier würde die böswillige Methode versuchen, ein Zebrazu hinzuzufügen var myGiraffes). Glücklicherweise schützt uns der Compiler vor dieser Gefahr.

Kovarianz (generisch mit parametrisiertem Typ verziert mit out)

Kovarianz wird häufig bei unveränderlichen Sammlungen verwendet (dh wenn neue Elemente nicht zu einer Sammlung hinzugefügt oder daraus entfernt werden können).

Die Lösung für das obige Beispiel besteht darin, sicherzustellen, dass ein kovarianter generischer Sammlungstyp verwendet wird, z. B. IEnumerable(definiert als IEnumerable<out T>). IEnumerableEs gibt keine Methoden zum Ändern der Sammlung. Aufgrund der outKovarianz LifeFormkann jetzt jede Sammlung mit dem Subtyp von an die Methode übergeben werden:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeFormskann nun mit genannt werden Zebras, Giraffesund jede IEnumerable<>von jeder Unterklasse vonLifeForm

Kontravarianz (generisch mit parametrisiertem Typ verziert mit in)

Kontravarianz wird häufig verwendet, wenn Funktionen als Parameter übergeben werden.

Hier ist ein Beispiel für eine Funktion, die a Action<Zebra>als Parameter verwendet und es für eine bekannte Instanz eines Zebras aufruft:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

Wie erwartet funktioniert dies einwandfrei:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

Intuitiv wird dies fehlschlagen:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

Dies ist jedoch erfolgreich

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

und auch das gelingt:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

Warum? Weil Actiondefiniert ist als Action<in T>, dh es ist contravariant, was bedeutet, dass für Action<Zebra> myAction, das myActionkann höchstens a sein Action<Zebra>, aber weniger abgeleitete Oberklassen von Zebrasind auch akzeptabel.

Obwohl dies zunächst möglicherweise nicht intuitiv ist (z. B. wie kann ein Action<object>Parameter als Parameter übergeben werden Action<Zebra>?), Werden Sie beim Entpacken der Schritte feststellen, dass die aufgerufene Funktion ( PerformZebraAction) selbst für die Übergabe von Daten verantwortlich ist (in diesem Fall eine ZebraInstanz) ) zur Funktion - die Daten stammen nicht aus dem aufrufenden Code.

Aufgrund des umgekehrten Ansatzes, Funktionen höherer Ordnung auf diese Weise zu verwenden, wird zum Zeitpunkt des ActionAufrufs der ZebraInstanz die stärker abgeleitete Instanz für die zebraActionFunktion aufgerufen (als Parameter übergeben), obwohl die Funktion selbst einen weniger abgeleiteten Typ verwendet.

StuartLC
quelle
7
Dies ist eine großartige Erklärung für die verschiedenen Varianzoptionen, da das Beispiel erläutert wird und auch klargestellt wird, warum der Compiler ohne die Schlüsselwörter in / out einschränkt oder zulässt
Vikhram,
Wo wird das inSchlüsselwort für die Kontravarianz verwendet ?
Javadba
@javadba oben Action<in T>und Func<in T, out TResult>sind im Eingabetyp kontravariant. (Meine Beispiele verwenden vorhandene invariante (Liste), kovariante (IEnumerable) und kontravariante (Action, Func) Typen)
StuartLC
Ok, das tue ich nicht C#, würde das nicht wissen.
Javadba
Es ist in Scala ziemlich ähnlich, nur eine andere Syntax - [+ T] wäre in T kovariant, [-T] wäre in T kontravariant, Scala kann auch die Einschränkung "zwischen" und die promiskuitive Unterklasse "Nichts" erzwingen, die C # hat nicht.
StuartLC
32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

Grundsätzlich konnten Sie, wenn Sie eine Funktion hatten, die eine Aufzählung eines Typs verwendet, eine Aufzählung eines abgeleiteten Typs nicht übergeben, ohne sie explizit umzuwandeln.

Nur um Sie vor einer Falle zu warnen:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

Das ist sowieso schrecklicher Code, aber er existiert und das sich ändernde Verhalten in C # 4 kann subtile und schwer zu findende Fehler verursachen, wenn Sie ein Konstrukt wie dieses verwenden.

Michael Stum
quelle
Dies betrifft also vor allem Sammlungen, da Sie in c # 3 einen stärker abgeleiteten Typ an eine Methode mit einem weniger abgeleiteten Typ übergeben können.
Rasiermesser
3
Ja, die große Änderung ist, dass IEnumerable dies jetzt unterstützt, während dies vorher nicht der Fall war.
Michael Stum
4

Von MSDN

Das folgende Codebeispiel zeigt die Unterstützung von Kovarianz und Kontravarianz für Methodengruppen

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}
Kamran Bigdely
quelle
4

Kontravarianz

In der realen Welt können Sie immer ein Tierheim anstelle eines Kaninchenhauses verwenden, da jedes Mal, wenn ein Tierheim ein Kaninchen beherbergt, es ein Tier ist. Wenn Sie jedoch ein Kaninchenhaus anstelle eines Tierheims verwenden, kann das Personal von einem Tiger gefressen werden.

In Code, bedeutet dies , dass , wenn Sie eine haben , IShelter<Animal> animalskönnen Sie einfach schreiben , IShelter<Rabbit> rabbits = animals wenn Sie versprechen , und die Verwendung Tin der IShelter<T>nur als Methodenparameter wie folgt:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

und ersetzen Sie einen Artikel durch einen allgemeineren, dh verringern Sie die Varianz oder führen Sie Kontra ein .

Kovarianz

In der realen Welt können Sie immer einen Lieferanten von Kaninchen anstelle eines Lieferanten von Tieren verwenden, da jedes Mal, wenn ein Kaninchenlieferant Ihnen ein Kaninchen gibt, es ein Tier ist. Wenn Sie jedoch einen Tierlieferanten anstelle eines Kaninchenlieferanten verwenden, können Sie von einem Tiger gefressen werden.

Im Code bedeutet dies, dass Sie, wenn Sie eine haben ISupply<Rabbit> rabbits, einfach schreiben können, ISupply<Animal> animals = rabbits wenn Sie versprechen und Tin der ISupply<T>nur als Methode verwendeten Rückgabewerte wie folgt verwenden:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

und ersetzen Sie einen Gegenstand durch einen abgeleiteten, dh erhöhen Sie die Varianz oder führen Sie eine Co- Varianz ein.

Alles in allem ist dies nur ein überprüfbares Versprechen von Ihnen zur Kompilierungszeit , dass Sie einen generischen Typ auf eine bestimmte Weise behandeln würden, um die Typensicherheit zu gewährleisten und niemanden zum Essen zu bringen.

Vielleicht möchten Sie dies lesen, um Ihren Kopf doppelt darum zu wickeln.

Ivan Rybalko
quelle
du kannst von einem Tiger gefressen werden Das war eine Gegenstimme wert
javadba
Ihr Kommentar zu contravarianceist interessant. Ich lese darin als Hinweis auf eine betriebliche Anforderung: dass der allgemeinere Typ die Anwendungsfälle aller davon abgeleiteten Typen unterstützen muss. In diesem Fall muss das Tierheim in der Lage sein, die Unterbringung aller Tierarten zu unterstützen. In diesem Fall kann das Hinzufügen einer neuen Unterklasse die Oberklasse beschädigen! Das heißt - wenn wir einen Subtyp Tyrannosaurus Rex hinzufügen , könnte dies unser bestehendes Tierheim zerstören .
Javadba
(Fortsetzung). Das unterscheidet sich stark von der strukturell klar beschriebenen Kovarianz : Alle spezifischeren Untertypen unterstützen die im Supertyp definierten Operationen - aber nicht unbedingt auf die gleiche Weise.
Javadba
3

Der Konverterdelegierte hilft mir, beide Konzepte zusammen zu visualisieren:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutputstellt die Kovarianz dar, bei der eine Methode einen spezifischeren Typ zurückgibt .

TInputstellt eine Kontravarianz dar, bei der eine Methode einem weniger spezifischen Typ übergeben wird .

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
Woggles
quelle