Selezione del metodo generici C #


9

Sto cercando di scrivere algoritmi generici in C # che possano funzionare con entità geometriche di diversa dimensione.

Nel seguente esempio inventato ho Point2e Point3, entrambi implementando una semplice IPointinterfaccia.

Ora ho una funzione GenericAlgorithmche chiama una funzione GetDim. Esistono più definizioni di questa funzione in base al tipo. Esiste anche una funzione di fallback definita per tutto ciò che implementa IPoint.

Inizialmente mi aspettavo che l'output del seguente programma fosse 2, 3. Tuttavia, è 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, quindi per qualche motivo le informazioni sul tipo concreto vengono perse GenericAlgorithm. Non capisco bene perché questo accada, ma va bene. Se non posso farlo in questo modo, quali altre alternative ho?


2
"Esiste anche una funzione di fallback" Qual è lo scopo di questo, esattamente? Il punto centrale dell'implementazione di un'interfaccia è garantire la NumDimsdisponibilità della proprietà. Perché lo stai ignorando in alcuni casi?
John Wu,

Quindi si compila, sostanzialmente. Inizialmente, ho pensato che la funzione di fallback fosse necessaria se in fase di esecuzione il compilatore JIT non è in grado di trovare un'implementazione specializzata per GetDim(ovvero passo un Point4ma GetDim<Point4>non esiste). Tuttavia, non sembra che il compilatore si preoccupi di cercare un'implementazione specializzata.
Mohamedmoussa,

1
@woggy: Dici "non sembra che il compilatore si preoccupi di cercare un'implementazione specializzata" come se questa fosse una questione di pigrizia da parte di progettisti e implementatori. Non è. È una questione di come i generici sono rappresentati in .NET. Non è lo stesso tipo di specializzazione del modello in C ++. Un metodo generico non viene compilato separatamente per ogni argomento di tipo: viene compilato una volta. Ci sono pro e contro di questo, certamente, ma non è una questione di "fastidio".
Jon Skeet,

@jonskeet Mi scuso se la mia scelta della lingua è stata scarsa, sono sicuro che ci sono delle complessità che non ho preso in considerazione. La mia comprensione è stata che il compilatore non compila funzioni separate per tipi di riferimento, ma lo fa per tipi / strutture di valore, è corretto?
Mohamedmoussa,

@woggy: quello è il compilatore JIT , che è una questione completamente separata dal compilatore C # - ed è il compilatore C # che esegue la risoluzione di sovraccarico. L'IL per il metodo generico viene generato una sola volta, non una volta per specializzazione.
Jon Skeet,

Risposte:


10

Questo metodo:

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

... sarà sempre chiamata GetDim<T>(T point). La risoluzione del sovraccarico viene eseguita in fase di compilazione e in quella fase non esiste un altro metodo applicabile.

Se si desidera chiamare la risoluzione di sovraccarico al momento dell'esecuzione , è necessario utilizzare la digitazione dinamica, ad es

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

Ma è generalmente un'idea migliore usare l'ereditarietà per questo - nel tuo esempio, ovviamente potresti semplicemente avere il singolo metodo e restituire point.NumDims. Presumo che nel tuo vero codice ci sia qualche ragione per cui l'equivalente sia più complicato da fare, ma senza più contesto non possiamo consigliare su come usare l'ereditarietà per eseguire la specializzazione. Quelle sono le tue opzioni però:

  • Ereditarietà (preferita) per la specializzazione basata sul tipo di tempo di esecuzione del target
  • Digitazione dinamica per la risoluzione del sovraccarico del tempo di esecuzione

La vera situazione è che ho un AxisAlignedBoundingBox2e AxisAlignedBoundingBox3. Ho un Containsmetodo statico che viene utilizzato per determinare se una raccolta di scatole contiene un Line2o Line3(quale dipende dal tipo di scatole). La logica dell'algoritmo tra i due tipi è esattamente la stessa, tranne che il numero di dimensioni è diverso. Ci sono anche chiamate Intersectinterne che devono essere specializzate nel tipo corretto. Voglio evitare chiamate di funzione virtuali / dinamiche, motivo per cui sto usando generics ... ovviamente, posso solo copiare / incollare il codice e andare avanti.
Mohamedmoussa,

1
@woggy: è abbastanza difficile visualizzarlo da una sola descrizione. Se vuoi aiuto cercando di farlo usando l'ereditarietà, ti suggerisco di creare una nuova domanda con un esempio minimo ma completo.
Jon Skeet,

OK, lo farò, accetterò questa risposta per ora in quanto sembra che non abbia fornito un buon esempio.
Mohamedmoussa,

6

A partire da C # 8.0 dovresti essere in grado di fornire un'implementazione predefinita per la tua interfaccia, piuttosto che richiedere il metodo generico.

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

L'implementazione di un metodo generico e sovraccarichi per IPointimplementazione viola anche il Principio di sostituzione di Liskov (la L in SOLID). Sarebbe meglio inserire l' algoritmo in ogni IPointimplementazione, il che significa che dovresti avere bisogno di una sola chiamata di metodo:

static int GetDim(IPoint point) => point.NumDims;

3

Modello visitatore

in alternativa dynamicall'utilizzo, potresti voler utilizzare un modello Visitatore come di seguito:

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
    }
}

1

Perché non definire la funzione GetDim in classe e interfaccia? In realtà, non è necessario definire la funzione GetDim, basta usare la proprietà NumDims.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.