การเลือกวิธี C # generics


9

ฉันกำลังพยายามเขียนอัลกอริทึมทั่วไปใน C # ที่สามารถทำงานกับหน่วยงานทางเรขาคณิตในมิติที่แตกต่างกัน

ในตัวอย่างที่วางแผนไว้ต่อไปนี้ฉันมีPoint2และPoint3ทั้งสองใช้IPointอินเทอร์เฟซที่เรียบง่าย

ตอนนี้ฉันมีฟังก์ชั่นที่เรียกฟังก์ชั่นGenericAlgorithm GetDimมีหลายคำจำกัดความของฟังก์ชั่นนี้ขึ้นอยู่กับประเภท IPointนอกจากนี้ยังมีฟังก์ชั่นฤดูใบไม้ร่วงหลังที่กำหนดไว้สำหรับสิ่งที่ดำเนินการ

ตอนแรกฉันคาดว่าผลลัพธ์ของโปรแกรมต่อไปนี้จะเป็น 2, 3 อย่างไรก็ตามมันคือ 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 !!
    }
}

GenericAlgorithmตกลงดังนั้นด้วยเหตุผลบางประเภทข้อมูลที่เป็นรูปธรรมจะหายไปใน ฉันไม่เข้าใจว่าทำไมสิ่งนี้ถึงเกิดขึ้น แต่ก็ดี ถ้าฉันทำแบบนี้ไม่ได้ฉันมีทางเลือกอื่นอีกไหม?


2
"นอกจากนี้ยังมีฟังก์ชั่นถอยกลับ" จุดประสงค์ของสิ่งนี้คืออะไรกันแน่? จุดรวมของการใช้อินเทอร์เฟซคือการรับประกันว่าNumDimsคุณสมบัติพร้อมใช้งาน ทำไมคุณถึงเพิกเฉยในบางกรณี
John Wu

ดังนั้นจึงรวบรวมโดยทั่วไป เริ่มแรกฉันคิดว่าจำเป็นต้องใช้ฟังก์ชั่นถอยกลับหากในขณะใช้งานคอมไพเลอร์ JIT ไม่สามารถหาการใช้งานแบบพิเศษสำหรับGetDim(เช่นฉันผ่านPoint4แต่GetDim<Point4>ไม่มีอยู่) อย่างไรก็ตามดูเหมือนว่าคอมไพเลอร์จะรบกวนการใช้งานแบบพิเศษ
mohamedmoussa

1
@woggy: คุณพูดว่า "มันดูเหมือนจะไม่คอมไพเลอร์รบกวนการมองหาการใช้งานพิเศษ" ราวกับว่ามันเป็นเรื่องของความเกียจคร้านในส่วนของนักออกแบบและผู้ดำเนินการ มันไม่ใช่. มันเป็นเรื่องของวิธีการแสดงทั่วไปใน. NET มันไม่ใช่ความเชี่ยวชาญแบบเดียวกับ templating ใน C ++ วิธีการทั่วไปไม่ได้รวบรวมแยกกันสำหรับแต่ละประเภทอาร์กิวเมนต์ - มันรวบรวมครั้งเดียว มีข้อดีและข้อเสียของเรื่องนี้อย่างแน่นอน แต่มันไม่ใช่เรื่องของ "รบกวน"
Jon Skeet

@jonskeet ขอโทษหากการเลือกภาษาของฉันแย่ฉันแน่ใจว่ามีความซับซ้อนที่ฉันไม่ได้พิจารณา ความเข้าใจของฉันคือคอมไพเลอร์ไม่ได้รวบรวมฟังก์ชั่นแยกต่างหากสำหรับประเภทการอ้างอิง แต่ทำเพื่อคุณค่า / ประเภทโครงสร้างนั้นถูกต้องหรือไม่?
mohamedmoussa

@woggy: นั่นคือJIT - คอมไพเลอร์ซึ่งเป็นเรื่องที่แยกออกจากคอมไพเลอร์ C # อย่างสิ้นเชิงและเป็นคอมไพเลอร์ C # ที่ทำงานแก้ปัญหาโอเวอร์โหลด IL สำหรับวิธีการทั่วไปจะถูกสร้างขึ้นเพียงครั้งเดียว - ไม่ใช่ครั้งเดียวต่อความเชี่ยวชาญเฉพาะทาง
Jon Skeet

