Pemilihan metode generik C #

9

Saya mencoba untuk menulis algoritma umum dalam C # yang dapat bekerja dengan entitas geometris dari dimensi yang berbeda.

Dalam contoh berikut yang saya buat Point2dan Point3, keduanya mengimplementasikan IPointantarmuka sederhana .

Sekarang saya memiliki fungsi GenericAlgorithmyang memanggil fungsi GetDim. Ada beberapa definisi fungsi ini berdasarkan jenisnya. Ada juga fungsi mundur yang didefinisikan untuk apa pun yang mengimplementasikan IPoint.

Saya awalnya mengharapkan output dari program berikut ini menjadi 2, 3. Namun, itu adalah 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, jadi karena alasan tertentu informasi tipe konkret hilang GenericAlgorithm. Saya tidak sepenuhnya mengerti mengapa ini terjadi, tetapi baik-baik saja. Jika saya tidak dapat melakukannya dengan cara ini, alternatif apa lagi yang saya miliki?

mohamedmoussa
sumber
2
"Ada juga fungsi mundur" Apa tujuan dari ini, tepatnya? Inti penerapan antarmuka adalah untuk menjamin bahwa NumDimsproperti tersedia. Mengapa Anda mengabaikannya dalam beberapa kasus?
John Wu
Jadi itu mengkompilasi, pada dasarnya. Awalnya, saya pikir fungsi mundur diperlukan jika pada saat run-time kompiler JIT tidak dapat menemukan implementasi khusus untuk GetDim(yaitu saya melewati Point4tetapi GetDim<Point4>tidak ada). Namun, tampaknya kompiler tidak peduli untuk mencari implementasi khusus.
mohamedmoussa
1
@woggy: Anda mengatakan "sepertinya kompiler tidak peduli untuk mencari implementasi khusus" seolah-olah ini adalah masalah kemalasan pada bagian desainer dan pelaksana. Ini bukan. Ini masalah bagaimana obat generik diwakili dalam .NET. Hanya saja bukan jenis spesialisasi yang sama dengan templating di C ++. Metode generik tidak dikompilasi secara terpisah untuk setiap argumen tipe - itu dikompilasi satu kali. Ada pro dan kontra dari ini, tentu saja, tetapi ini bukan masalah "mengganggu".
Jon Skeet
@jonskeet Permintaan maaf jika pilihan bahasa saya buruk, saya yakin ada kerumitan di sini yang belum saya pertimbangkan. Pemahaman saya adalah kompiler tidak mengkompilasi fungsi terpisah untuk tipe referensi, tetapi itu untuk tipe nilai / struct, apakah itu benar?
mohamedmoussa
@woggy: Itulah kompiler JIT , yang merupakan masalah yang sepenuhnya terpisah dari kompiler C # - dan kompiler C # yang melakukan resolusi kelebihan beban. IL untuk metode generik hanya dihasilkan satu kali - tidak satu kali per spesialisasi.
Jon Skeet

Jawaban:

10

Metode ini:

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

... akan selalu menelepon GetDim<T>(T point). Resolusi kelebihan dilakukan pada waktu kompilasi , dan pada tahap itu tidak ada metode lain yang berlaku.

Jika Anda ingin resolusi kelebihan dipanggil pada waktu eksekusi , Anda harus menggunakan pengetikan dinamis, mis

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

Tetapi umumnya ide yang lebih baik untuk menggunakan warisan untuk ini - dalam contoh Anda, jelas Anda hanya bisa memiliki satu metode dan kembali point.NumDims. Saya berasumsi dalam kode asli Anda ada beberapa alasan yang setara sulit dilakukan, tetapi tanpa lebih banyak konteks kami tidak dapat menyarankan tentang cara menggunakan warisan untuk melakukan spesialisasi. Itu adalah opsi Anda:

  • Warisan (lebih disukai) untuk spesialisasi berdasarkan jenis waktu eksekusi dari target
  • Pengetikan dinamis untuk resolusi kelebihan waktu eksekusi
Jon Skeet
sumber
Situasi sebenarnya adalah saya punya AxisAlignedBoundingBox2dan AxisAlignedBoundingBox3. Saya memiliki Containsmetode statis yang digunakan untuk menentukan apakah kumpulan kotak berisi Line2atau Line3(yang mana tergantung pada jenis kotak). Logika algoritma antara kedua jenis ini persis sama, kecuali jumlah dimensi berbeda. Ada juga panggilan ke Intersectinternal yang perlu dikhususkan untuk jenis yang benar. Saya ingin menghindari panggilan fungsi virtual / dinamis, itulah sebabnya saya menggunakan obat generik ... tentu saja, saya hanya dapat menyalin / menempelkan kode dan melanjutkan.
mohamedmoussa
1
@woggy: Sulit memvisualisasikannya hanya dari deskripsi. Jika Anda ingin bantuan mencoba melakukan ini menggunakan warisan, saya sarankan Anda membuat pertanyaan baru dengan contoh minimal tapi lengkap.
Jon Skeet
OK, akan lakukan, saya akan menerima jawaban ini untuk saat ini karena sepertinya saya belum memberikan contoh yang baik.
mohamedmoussa
6

Pada C # 8.0 Anda harus dapat memberikan implementasi default untuk antarmuka Anda, daripada membutuhkan metode generik.

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

Menerapkan metode umum dan kelebihan muatan per IPointimplementasi juga melanggar Prinsip Pergantian Liskov (L dalam SOLID). Anda akan lebih baik untuk mendorong algoritma ke dalam setiap IPointimplementasi, yang berarti Anda hanya perlu satu panggilan metode:

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

Pola Pengunjung

sebagai alternatif dynamicpenggunaan, Anda mungkin ingin menggunakan pola Pengunjung seperti di bawah ini:

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
    }
}
Hebat
sumber
1

Mengapa Anda tidak mendefinisikan fungsi GetDim di kelas dan antarmuka? Sebenarnya, Anda tidak perlu mendefinisikan fungsi GetDim, cukup gunakan properti NumDims.

player2135
sumber