ไม่สนับสนุนการเริ่มต้นคอนสตรัคเตอร์โดยทางโปรแกรมหรือไม่

ฉันมีคลาส Foo และคลาสคงที่ FooFactory ซึ่งใช้เพื่อสร้างอินสแตนซ์ของคลาสที่ได้รับ Foo และ Foo ผ่าน API ต่อไปนี้:

public static class FooFactory {
  public static T Create<T>() where T : Foo, new() { 
    ...
    return new T();
  }
}

ความเชี่ยวชาญพิเศษ new() ของพารามิเตอร์ประเภท T กำหนดให้ Foo มีตัวสร้างเริ่มต้น public กล่าวอีกนัยหนึ่ง ไม่มีอะไรขัดขวางผู้ใช้ Foo ในการเริ่มต้นคลาสโดยตรงผ่าน Constructor

อย่างไรก็ตาม FooFactory มีวัตถุประสงค์เพื่อติดตามอินสแตนซ์ Foo ทั้งหมด ดังนั้นฉันต้องการบังคับให้ผู้ใช้สร้างอินสแตนซ์ที่ได้รับ Foo และ Foo ทั้งหมดผ่านทาง FooFactory

สิ่งที่ใกล้เคียงที่สุดที่ฉันสามารถป้องกันการเริ่มต้นคอนสตรัคเตอร์โดยตรงได้คือการตกแต่งคอนสตรัคเตอร์เริ่มต้น Foo ด้วยแอตทริบิวต์ [Obsolete("message", error: true)]:

public class Foo {      
  [Obsolete("Fail", true)]
  public Foo() { ... }
}

ด้วยการตกแต่งนี้ โค้ดจะไม่คอมไพล์เมื่อฉันเรียกใช้คอนสตรัคเตอร์เริ่มต้น Foo โดยตรง ในขณะที่การเริ่มต้นผ่าน FooFactory.Create<Foo>() ใช้งานได้

แต่ด้วยการตกแต่ง [Obsolete] นี้ ฉันยังคงมีปัญหากับคลาสที่ได้รับ สิ่งต่อไปนี้จะไม่คอมไพล์ด้วยซ้ำเนื่องจากการตั้งค่า error: true สำหรับตัวสร้างเริ่มต้น Foo:

public class Bar : Foo { ... }

public class Baz : Foo { public Baz() : base() { ... } }

ฉันสามารถประกาศโอเวอร์โหลดคอนสตรัคเตอร์ protected Foo ที่คลาสที่ได้รับเรียกใช้แทนคอนสตรัคเตอร์เริ่มต้น แต่จากนั้นฉันจะไม่สามารถป้องกันการเริ่มต้นโดยตรงของคลาสที่ได้รับโดยทางโปรแกรมได้

ถ้าฉันตั้งค่า error ใน ObsoleteAttribute เป็น false ฉันจะได้รับคำเตือนการคอมไพล์แทน แต่ฉันอยากจะมีความท้อแท้มากกว่าคำเตือน...

มีวิธีใดบ้างที่ฉันสามารถป้องกันการเรียกใช้คอนสตรัคเตอร์เริ่มต้นโดยตรงโดยทางโปรแกรมสำหรับทั้ง Foo และคลาสที่ได้รับเมื่อจำเป็นต้องประกาศคอนสตรัคเตอร์เริ่มต้น public?


