C # Generics - วิธีการหลีกเลี่ยงวิธีการซ้ำซ้อน?


28

สมมติว่าฉันมีสองคลาสที่มีลักษณะเช่นนี้ (บล็อกแรกของรหัสและปัญหาทั่วไปเกี่ยวข้องกับ C #):

class A 
{
    public int IntProperty { get; set; }
}

class B 
{
    public int IntProperty { get; set; }
}

คลาสเหล่านี้ไม่สามารถเปลี่ยนแปลงได้ แต่อย่างใด (เป็นส่วนหนึ่งของการประชุมของบุคคลที่สาม) ดังนั้นฉันไม่สามารถทำให้พวกเขาใช้อินเทอร์เฟซเดียวกันหรือสืบทอดคลาสเดียวกันที่จะมี IntProperty

ฉันต้องการใช้ตรรกะบางอย่างกับIntPropertyคุณสมบัติของทั้งสองคลาสและใน C ++ ฉันสามารถใช้คลาสเทมเพลตเพื่อทำสิ่งนั้นได้อย่างง่ายดาย:

template <class T>
class LogicToBeApplied
{
    public:
        void T CreateElement();

};

template <class T>
T LogicToBeApplied<T>::CreateElement()
{
    T retVal;
    retVal.IntProperty = 50;
    return retVal;
}

แล้วฉันจะทำอะไรเช่นนี้

LogicToBeApplied<ClassA> classALogic;
LogicToBeApplied<ClassB> classBLogic;
ClassA classAElement = classALogic.CreateElement();
ClassB classBElement = classBLogic.CreateElement();   

ด้วยวิธีนี้ฉันสามารถสร้างคลาสโรงงานทั่วไปที่สามารถใช้ได้ทั้ง ClassA และ ClassB

อย่างไรก็ตามใน C # ฉันต้องเขียนสองคลาสที่มีสองส่วนที่แตกต่างกันwhereแม้ว่ารหัสสำหรับลอจิกจะเหมือนกันทุกประการ:

public class LogicAToBeApplied<T> where T : ClassA, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

public class LogicBToBeApplied<T> where T : ClassB, new()
{
    public T CreateElement()
    {
        T retVal = new T();
        retVal.IntProperty = 50;
        return retVal;
    }
}

ฉันรู้ว่าถ้าฉันต้องการมีคลาสที่แตกต่างกันในwhereข้อพวกเขาจำเป็นต้องเกี่ยวข้องเช่นการสืบทอดคลาสเดียวกันถ้าฉันต้องการใช้รหัสเดียวกันกับพวกเขาในแง่ที่ฉันอธิบายข้างต้น มันเป็นเรื่องที่น่ารำคาญมากที่มีสองวิธีที่เหมือนกันโดยสิ้นเชิง ฉันไม่ต้องการใช้การสะท้อนเนื่องจากปัญหาด้านประสิทธิภาพ

ใครสามารถแนะนำวิธีการบางอย่างที่สิ่งนี้สามารถเขียนในแบบที่หรูหรามากขึ้น?


3
ทำไมคุณใช้ยาชื่อสามัญนี้ตั้งแต่แรก? ไม่มีอะไรทั่วไปเกี่ยวกับทั้งสองฟังก์ชั่น
Luaan

1
@ Luaan นี่เป็นตัวอย่างที่ง่ายของการเปลี่ยนแปลงของรูปแบบนามธรรมของโรงงาน ลองนึกภาพว่ามีคลาสมากมายที่สืบทอด ClassA หรือ ClassB และ ClassA และ ClassB เป็นคลาสนามธรรม คลาสที่สืบทอดมาจะไม่มีข้อมูลเพิ่มเติมและจำเป็นต้องมีอินสแตนซ์ แทนที่จะเขียนโรงงานสำหรับแต่ละโรงงานฉันเลือกใช้ยาชื่อสามัญ
Vladimir Stokic

6
คุณสามารถใช้การไตร่ตรองหรือการเปลี่ยนแปลงถ้าคุณมั่นใจว่าพวกเขาจะไม่ทำลายมันในการเปิดตัวในอนาคต
Casey

นี่คือการร้องเรียนที่ใหญ่ที่สุดของฉันเกี่ยวกับยาชื่อสามัญว่ามันไม่สามารถทำได้
Joshua

1
@ โจชัวฉันคิดว่ามันเป็นปัญหากับอินเทอร์เฟซที่ไม่รองรับ "การพิมพ์เป็ด"
เอียน

คำตอบ:


49

เพิ่มส่วนต่อประสานพร็อกซี (บางครั้งเรียกว่าอะแดปเตอร์บางครั้งมีความแตกต่างเล็กน้อย) นำไปใช้LogicToBeAppliedในแง่ของพร็อกซีจากนั้นเพิ่มวิธีสร้างอินสแตนซ์ของพร็อกซีนี้จากสอง lambdas: หนึ่งสำหรับคุณสมบัติ

interface IProxy
{
    int Property { get; set; }
}
class LambdaProxy : IProxy
{
    private Function<int> getFunction;
    private Action<int> setFunction;
    int Property
    {
        get { return getFunction(); }
        set { setFunction(value); }
    }
    public LambdaProxy(Function<int> getter, Action<int> setter)
    {
        getFunction = getter;
        setFunction = setter;
    }
}

ตอนนี้เมื่อใดก็ตามที่คุณต้องการส่งผ่าน IProxy แต่มีอินสแตนซ์ของคลาสบุคคลที่สามคุณสามารถส่งผ่าน lambdas:

A a = new A();
B b = new B();
IProxy proxyA = new LambdaProxy(() => a.Property, (val) => a.Property = val);
IProxy proxyB = new LambdaProxy(() => b.Property, (val) => b.Property = val);
proxyA.Property = 12; // mutates the proxied `a` as well

นอกจากนี้คุณสามารถเขียนตัวช่วยอย่างง่ายเพื่อสร้างอินสแตนซ์ของ LamdaProxy จากอินสแตนซ์ของ A หรือ B พวกเขายังสามารถเป็นวิธีการขยายเพื่อให้คุณมีสไตล์ "คล่องแคล่ว":

public static class ProxyExtension
{
    public static IProxy Proxied(this A a)
    {
      return new LambdaProxy(() => a.Property, (val) => a.Property = val);
    }

    public static IProxy Proxied(this B b)
    {
      return new LambdaProxy(() => b.Property, (val) => b.Property = val);
    }
}

และตอนนี้การก่อสร้างผู้รับมอบฉันทะมีลักษณะเช่นนี้:

IProxy proxyA = new A().Proxied();
IProxy proxyB = new B().Proxied();

สำหรับโรงงานของคุณฉันจะดูว่าคุณสามารถปรับโครงสร้างใหม่เป็นวิธี "โรงงาน" หลักที่ยอมรับ IProxy และดำเนินการกับตรรกะทั้งหมดและวิธีอื่น ๆ ที่เพิ่งผ่านnew A().Proxied()หรือnew B().Proxied():

public class LogicToBeApplied
{
    public A CreateA() {
      A a = new A();
      InitializeProxy(a.Proxied());
      return a; // or maybe return the proxy if you'd rather use that
    }

    public B CreateB() {
      B b = new B();
      InitializeProxy(b.Proxied());
      return b;
    }

    private void InitializeProxy(IProxy proxy)
    {
        proxy.IntProperty = 50;
    }
}

ไม่มีวิธีที่จะทำเทียบเท่ากับรหัส C ++ ของคุณใน C # เพราะแม่แบบ C ++ อาศัยการพิมพ์เชิงโครงสร้าง ตราบใดที่สองคลาสมีชื่อเมธอดและลายเซ็นเหมือนกันใน C ++ คุณสามารถเรียกใช้เมธอดนั้นได้ทั้งสองคลาส C # มีการพิมพ์เล็กน้อย - ชื่อของคลาสหรือส่วนต่อประสานเป็นส่วนหนึ่งของประเภท ดังนั้นคลาสAและBไม่สามารถได้รับการปฏิบัติเหมือนกันในทุก ๆ ความจุเว้นแต่จะมีการกำหนดความสัมพันธ์ "เป็น" อย่างชัดเจนผ่านการสืบทอดหรือการใช้อินเตอร์เฟส

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

public class ReflectiveProxier 
{
    public object proxyReflectively(object proxied)
    {
        PropertyInfo prop = proxied.GetType().GetProperty("Property");
        return new LambdaProxy(
            () => prop.GetValue(proxied),
            (val) => prop.SetValue(proxied, val));
     }
}

สิ่งนี้จะล้มเหลวอย่างไม่เหมาะสมเมื่อวัตถุที่ได้รับมีชนิดไม่ถูกต้อง การสะท้อนกลับมีความเป็นไปได้ที่จะเกิดความล้มเหลวในระบบ C # type ไม่สามารถป้องกันได้ โชคดีที่คุณสามารถหลีกเลี่ยงการสะท้อนได้จนกว่าภาระการบำรุงรักษาของผู้ช่วยเหลือจะมากเกินไปเพราะคุณไม่จำเป็นต้องดัดแปลงอินเทอร์เฟซ IProxy หรือการใช้ LambdaProxy เพื่อเพิ่มน้ำตาลสะท้อนแสง

ส่วนหนึ่งของเหตุผลที่งานนี้LambdaProxyคือ "ทั่วไปมากที่สุด"; มันสามารถปรับค่าใด ๆ ที่ใช้ "จิตวิญญาณ" ของสัญญา IProxy เนื่องจากการดำเนินการของ LambdaProxy นั้นถูกกำหนดโดยฟังก์ชัน getter และ setter ที่กำหนดไว้อย่างสมบูรณ์ มันยังใช้งานได้ถ้าชั้นเรียนมีชื่อแตกต่างกันสำหรับคุณสมบัติหรือประเภทที่แตกต่างกันที่มีเหตุผลและปลอดภัยเป็นints หรือมีวิธีการแมปแนวคิดที่Propertyควรจะแสดงถึงคุณสมบัติอื่น ๆ ของชั้นเรียน การให้ทางอ้อมของฟังก์ชั่นให้ความยืดหยุ่นสูงสุด


วิธีการที่น่าสนใจมากและสามารถใช้เรียกฟังก์ชั่นนี้ได้อย่างแน่นอน แต่สามารถใช้กับโรงงานได้หรือไม่ซึ่งฉันต้องสร้างวัตถุของ ClassA และ ClassB หรือไม่
Vladimir Stokic

@VladimirStokic ดูการแก้ไขฉันได้ขยายเรื่องนี้ไปเล็กน้อย
Jack

แน่นอนว่าวิธีนี้ยังต้องการให้คุณแมปคุณสมบัติอย่างชัดเจนสำหรับแต่ละประเภทด้วยความเป็นไปได้ที่เพิ่มขึ้นของข้อผิดพลาดรันไทม์หากฟังก์ชันการทำแผนที่ของคุณเป็นบั๊ก
Ewan

เป็นทางเลือกที่ReflectiveProxierสามารถให้คุณสามารถสร้างพร็อกซี่โดยใช้dynamicคำหลัก? ดูเหมือนว่าคุณจะมีปัญหาพื้นฐานเหมือนกัน (เช่นข้อผิดพลาดที่ติดอยู่ที่รันไทม์เท่านั้น) แต่ไวยากรณ์และการบำรุงรักษาจะง่ายกว่ามาก
Bobson

1
@Jack - ยุติธรรมเพียงพอ ฉันเพิ่มคำตอบของฉันสาธิตมัน มันเป็นคุณสมบัติที่มีประโยชน์มากในบางสถานการณ์ที่หายาก (เช่นนี้)
Bobson

12

นี่คือโครงร่างวิธีการใช้อะแดปเตอร์โดยไม่ต้องสืบทอดจาก A และ / หรือ B โดยมีความเป็นไปได้ที่จะใช้พวกมันสำหรับวัตถุ A และ B ที่มีอยู่:

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : IAdapter
{
    A _a;

    public AAdapter()  // use this if you want to have the "logic" part create new objects
    {
        _a=new A();
    }

    public AAdapter(A a) // if you need an adapter for an existing object afterwards
    {
       _a=a;
    }

    public int Property
    {
        get { return _a.Property; }
        set { _a.Property = value; }
    }

    public A {get{return _a; } } // to provide access for non-generic code
}

class BAdapter 
{
     // analogously
}

โดยทั่วไปแล้วฉันจะชอบอแด็ปเตอร์วัตถุประเภทนี้มากกว่าพร็อกซีของชั้นเรียนพวกเขาหลีกเลี่ยงปัญหาที่น่าเกลียดที่คุณสามารถรับมรดกได้ ตัวอย่างเช่นการแก้ปัญหานี้จะทำงานแม้ว่า A และ B เป็นคลาสที่ปิดผนึก


ทำไมnew int Property? คุณไม่ได้เงาอะไรเลย
pinkfloydx33

@ pinkfloydx33: แค่พิมพ์ผิดเปลี่ยนเป็นขอบคุณ
Doc Brown

9

คุณสามารถปรับClassAและClassBผ่านทางอินเทอร์เฟซทั่วไป วิธีนี้รหัสของคุณLogicAToBeAppliedยังคงเหมือนเดิม ไม่แตกต่างจากที่คุณคิด

class A
{
    public int Property { get; set; }
}
class B
{
    public int Property { get; set; }
}

interface IAdapter
{
    int Property { get; set; }
}

class LogicToBeApplied<T> where T : IAdapter, new()
{
    public T Create()
    {
        var ret = new T();
        ret.Property = 50;
        return ret;
    }
}

class AAdapter : A, IAdapter { }

class BAdapter : B, IAdapter { }

1
+1 ที่ใช้รูปแบบตัวแปลงเป็นโซลูชัน OOP แบบดั้งเดิมที่นี่ มันขึ้นจากอะแดปเตอร์กว่าพร็อกซี่ตั้งแต่เราปรับA, Bประเภทที่จะติดต่อกัน ข้อได้เปรียบที่สำคัญคือเราไม่ต้องทำซ้ำตรรกะทั่วไป ข้อเสียคือตรรกะในขณะนี้ instantiates wrapper / proxy แทนประเภทที่เกิดขึ้นจริง
amon

5
ปัญหาเกี่ยวกับวิธีแก้ปัญหานี้คือคุณไม่สามารถนำวัตถุสองประเภทที่พิมพ์ A และ B มาแปลงเป็น AProxy และ BProxy จากนั้นใช้ LogicToBeApplied ปัญหานี้สามารถแก้ไขได้โดยใช้การรวมแทนการสืบทอด (resp. ใช้วัตถุพร็อกซีไม่ได้มาจาก A และ B แต่โดยการอ้างอิงถึงวัตถุ A และ B) อีกตัวอย่างหนึ่งของการใช้การสืบทอดอย่างไม่ถูกต้องทำให้เกิดปัญหาได้อย่างไร
Doc Brown

@DocBrown ในกรณีนี้เป็นอย่างไรบ้าง?
Vladimir Stokic

1
@ แจ็ค: วิธีแก้ปัญหาประเภทนี้เหมาะสมเมื่อLogicToBeAppliedมีความซับซ้อนบางอย่างและไม่ควรทำซ้ำสองจุดในฐานรหัสโดยไม่มีสถานการณ์ จากนั้นรหัสสำเร็จรูปเพิ่มเติมมักจะเล็กน้อย
Doc Brown

1
@Jack ความซ้ำซ้อนอยู่ที่ไหน สองคลาสไม่มีอินเทอร์เฟซทั่วไป คุณสามารถสร้างห่อที่ไม่ได้ติดต่อกัน คุณใช้อินเทอร์เฟซทั่วไปนั้นเพื่อใช้ตรรกะของคุณ มันไม่เหมือนกับความซ้ำซ้อนแบบเดียวกันที่ไม่มีอยู่ในรหัส C ++ แต่มันซ่อนอยู่หลังการสร้างรหัสเล็กน้อย หากคุณรู้สึกอย่างนั้นเกี่ยวกับสิ่งต่าง ๆ ที่มีลักษณะเหมือนกันถึงแม้ว่ามันจะไม่เหมือนกันคุณสามารถใช้ T4s หรือระบบ templating อื่น ๆ ได้เสมอ
Luaan

8

รุ่น C ++ ใช้งานได้เฉพาะเพราะเทมเพลตใช้ "การพิมพ์เป็ดแบบคงที่" - ทุกอย่างจะต้องคอมไพล์ตราบใดที่ประเภทนั้นมีชื่อที่ถูกต้อง มันเป็นเหมือนระบบมาโคร ระบบ generics ของ C # และภาษาอื่นทำงานแตกต่างกันมาก

คำตอบของ devnull และ Doc Brown แสดงให้เห็นว่ารูปแบบของอะแดปเตอร์สามารถใช้ในการรักษาอัลกอริทึมของคุณอย่างไรและยังคงใช้งานได้ตามประเภท ... โดยมีข้อ จำกัด สองประการ ตอนนี้คุณกำลังสร้างประเภทที่แตกต่างจากที่คุณต้องการจริงๆ

ด้วยการใช้เล่ห์เหลี่ยมเล็กน้อยคุณสามารถใช้ชนิดที่ต้องการโดยไม่เปลี่ยนแปลง อย่างไรก็ตามตอนนี้เราต้องแยกการโต้ตอบทั้งหมดกับประเภทเป้าหมายลงในอินเทอร์เฟซแยกต่างหาก ที่นี่การโต้ตอบเหล่านี้เป็นการก่อสร้างและการมอบหมายอสังหาริมทรัพย์:

interface IInteractions<T> {
  T Instantiate();
  void AssignProperty(T target, int value);
}

ในการตีความ OOP นี่จะเป็นตัวอย่างของรูปแบบกลยุทธ์แม้ว่าจะผสมกับยาชื่อสามัญ

จากนั้นเราสามารถเขียนตรรกะของคุณเพื่อใช้การโต้ตอบเหล่านี้:

public class LogicBToBeApplied<T>
{
    public T CreateElement(IInteractions<T> interactions)
    {
        T retVal = interactions.Instantiate();
        interactions.AssignProperty(retVal, 50);
        return retVal;
    }
}

นิยามการโต้ตอบจะมีลักษณะดังนี้:

class Interactions_ClassA : IInteractions<ClassA> {
  public override ClassA Instantiate() { return new ClassA(); }
  public override void AssignProperty(ClassA target, int value) { target.IntProperty = value; }
}

ข้อเสียอย่างใหญ่หลวงของวิธีการนี้คือโปรแกรมเมอร์ต้องการเขียนและส่งอินสแตนซ์การโต้ตอบเมื่อเรียกใช้ลอจิก สิ่งนี้ค่อนข้างคล้ายกับโซลูชันที่ใช้รูปแบบอะแดปเตอร์ แต่ค่อนข้างกว้างกว่าเล็กน้อย

จากประสบการณ์ของฉันนี่เป็นสิ่งที่ใกล้เคียงที่สุดที่คุณสามารถเข้าถึงฟังก์ชั่นเทมเพลตในภาษาอื่น ๆ มีการใช้เทคนิคที่คล้ายกันใน Haskell, Scala, Go และ Rust เพื่อนำอินเตอร์เฟสไปใช้นอกนิยามประเภท อย่างไรก็ตามในภาษาเหล่านี้ขั้นตอนคอมไพเลอร์ในและเลือกอินสแตนซ์การโต้ตอบที่ถูกต้องโดยปริยายดังนั้นคุณจะไม่เห็นอาร์กิวเมนต์พิเศษ นี่ก็คล้ายกับวิธีการขยาย C # แต่ไม่ จำกัด วิธีการคงที่


แนวทางที่น่าสนใจ ไม่ใช่ตัวเลือกแรกของฉัน แต่ฉันคิดว่ามันมีข้อดีเมื่อเขียนกรอบงานหรืออะไรแบบนั้น
Doc Brown

8

หากคุณต้องการระวังลมจริงๆคุณสามารถใช้ "ไดนามิก" เพื่อทำให้คอมไพเลอร์ดูแลความน่ารำคาญของภาพสะท้อนทั้งหมดสำหรับคุณ สิ่งนี้จะส่งผลให้เกิดข้อผิดพลาดรันไทม์ถ้าคุณส่งวัตถุไปยัง SetSomeProperty ที่ไม่มีคุณสมบัติชื่อ SomeProperty

using System;

namespace ConsoleApplication3
{
    class A
    {
        public int SomeProperty { get; set; }
    }

    class B
    {
        public int SomeProperty { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var a = new A();
            var b = new B();

            SetSomeProperty(a, 7);
            SetSomeProperty(b, 12);

            Console.WriteLine($"a.SomeProperty = {a.SomeProperty}, b.SomeProperty = {b.SomeProperty}");
        }

        static void SetSomeProperty(dynamic obj, int value)
        {
            obj.SomeProperty = value;
        }
    }
}

4

คำตอบอื่น ๆ ระบุปัญหาได้อย่างถูกต้องและเสนอวิธีแก้ปัญหาที่ใช้การได้ C # ไม่สนับสนุน (โดยทั่วไป) "การพิมพ์เป็ด" ("ถ้ามันเดินเหมือนเป็ด ... ") ดังนั้นจึงไม่มีทางบังคับให้คุณClassAและClassBเปลี่ยนได้หากพวกเขาไม่ได้ออกแบบมาอย่างนั้น

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

C # มีdynamicคำหลักที่เหมาะสำหรับสถานการณ์เช่นนี้ มันบอกคอมไพเลอร์ว่า "ฉันไม่รู้ว่ามันเป็นแบบไหนจนกระทั่งรันไทม์ (และอาจจะไม่ถึงตอนนั้น) ดังนั้นให้ฉันทำอะไรที่มัน"

คุณสามารถสร้างฟังก์ชั่นที่คุณต้องการ:

public class LogicToBeApplied<T> where T : new()
{
    public static T CreateElement()
    {
        dynamic retVal = new T(); // This doesn't care what type T is.
        retVal.IntProperty = 50;  // This will fail at runtime if there is no "IntProperty" 
                                  // or it doesn't accept an int.
        return retVal;            // Once again, we don't care what it is.
    }
}

สังเกตการใช้staticคำสำคัญเช่นกัน ที่ช่วยให้คุณใช้สิ่งนี้เป็น:

A classAElement = LogicToBeApplied<A>.CreateElement();
B classBElement = LogicToBeApplied<B>.CreateElement();

ไม่มีนัยยะเกี่ยวกับประสิทธิภาพของภาพขนาดใหญ่ในการใช้dynamicวิธีการตีครั้งเดียว (และเพิ่มความซับซ้อน) ในการใช้ Reflection ในครั้งแรกที่รหัสของคุณถึงการโทรแบบไดนามิกที่มีประเภทเฉพาะจะมีค่าใช้จ่ายเล็กน้อย แต่การโทรซ้ำ ๆ จะเร็วเท่ากับรหัสมาตรฐาน อย่างไรก็ตามคุณจะได้รับRuntimeBinderExceptionหากคุณพยายามส่งผ่านบางสิ่งที่ไม่มีคุณสมบัตินั้นและไม่มีวิธีที่ดีในการตรวจสอบล่วงหน้า คุณอาจต้องการจัดการข้อผิดพลาดนั้นโดยเฉพาะในลักษณะที่มีประโยชน์


สิ่งนี้อาจช้า แต่บ่อยครั้งที่รหัสช้าไม่ใช่ปัญหา
เอียน

@Ian - จุดดี ฉันเพิ่มอีกเล็กน้อยเกี่ยวกับประสิทธิภาพ จริงๆแล้วมันไม่เลวร้ายอย่างที่คุณคิดถ้าคุณใช้คลาสเดียวกันซ้ำในจุดเดียวกัน
Bobson

จำไว้ว่าในเทมเพลต C ++ นั้นไม่มีแม้แต่วิธีการเสมือนจริง
เอียน

2

คุณสามารถใช้การสะท้อนเพื่อดึงคุณสมบัติออกตามชื่อ

public class logic 
{
    public object getNew<T>() where T : new()
    {
        T ret = new T();
        try
        {
            var property = typeof(T).GetProperty("IntProperty");
            if (property != null && property.PropertyType == typeof(int))
            {
                property.SetValue(ret, 50);
            }
        }
        catch (AmbiguousMatchException)
        {
            //hmm..
        }
        return ret;
    }
}

แน่นอนคุณเสี่ยงข้อผิดพลาด runtime ด้วยวิธีนี้ C # พยายามหยุดคุณทำอะไร

ฉันได้อ่านที่ไหนสักแห่งว่า C # เวอร์ชันในอนาคตจะช่วยให้คุณสามารถส่งวัตถุเป็นอินเทอร์เฟซที่พวกเขาไม่สืบทอด แต่จับคู่กัน ซึ่งจะแก้ปัญหาของคุณ

(ฉันจะพยายามขุดบทความ)

วิธีอื่นแม้ว่าฉันไม่แน่ใจว่าจะช่วยให้คุณรหัสใด ๆ จะ subclass ทั้ง A และ B และยังสืบทอด Interface กับ IntProperty

public interface IIntProp {
    public int IntProperty {get, set}
}

public class A2 : A, IIntProp {}

public class B2 : B, IIntProp {}

ความเป็นไปได้ของข้อผิดพลาดรันไทม์และปัญหาด้านประสิทธิภาพเป็นสาเหตุที่ฉันไม่ต้องการไปด้วย อย่างไรก็ตามฉันสนใจที่จะอ่านบทความที่คุณกล่าวถึงในคำตอบของคุณ รอคอยที่จะอ่านมัน
Vladimir Stokic

1
คุณมีความเสี่ยงเช่นเดียวกันกับโซลูชัน c ++ ของคุณหรือไม่
Ewan

4
@Ewan no, c ++ ตรวจสอบสมาชิก ณ เวลารวบรวม
Caleth

Reflection หมายถึงปัญหาการปรับให้เหมาะสมและ (สำคัญกว่า) การดีบักข้อผิดพลาดรันไทม์ยาก การสืบทอดและอินเทอร์เฟซทั่วไปหมายถึงการประกาศคลาสย่อยล่วงหน้าสำหรับทุกคลาสเหล่านี้ (ไม่มีวิธีที่จะทำให้ชื่อไม่ระบุชื่อ) และไม่ทำงานหากไม่ได้ใช้ชื่อคุณสมบัติเดียวกันทุกครั้ง
แจ็ค

1
@Jack มีข้อเสีย แต่พิจารณาภาพสะท้อนที่ถูกใช้อย่างกว้างขวางใน Mappers, serializers, พึ่งพาการฉีดกรอบ ฯลฯ และที่มีเป้าหมายที่จะทำมันมีจำนวนน้อยที่สุดของการทำสำเนารหัส
Ewan

0

ฉันแค่ต้องการใช้implicit operatorการแปลงร่วมกับแนวทาง / แลมบ์ดาของคำตอบของแจ็ค AและBถือว่าเป็น:

// A and B are mutable reference types

class A
{
  public int IntProperty { get; set; }
}

class B
{
  public int IntProperty { get; set; }
}

จากนั้นจะเป็นการง่ายที่จะได้รับไวยากรณ์ที่ดีด้วยการแปลงที่ผู้ใช้กำหนดโดยปริยาย (ไม่จำเป็นต้องมีวิธีการขยายหรือคล้ายกัน):

// Adapter is an immutable type. However, the delegate instances have a captured reference to an A or a B (closure semantics)
struct Adapter
{
  readonly Func<int> getter;
  readonly Action<int> setter;

  Adapter(Func<int> getter, Action<int> setter)
  {
    this.getter = getter;
    this.setter = setter;
  }

  public int IntProperty
  {
    get { return getter(); }
    set { setter(value); }
  }

  public static implicit operator Adapter(A a) => new Adapter(() => a.IntProperty, x => a.IntProperty = x);
  public static implicit operator Adapter(B b) => new Adapter(() => b.IntProperty, x => b.IntProperty = x);

  public A CloneToA() => new A { IntProperty = getter(), };
  public B CloneToB() => new B { IntProperty = getter(), };
}

ภาพประกอบการใช้งาน:

class LogicToBeApplied
{
  public static A CreateA()
  {
    var a = new A();
    Initialize(a);
    return a;
  }
  public static B CreateB()
  {
    var b = new B();
    Initialize(b);
    return b;
  }

  static void Initialize(Adapter a)
  {
    a.IntProperty = 50;
  }
}

Initializeวิธีการแสดงให้เห็นว่าคุณสามารถทำงานกับAdapterโดยไม่ต้องดูแลเกี่ยวกับการไม่ว่าจะเป็นAหรือBหรือสิ่งอื่นใด การขอร้องของInitializeวิธีการแสดงให้เห็นว่าเราไม่ต้องการการหล่อ (หรือมองเห็น) ใด ๆ.AsProxy()หรือคล้ายกันในการรักษาคอนกรีตAหรือBในฐานะที่เป็นAdapterเป็น

ลองพิจารณาว่าคุณต้องการที่จะโยนการArgumentNullExceptionแปลงที่ผู้ใช้กำหนดหรือไม่ถ้าการโต้แย้งที่ส่งนั้นเป็นการอ้างอิงที่เป็นโมฆะหรือไม่

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