Wybór metody ogólnej C #

9

Próbuję pisać ogólne algorytmy w języku C #, które mogą pracować z elementami geometrycznymi o innym wymiarze.

W poniższym wymyślonym przykładzie mam Point2i Point3oba, implementujące prosty IPointinterfejs.

Teraz mam funkcję, GenericAlgorithmktóra wywołuje funkcję GetDim. Istnieje wiele definicji tej funkcji w zależności od typu. Istnieje również funkcja rezerwowa zdefiniowana dla wszystkiego, co implementuje IPoint.

Początkowo spodziewałem się, że wynikiem następującego programu będzie 2, 3. Jest to jednak 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, więc z jakiegoś powodu zaginęła konkretna informacja o typie GenericAlgorithm. Nie do końca rozumiem, dlaczego tak się dzieje, ale dobrze. Jeśli nie mogę tego zrobić w ten sposób, jakie mam inne alternatywy?

mohamedmoussa
źródło
2
„Istnieje również funkcja rezerwowa”. Jaki jest dokładnie tego cel? Celem implementacji interfejsu jest zagwarantowanie, że NumDimswłaściwość jest dostępna. Dlaczego w niektórych przypadkach ignorujesz to?
John Wu
Zasadniczo się kompiluje. Początkowo myślałem, że funkcja rezerwowa jest wymagana, jeśli w czasie wykonywania kompilator JIT nie może znaleźć specjalnej implementacji GetDim(tzn. Przekazuję, Point4ale GetDim<Point4>nie istnieje). Wydaje się jednak, że kompilator nie chce szukać specjalnej implementacji.
mohamedmoussa
1
@ woggy: Mówisz: „Wydaje się, że kompilator nie chce szukać specjalistycznej implementacji”, jakby to było lenistwo ze strony projektantów i implementatorów. To nie jest. To kwestia tego, w jaki sposób generyczne są reprezentowane w .NET. To po prostu inna specjalizacja niż szablony w C ++. Metoda ogólna nie jest kompilowana osobno dla każdego argumentu typu - jest kompilowana raz. Są oczywiście plusy i minusy, ale to nie jest kwestia „przeszkadzania”.
Jon Skeet
@jonskeet Przepraszam, jeśli mój wybór języka był słaby, jestem pewien, że są tutaj złożone kwestie, których nie wziąłem pod uwagę. Rozumiem, że kompilator nie kompiluje osobnych funkcji dla typów referencyjnych, ale robi to dla typów wartości / struktur, czy to prawda?
mohamedmoussa
@ woggy: To jest kompilator JIT , który jest całkowicie odrębną sprawą od kompilatora C # - i to kompilator C # wykonuje rozdzielczość przeciążenia. IL dla metody ogólnej jest generowany tylko raz - nie raz dla specjalizacji.
Jon Skeet

Odpowiedzi:

10

Ta metoda:

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

... zawsze zadzwoni GetDim<T>(T point). Rozdzielczość przeciążenia jest wykonywana w czasie kompilacji i na tym etapie nie ma innej stosownej metody.

Jeśli chcesz, aby w czasie wykonywania wywoływana była rozdzielczość przeciążenia , musisz użyć dynamicznego pisania, np

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

Ale generalnie lepszym pomysłem jest użycie do tego dziedziczenia - w twoim przykładzie oczywiście możesz mieć tylko jedną metodę i zwrócić point.NumDims. Zakładam, że w twoim prawdziwym kodzie istnieje jakiś powód, dla którego ekwiwalent jest trudniejszy, ale bez większego kontekstu nie możemy doradzić, jak użyć dziedziczenia do wykonania specjalizacji. Są to jednak twoje opcje:

  • Dziedziczenie (preferowane) dla specjalizacji opartej na typie czasu wykonania celu
  • Pisanie dynamiczne w celu rozwiązania problemu przeciążenia w czasie wykonywania
Jon Skeet
źródło
Prawdziwa sytuacja jest taka, że ​​mam AxisAlignedBoundingBox2i AxisAlignedBoundingBox3. Mam Containsmetodę statyczną, która jest używana do ustalenia, czy zbiór pudełek zawiera znak Line2lub Line3(który zależy od rodzaju pudełek). Logika algorytmu między tymi dwoma typami jest dokładnie taka sama, z tym wyjątkiem, że liczba wymiarów jest inna. Istnieją również połączenia Intersectwewnętrzne, które muszą być wyspecjalizowane dla właściwego typu. Chcę uniknąć wywoływania funkcji wirtualnych / dynamicznych, dlatego używam ogólnych ... oczywiście, mogę po prostu skopiować / wkleić kod i przejść dalej.
mohamedmoussa
1
@ woggy: Trudno to sobie wyobrazić na podstawie samego opisu. Jeśli potrzebujesz pomocy w próbie zrobienia tego za pomocą dziedziczenia, sugeruję, aby utworzyć nowe pytanie z minimalnym, ale kompletnym przykładem.
Jon Skeet
OK, zrobię, na razie zaakceptuję tę odpowiedź, ponieważ wydaje się, że nie podałem dobrego przykładu.
mohamedmoussa
6

Począwszy od wersji C # 8.0 powinieneś być w stanie zapewnić domyślną implementację interfejsu, zamiast wymagać ogólnej metody.

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

Wdrożenie ogólnej metody i przeciążenia na IPointwdrożenie również narusza zasadę podstawienia Liskowa (L w SOLID). Lepiej byłoby wcisnąć algorytm do każdej IPointimplementacji, co oznacza, że ​​potrzebujesz tylko jednego wywołania metody:

static int GetDim(IPoint point) => point.NumDims;
Matthew Layton
źródło
3

Wzór gościa

jako alternatywę dla dynamicużycia możesz użyć wzorca gościa, jak poniżej:

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
źródło
1

Dlaczego nie zdefiniujesz funkcji GetDim w klasie i interfejsie? W rzeczywistości nie musisz definiować funkcji GetDim, po prostu użyj właściwości NumDims.

player2135
źródło