person Anders Gustafsson    schedule 04.09.2017    source แหล่งที่มา
comment
สร้าง Constructor internal ไปที่ไลบรารีและลบข้อจำกัด new() ซึ่งหมายความว่าคุณต้องรับผิดชอบในการสร้าง Foo ทุกประเภท   -  person Nkosi    schedule 04.09.2017
comment
หากฉันจำไม่ผิด ข้อกำหนด new() จะใช้ได้ก็ต่อเมื่อมีการประกาศตัวสร้าง public   -  person Anders Gustafsson    schedule 04.09.2017
comment
คุณจะต้องลบข้อจำกัดออกไป   -  person Nkosi    schedule 04.09.2017
comment
นั่นไม่ใช่ทางเลือก...   -  person Anders Gustafsson    schedule 04.09.2017
comment
นั่นคือวิธีการทำงานของข้อจำกัด คุณไม่สามารถรับสิ่งที่คุณต้องการได้   -  person Nkosi    schedule 04.09.2017
comment
เมื่ออยู่ในหลุมให้หยุดขุด ทำให้ตัวสร้างของ Foo ทำการติดตามอินสแตนซ์ ผ่านการเรียกใช้ฟิลด์คงที่และ this.GetType()   -  person Jeroen Mostert    schedule 04.09.2017
comment
หากคุณสามารถควบคุม Foo และประเภทที่ได้รับมาได้อย่างสมบูรณ์ คุณสามารถทำให้เป็นประเภทภายในและเปิดเผยอินเทอร์เฟซที่พวกเขานำไปใช้ได้ วิธีการสร้างของคุณอาจใช้อินเทอร์เฟซเป็นพารามิเตอร์ และผ่านการไตร่ตรองเพื่อกำหนดว่าคลาสใดที่จะสร้างอินสแตนซ์ อย่างไรก็ตาม: หากทำเช่นนี้ คุณจะสูญเสียการตรวจสอบเวลาคอมไพล์ของประเภทพารามิเตอร์สำหรับ Create   -  person Johan Donne    schedule 04.09.2017
comment
หวัดดีครับเจโรน ตอนแรกฉันทำสิ่งนี้ แต่ลบออกเมื่อฉันปรับโครงสร้างชั้นเรียนใหม่ เมื่อตรวจสอบซอร์สโค้ดปัจจุบันอีกครั้ง ฉันพบว่ามันสมเหตุสมผลที่จะแนะนำการลงทะเบียนในตัวสร้างอีกครั้ง ขอบคุณ!   -  person Anders Gustafsson    schedule 04.09.2017
comment
เยี่ยมมาก เพราะตามที่ระบุไว้ ความต้องการของคุณเป็นไปไม่ได้เลยที่จะตอบสนองเป็นอย่างอื่น โดยเฉพาะอย่างยิ่ง เป็นไปไม่ได้ที่จะห้ามการประกาศ Baz ที่สามารถสร้างได้โดยตรง ผู้เขียนสามารถเสมอจัดเตรียมตัวสร้างสาธารณะสำหรับผู้ใช้ที่ข้ามความต้องการใดๆ ของคุณ ไม่ว่า Foo จะถูกตั้งค่าอย่างไรก็ตาม หากคุณถือว่าผู้เขียนสมรู้ร่วมคิดอยู่เสมอและสามารถสันนิษฐานได้ว่าพวกเขาจะประพฤติตัวได้ คำแนะนำที่สองของฉันคือละทิ้งข้อจำกัด new() และส่งต่อการเรียกคอนสตรัคเตอร์เป็น Func<T> ไปยังเมธอดภายใน วิธีนี้จะรักษาความปลอดภัยของประเภทแต่จะยุ่งยากสำหรับผู้เขียน   -  person Jeroen Mostert    schedule 04.09.2017
comment
@JohanDonne ขอบคุณสำหรับข้อเสนอแนะของคุณ แต่นี่ไม่สอดคล้องกับสิ่งที่ฉันกำลังมองหาจริงๆ เหตุผลหนึ่งที่ยังคงใช้ความเชี่ยวชาญพิเศษ new() ก็คือสามารถพกพาข้ามแพลตฟอร์มได้ การสะท้อนโดยเฉพาะอย่างยิ่งกับวิธีที่ไม่ใช่ public อาจเล่นได้ไม่ดีในทุกแพลตฟอร์มเสมอไป   -  person Anders Gustafsson    schedule 04.09.2017
comment
บุคคลที่สามจะต้องสามารถซับคลาส Foo ได้หรือไม่ หากนั่นไม่ใช่ข้อกำหนด การแก้ปัญหาของคุณก็จะง่ายกว่ามาก   -  person Eric Lippert    schedule 04.09.2017
comment
ขอบคุณที่มองเรื่องนี้เอริค ใช่ ความตั้งใจของฉันคือบุคคลที่สามควรจะสามารถซับคลาส Foo ได้   -  person Anders Gustafsson    schedule 04.09.2017


คำตอบ (1)


คุณไม่สามารถสืบทอดจากคลาสด้วยตัวสร้างส่วนตัวเท่านั้น แต่คุณสามารถทำให้ตัวสร้างที่ว่างเปล่าของคุณเป็นส่วนตัวและเพิ่มตัวสร้างด้วยคุณสมบัติเช่น createdInsideFooFactory คุณควรลบข้อจำกัด new() ในโรงงานของคุณออกด้วย นี่เป็นแฮ็ก แต่จะทำให้นักพัฒนาทราบว่าควรสร้างอินสแตนซ์ภายใน foo Factory

public class Foo
{
    private Foo()
    {
    }

