Seleção de método genérico em C #

9

Estou tentando escrever algoritmos genéricos em c # que podem funcionar com entidades geométricas de diferentes dimensões.

No exemplo a seguir, eu tenho Point2e Point3, ambos implementando uma IPointinterface simples .

Agora eu tenho uma função GenericAlgorithmque chama uma função GetDim. Existem várias definições dessa função com base no tipo. Há também uma função de fallback definida para qualquer coisa que implemente IPoint.

Inicialmente, eu esperava que a saída do programa a seguir fosse 2, 3. No entanto, é 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 algum motivo as informações do tipo concreto são perdidas GenericAlgorithm. Não entendo completamente por que isso acontece, mas tudo bem. Se não posso fazer dessa maneira, que outras alternativas tenho?

mohamedmoussa
fonte
2
"Existe também uma função de reserva" Qual é o objetivo disso, exatamente? O objetivo principal da implementação de uma interface é garantir que a NumDimspropriedade esteja disponível. Por que você está ignorando isso em alguns casos?
John Wu
Então, ele compila, basicamente. Inicialmente, pensei que a função fallback seria necessária se, em tempo de execução, o compilador JIT não conseguir encontrar uma implementação especializada para GetDim(ou seja, eu passo uma, Point4mas GetDim<Point4>não existe). No entanto, não parece que o compilador se preocupe em procurar uma implementação especializada.
mohamedmoussa
11
@ woggy: Você diz "não parece que o compilador se incomoda em procurar uma implementação especializada" como se isso fosse uma questão de preguiça por parte de designers e implementadores. Não é. É uma questão de como os genéricos são representados no .NET. Simplesmente não é o mesmo tipo de especialização que o modelo em C ++. Um método genérico não é compilado separadamente para cada argumento de tipo - ele é compilado uma vez. Existem prós e contras disso, certamente, mas não se trata de "incomodar".
Jon Skeet
@jonskeet Desculpas se minha escolha de idioma foi ruim, tenho certeza de que há complexidades aqui que não considerei. Meu entendimento era que o compilador não compila funções separadas para tipos de referência, mas sim para tipos / estruturas de valor, está correto?
mohamedmoussa 26/02
@ woggy: Esse é o compilador JIT , que é um assunto totalmente separado do compilador C # - e é o compilador C # que executa a resolução de sobrecarga. A IL para o método genérico é gerada apenas uma vez - não uma vez por especialização.
Jon Skeet

Respostas:

10

Este método:

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

... vai sempre chamar GetDim<T>(T point). A resolução de sobrecarga é executada em tempo de compilação e, nesse estágio, não há outro método aplicável.

Para que a resolução de sobrecarga seja chamada no tempo de execução , você precisará usar a digitação dinâmica, por exemplo

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

Mas geralmente é uma idéia melhor usar herança para isso - no seu exemplo, obviamente, você pode ter apenas o método único e retornar point.NumDims. Presumo que no seu código real há alguma razão para o equivalente ser mais difícil de fazer, mas sem mais contexto, não podemos aconselhar sobre como usar a herança para executar a especialização. Essas são as suas opções:

  • Herança (preferencial) para especialização com base no tipo de tempo de execução do destino
  • Digitação dinâmica para resolução de sobrecarga no tempo de execução
Jon Skeet
fonte
A situação real é que eu tenho um AxisAlignedBoundingBox2e AxisAlignedBoundingBox3. Eu tenho um Containsmétodo estático que é usado para determinar se uma coleção de caixas contém um Line2ou Line3(qual deles depende do tipo das caixas). A lógica do algoritmo entre os dois tipos é exatamente a mesma, exceto que o número de dimensões é diferente. Também existem chamadas para Intersectinternamente que precisam ser especializadas no tipo correto. Quero evitar chamadas de funções virtuais / dinâmicas, e é por isso que estou usando genéricos ... é claro, posso apenas copiar / colar o código e seguir em frente.
mohamedmoussa
11
@ woggy: É muito difícil visualizar isso a partir de apenas uma descrição. Se você quiser ajudar a tentar fazer isso usando herança, sugiro que você crie uma nova pergunta com um exemplo mínimo, mas completo.
Jon Skeet
OK, aceitarei esta resposta por enquanto, pois parece que não forneci um bom exemplo.
mohamedmoussa
6

A partir do C # 8.0, você deve fornecer uma implementação padrão para sua interface, em vez de exigir o método genérico.

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

A implementação de um método genérico e sobrecargas por IPointimplementação também viola o Princípio de Substituição de Liskov (o L no SOLID). Seria melhor inserir o algoritmo em cada IPointimplementação, o que significa que você só precisa de uma única chamada de método:

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

Padrão do visitante

como alternativa ao dynamicuso, convém usar um padrão de visitante como abaixo:

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
fonte
1

Por que você não define a função GetDim na classe e na interface? Na verdade, você não precisa definir a função GetDim, basta usar a propriedade NumDims.

player2135
fonte