Selección del método genérico C #


9

Estoy tratando de escribir algoritmos genéricos en C # que pueden funcionar con entidades geométricas de diferentes dimensiones.

En el siguiente ejemplo artificial que tengo Point2y Point3, ambos implementamos una IPointinterfaz simple .

Ahora tengo una función GenericAlgorithmque llama a una función GetDim. Existen múltiples definiciones de esta función según el tipo. También hay una función alternativa que se define para cualquier cosa que implemente IPoint.

Inicialmente esperaba que la salida del siguiente programa fuera 2, 3. Sin embargo, es 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, por alguna razón, la información del tipo concreto se pierde GenericAlgorithm. No entiendo completamente por qué sucede esto, pero está bien. Si no puedo hacerlo de esta manera, ¿qué otras alternativas tengo?


2
"También hay una función alternativa" ¿Cuál es exactamente el propósito de esto? El objetivo de implementar una interfaz es garantizar que la NumDimspropiedad esté disponible. ¿Por qué lo ignoras en algunos casos?
John Wu

Entonces se compila, básicamente. Inicialmente, pensé que la función de retroceso es necesaria si en tiempo de ejecución el compilador JIT no puede encontrar una implementación especializada GetDim(es decir, paso un Point4pero GetDim<Point4>no existe). Sin embargo, no parece que el compilador se moleste en buscar una implementación especializada.
Mohamedmoussa

1
@woggy: Dices "no parece que el compilador se moleste en buscar una implementación especializada" como si fuera una cuestión de pereza por parte de los diseñadores e implementadores. No es. Se trata de cómo se representan los genéricos en .NET. Simplemente no es el mismo tipo de especialización que la plantilla en C ++. Un método genérico no se compila por separado para cada argumento de tipo, se compila una vez. Hay pros y contras de esto, sin duda, pero no se trata de "molestar".
Jon Skeet

@jonskeet Disculpas si mi elección de idioma fue pobre, estoy seguro de que aquí hay complejidades que no he considerado. Entiendo que el compilador no compila funciones separadas para los tipos de referencia, pero sí para los tipos / estructuras de valor, ¿es correcto?
Mohamedmoussa

@woggy: Ese es el compilador JIT , que es un asunto completamente separado del compilador C #, y es el compilador C # que realiza la resolución de sobrecarga. La IL para el método genérico solo se genera una vez, no una vez por especialización.
Jon Skeet

Respuestas:


10

Este método:

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

... siempre llamará GetDim<T>(T point). La resolución de sobrecarga se realiza en tiempo de compilación , y en esa etapa no hay otro método aplicable.

Si desea que se invoque la resolución de sobrecarga en el momento de la ejecución , deberá usar la tipificación dinámica, p. Ej.

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

Pero generalmente es una mejor idea usar la herencia para esto; en su ejemplo, obviamente podría tener el método único y regresar point.NumDims. Supongo que en su código real hay alguna razón por la que es más difícil hacer el equivalente, pero sin más contexto no podemos aconsejar sobre cómo usar la herencia para realizar la especialización. Sin embargo, esas son sus opciones:

  • Herencia (preferida) para especialización basada en el tipo de tiempo de ejecución del objetivo
  • Escritura dinámica para resolución de sobrecarga en tiempo de ejecución

La situación real es que tengo un AxisAlignedBoundingBox2y AxisAlignedBoundingBox3. Tengo un Containsmétodo estático que se utiliza para determinar si una colección de cuadros contiene un Line2o Line3(cuál depende del tipo de cuadros). La lógica del algoritmo entre los dos tipos es exactamente la misma, excepto que el número de dimensiones es diferente. También hay llamadas Intersectinternas que necesitan especializarse para el tipo correcto. Quiero evitar las llamadas a funciones virtuales / dinámicas, por eso estoy usando genéricos ... por supuesto, puedo copiar / pegar el código y seguir adelante.
mohamedmoussa

1
@woggy: es bastante difícil visualizar eso solo con una descripción. Si desea ayuda para intentar hacer esto usando la herencia, le sugiero que cree una nueva pregunta con un ejemplo mínimo pero completo.
Jon Skeet

Bien, lo haré, aceptaré esta respuesta por ahora ya que parece que no he dado un buen ejemplo.
Mohamedmoussa

6

A partir de C # 8.0, debería poder proporcionar una implementación predeterminada para su interfaz, en lugar de requerir el método genérico.

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

Implementar un método genérico y sobrecargas por IPointimplementación también viola el Principio de sustitución de Liskov (la L en SÓLIDO). Sería mejor insertar el algoritmo en cada IPointimplementación, lo que significa que solo debería necesitar una sola llamada al método:

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

3

Patrón de visitante

Como alternativa al dynamicuso, es posible que desee utilizar un patrón de visitante como se muestra a continuación:

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

¿Por qué no define la función GetDim en clase e interfaz? En realidad, no necesita definir la función GetDim, solo use la propiedad NumDims.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.