    protected Foo(bool createdInsideFooFactory)
    {
        if (createdInsideFooFactory) return;

        throw new MyException();
    }
}
person Alex Zaitsev    schedule 04.09.2017
comment
ข้อความของคุณที่คุณไม่สามารถสืบทอดจากคลาสที่มีตัวสร้างส่วนตัวนั้นเป็นเท็จ คุณช่วยหาวิธีการเขียนโปรแกรมที่มีคลาสที่มีตัวสร้างส่วนตัวและคลาสที่ได้รับได้หรือไม่? มีอย่างน้อยสองวิธีที่จะทำ - person Eric Lippert; 04.09.2017
comment
@EricLippert: ฉันทำไม่ได้ โปรดให้ความกระจ่างแก่เราด้วย วิธีที่ชัดเจนที่ฉันคิดได้คือ 1) จัดเตรียมคอนสตรัคเตอร์เพิ่มเติมที่ไม่ใช่ส่วนตัว (แต่คำตอบที่ระบุคอนสตรัคเตอร์ส่วนตัว เท่านั้น ดังนั้นนั่นคือการโกงและ 2) ทำให้คลาสที่ได้รับเป็นคลาสที่ซ้อนกัน (แต่ ที่ต้องควบคุมคำจำกัดความของ Foo ซึ่งเป็นการโกงเช่นกัน) - person Jeroen Mostert; 05.09.2017
comment
ใช่ครับ เป็นแค่เอกชนเท่านั้น คลาสที่ซ้อนกันเป็นวิธีที่ง่าย ยังมีวิธีที่ยาก - person Eric Lippert; 05.09.2017
comment
คำแนะนำ: คลาสที่ได้รับ มี ที่จะเรียกคลาสพื้นฐาน ctor หรือไม่ - person Eric Lippert; 05.09.2017
comment
@EricLippert: ถ้าเราทำทุกอย่างยกเว้นข้อมูลจำเพาะภาษา C# ก็ใช่ โดยอาจเรียกตัวสร้างคลาสพื้นฐาน (โดยชัดแจ้งหรือโดยนัย) หรือตัวสร้างอื่นของคลาสเดียวกัน และห้ามใช้การวนซ้ำ (ทางตรงหรือทางอ้อม) ดังนั้นท้ายที่สุดแล้วจึงต้องเรียกสำเนาลับถึง แน่นอนว่า ณ รันไทม์คุณสามารถหลีกเลี่ยงการเรียกมันผ่านข้อยกเว้นได้ แต่นั่นไม่ได้เป็นการหลีกเลี่ยงการประกาศ แม้ว่าจะมีวิธีที่น่ากลัวในการทำสิ่งนี้ให้สำเร็จ แต่ฉันค่อนข้างแน่ใจว่าคุณจะต้องจบลงด้วยชั้นเรียนที่ไม่มีอินสแตนซ์ ตอนนี้ทำหกไปแล้ว พวกเราบางคนไม่ได้เขียนคอมไพเลอร์ :-ป - person Jeroen Mostert; 05.09.2017
comment
@AlexZaitsev ด้วยเหตุผลหลายประการ มันไม่ใช่ตัวเลือกสำหรับฉันที่จะลบ new() ข้อ จำกัด สิ่งที่คุณเสนอจะทำให้สามารถกำหนดคอนสตรัคเตอร์เริ่มต้นของคลาสย่อยได้ ซึ่งถูกต้อง แต่ฉันไม่เห็นวิธีที่จะควบคุมว่าคอนสตรัคเตอร์เริ่มต้นนั้นถูกเรียกโดยตรงหรือผ่านทางโรงงานหรือไม่ นั่นคือคุณสามารถมี public Bar() : Foo(true) หรือ public Bar() : Foo(false) ได้ แต่ฉันไม่เห็นวิธีที่จะควบคุมว่าตัวสร้างนี้ถูกเรียกมาจากโรงงานหรือโดยตรง - person Anders Gustafsson; 05.09.2017
comment
@JeroenMostert: อ่า เทคนิคที่สองถูกลบออกไปแล้ว เคยถูกกฎหมายที่จะมีลูปในการเรียกคอนสตรัคเตอร์นี้ นั่นช่วยให้คุณเรียกใช้ ctor ฐานได้ ซึ่งช่วยให้โปรแกรมคอมไพล์ได้ จากนั้นคุณทำให้เกิดข้อยกเว้นในการก่อสร้าง จับข้อยกเว้น และฟื้นคืนวัตถุผ่าน Finalizer และคุณมีอินสแตนซ์ของคลาสที่ได้รับที่ไม่เคยเรียกว่าคลาสพื้นฐาน ctor - person Eric Lippert; 05.09.2017
comment
@EricLippert: แน่นอนว่าตอนนี้ฉันรู้สึกโง่ที่ไม่คิดอย่างนั้น :-P ความจริงที่ว่าคุณสามารถฟื้นคืนวัตถุที่สร้างขึ้นบางส่วนในการสรุปผลนั้นก็น่ากลัวเพียงพอแล้วด้วยตัวมันเอง ฉันยืนยันได้ว่านั่นยังคงใช้งานได้ค่อนข้างดี - person Jeroen Mostert; 05.09.2017