คำตอบ:


10

วิธีนี้:

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

... จะเสมอGetDim<T>(T point)เรียก ความละเอียดเกินพิกัดจะดำเนินการในเวลารวบรวมและในขั้นตอนนั้นไม่มีวิธีการอื่นที่เกี่ยวข้อง

หากคุณต้องการเรียกความละเอียดเกินพิกัด ณเวลาที่ดำเนินการคุณจะต้องใช้การพิมพ์แบบไดนามิกเช่น

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

แต่โดยทั่วไปแล้วความคิดที่ดีกว่าคือการใช้การสืบทอดสำหรับสิ่งนี้ - ในตัวอย่างของคุณเห็นได้ชัดว่าคุณสามารถมีวิธีการเดียวและส่งคืนpoint.NumDimsได้ ฉันสันนิษฐานว่าในรหัสจริงของคุณมีเหตุผลบางอย่างที่เทียบเท่าทำได้ยากกว่า แต่ถ้าไม่มีบริบทเพิ่มเติมเราไม่สามารถแนะนำวิธีใช้การสืบทอดเพื่อดำเนินการเฉพาะด้านได้ นี่คือตัวเลือกของคุณ:

  • การสืบทอด (แนะนำ) สำหรับความเชี่ยวชาญขึ้นอยู่กับประเภทเวลาปฏิบัติการของเป้าหมาย
  • การพิมพ์แบบไดนามิกสำหรับการแก้ปัญหาการโอเวอร์โหลดเวลาทำงาน

สถานการณ์จริงคือผมมีและAxisAlignedBoundingBox2 AxisAlignedBoundingBox3ฉันมีContainsวิธีคงที่ที่ใช้ในการตรวจสอบว่าชุดของกล่องมีLine2หรือLine3(ซึ่งขึ้นอยู่กับชนิดของกล่อง) ตรรกะของอัลกอริธึมระหว่างทั้งสองประเภทจะเหมือนกันทุกประการยกเว้นจำนวนมิติจะแตกต่างกัน นอกจากนี้ยังมีการโทรIntersectภายในที่ต้องมีความเชี่ยวชาญในประเภทที่ถูกต้อง ฉันต้องการหลีกเลี่ยงการเรียกใช้ฟังก์ชั่นเสมือน / ไดนามิกซึ่งเป็นเหตุผลว่าทำไมฉันใช้ Generics ... แน่นอนฉันสามารถคัดลอก / วางรหัสและย้ายไป
mohamedmoussa

1
@woggy: มันค่อนข้างยากที่จะจินตนาการว่าจากเพียงแค่คำอธิบาย หากคุณต้องการความช่วยเหลือในการพยายามทำสิ่งนี้โดยใช้การสืบทอดฉันขอแนะนำให้คุณสร้างคำถามใหม่ด้วยตัวอย่างน้อยที่สุด แต่สมบูรณ์
Jon Skeet

ตกลงจะทำฉันจะยอมรับคำตอบนี้ในตอนนี้เพราะฉันไม่ได้เป็นตัวอย่างที่ดี
mohamedmoussa

6

ในฐานะของ C # 8.0 คุณควรจะสามารถให้การใช้งานเริ่มต้นสำหรับอินเทอร์เฟซของคุณแทนที่จะต้องใช้วิธีการทั่วไป

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

การใช้วิธีการทั่วไปและการโอเวอร์โหลดต่อIPointการใช้งานยังเป็นการฝ่าฝืนหลักการการทดแทน Liskov (L ใน SOLID) คุณจะดีกว่าที่จะผลักดันอัลกอริทึมในIPointการใช้งานแต่ละครั้งซึ่งหมายความว่าคุณควรจะต้องเรียกวิธีการเดียว:

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

3

รูปแบบผู้เข้าชม

เพื่อทดแทนการdynamicใช้งานคุณอาจต้องการใช้รูปแบบผู้เข้าชมดังนี้:

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

ทำไมคุณไม่กำหนดฟังก์ชัน GetDim ในคลาสและอินเทอร์เฟซ? จริงๆแล้วคุณไม่จำเป็นต้องกำหนดฟังก์ชัน GetDim เพียงใช้คุณสมบัติ NumDims

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.