Auswahl der C # -Generikmethode

9

Ich versuche, generische Algorithmen in C # zu schreiben, die mit geometrischen Objekten unterschiedlicher Dimension arbeiten können.

Im folgenden erfundenen Beispiel habe ich Point2und Point3beide implementieren eine einfache IPointSchnittstelle.

Jetzt habe ich eine Funktion GenericAlgorithm, die eine Funktion aufruft GetDim. Je nach Typ gibt es mehrere Definitionen dieser Funktion. Es gibt auch eine Fallback-Funktion, die für alles definiert ist, was implementiert wird IPoint.

Ich habe anfangs erwartet, dass die Ausgabe des folgenden Programms 2, 3 ist. Es ist jedoch 0, 0.

interface IPoint {
    public int NumDims { get; } 
}

public struct Point2 : IPoint {
    public int NumDims => 2;
}

public struct Point3 : IPoint {
    public int NumDims => 3;
}

class Program
{
    static int GetDim<T>(T point) where T: IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 0 !!
        Console.WriteLine("{0:d}", d2);        // returns 0 !!
    }
}

OK, aus irgendeinem Grund gehen die konkreten Typinformationen in verloren GenericAlgorithm. Ich verstehe nicht ganz, warum das passiert, aber gut. Welche anderen Alternativen habe ich, wenn ich es nicht so machen kann?

mohamedmoussa
quelle
2
"Es gibt auch eine Fallback-Funktion" Was genau ist der Zweck davon? Bei der Implementierung einer Schnittstelle geht es darum, sicherzustellen, dass die NumDimsEigenschaft verfügbar ist. Warum ignorierst du es in einigen Fällen?
John Wu
Also kompiliert es im Grunde. Anfangs dachte ich, dass die Fallback-Funktion erforderlich ist, wenn der JIT-Compiler zur Laufzeit keine spezielle Implementierung für finden kann GetDim(dh ich übergebe eine Point4, GetDim<Point4>aber nicht vorhanden). Es scheint jedoch nicht, dass der Compiler sich die Mühe macht, nach einer speziellen Implementierung zu suchen.
Mohamedmoussa
1
@woggy: Sie sagen, "es scheint den Compiler nicht zu stören, nach einer speziellen Implementierung zu suchen", als ob dies eine Frage der Faulheit von Designern und Implementierern wäre. Es ist nicht. Es geht darum, wie Generika in .NET dargestellt werden. Es ist einfach nicht die gleiche Spezialisierung wie das Templating in C ++. Eine generische Methode wird nicht für jedes Typargument separat kompiliert, sondern einmal kompiliert. Es gibt sicherlich Vor- und Nachteile, aber es geht nicht darum, "zu stören".
Jon Skeet
@jonskeet Entschuldigung, wenn meine Sprachwahl schlecht war, bin ich sicher, dass es hier Komplexitäten gibt, die ich nicht berücksichtigt habe. Mein Verständnis war, dass der Compiler keine separaten Funktionen für Referenztypen kompiliert, sondern für Werttypen / Strukturen. Ist das richtig?
Mohamedmoussa
@woggy: Das ist der JIT- Compiler, der vom C # -Compiler völlig getrennt ist - und der C # -Compiler, der die Überlastungsauflösung durchführt. Die IL für die generische Methode wird nur einmal generiert - nicht einmal pro Spezialisierung.
Jon Skeet

Antworten:

10

Diese Methode:

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

... wird immer anrufen GetDim<T>(T point). Die Überlastungsauflösung wird zur Kompilierungszeit durchgeführt , und zu diesem Zeitpunkt gibt es keine andere anwendbare Methode.

Wenn Sie möchten, dass die Überlastungsauflösung zur Ausführungszeit aufgerufen wird , müssen Sie die dynamische Typisierung verwenden, z

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim((dynamic) point);

Im Allgemeinen ist es jedoch besser, die Vererbung dafür zu verwenden. In Ihrem Beispiel könnten Sie natürlich nur die einzige Methode verwenden und zurückkehren point.NumDims. Ich gehe davon aus, dass es in Ihrem realen Code einen Grund gibt, warum das Äquivalent schwieriger zu tun ist, aber ohne mehr Kontext können wir nicht raten, wie die Vererbung zur Durchführung der Spezialisierung verwendet werden soll. Dies sind jedoch Ihre Optionen:

  • Vererbung (bevorzugt) zur Spezialisierung basierend auf dem Ausführungstyp des Ziels
  • Dynamische Eingabe für die Überlastungsauflösung während der Ausführung
Jon Skeet
quelle
Die wirkliche Situation ist, dass ich ein AxisAlignedBoundingBox2und habe AxisAlignedBoundingBox3. Ich habe eine Containsstatische Methode, mit der bestimmt wird, ob eine Sammlung von Boxen ein Line2oder enthält Line3(welche davon vom Typ der Boxen abhängt). Die Algorithmuslogik zwischen den beiden Typen ist genau gleich, außer dass die Anzahl der Dimensionen unterschiedlich ist. Es gibt auch Anrufe zuIntersect interne die auf den richtigen Typ spezialisiert werden müssen. Ich möchte virtuelle Funktionsaufrufe / Dynamics vermeiden, weshalb ich Generika verwende. Natürlich kann ich den Code einfach kopieren / einfügen und weitermachen.
Mohamedmoussa
1
@woggy: Es ist ziemlich schwer, das nur anhand einer Beschreibung zu visualisieren. Wenn Sie Hilfe bei der Vererbung benötigen, empfehlen wir Ihnen, eine neue Frage mit einem minimalen, aber vollständigen Beispiel zu erstellen.
Jon Skeet
OK, ich werde diese Antwort vorerst akzeptieren, da ich anscheinend kein gutes Beispiel geliefert habe.
Mohamedmoussa
6

Ab C # 8.0 sollten Sie in der Lage sein, eine Standardimplementierung für Ihre Schnittstelle bereitzustellen, anstatt die generische Methode zu benötigen.

interface IPoint {
    int NumDims { get => 0; }
}

Das Implementieren einer generischen Methode und Überladungen pro IPointImplementierung verstößt auch gegen das Liskov-Substitutionsprinzip (das L in SOLID). Es ist besser, den Algorithmus in jede IPointImplementierung zu übertragen, was bedeutet, dass Sie nur einen einzigen Methodenaufruf benötigen sollten:

static int GetDim(IPoint point) => point.NumDims;
Matthew Layton
quelle
3

Besuchermuster

Als Alternative zur dynamicVerwendung möchten Sie möglicherweise ein Besuchermuster wie folgt verwenden :

interface IPoint
{
    public int NumDims { get; }
    public int Accept(IVisitor visitor);
}

public struct Point2 : IPoint
{
    public int NumDims => 2;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public struct Point3 : IPoint
{
    public int NumDims => 3;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public class Visitor : IVisitor
{
    public int Visit(Point2 toVisit)
    {
        return toVisit.NumDims;
    }

    public int Visit(Point3 toVisit)
    {
        return toVisit.NumDims;
    }
}

public interface IVisitor<T>
{
    int Visit(T toVisit);
}

public interface IVisitor : IVisitor<Point2>, IVisitor<Point3> { }

class Program
{
    static int GetDim<T>(T point) where T : IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => point.Accept(new Visitor());

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 2
        Console.WriteLine("{0:d}", d2);        // returns 3
    }
}
Fab
quelle
1

Warum definieren Sie die GetDim-Funktion nicht in Klasse und Schnittstelle?? Eigentlich müssen Sie die GetDim-Funktion nicht definieren, sondern verwenden Sie einfach die Eigenschaft NumDims.

player2135
quelle