Secara terprogram mencegah inisialisasi konstruktor?

Saya memiliki kelas Foo dan kelas statis FooFactory, yang digunakan untuk membuat instance kelas turunan Foo dan Foo melalui API berikut:

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

Spesialisasi new() dari parameter tipe T mengharuskan Foo memiliki konstruktor default public. Dengan kata lain, tidak ada yang menghalangi pengguna Foo untuk menginisialisasi kelas secara langsung melalui konstruktor.

Namun, FooFactory dimaksudkan untuk melacak semua instance Foo, jadi saya ingin memaksa pengguna untuk membuat semua instance turunan Foo dan Foo melalui FooFactory.

Cara terdekat yang dapat saya cegah untuk inisialisasi konstruktor langsung adalah dengan mendekorasi konstruktor default Foo dengan atribut [Obsolete("message", error: true)] :

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

Dengan dekorasi ini, kode tidak dapat dikompilasi ketika saya memanggil konstruktor default Foo secara langsung, sedangkan inisialisasi melalui FooFactory.Create<Foo>() berfungsi.

Namun dengan dekorasi [Obsolete] ini, saya masih mengalami masalah dengan kelas turunan. Yang berikut ini bahkan tidak dapat dikompilasi karena pengaturan error: true untuk konstruktor default Foo:

public class Bar : Foo { ... }

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

Saya dapat mendeklarasikan kelebihan konstruktor protected Foo yang dipanggil oleh kelas turunan alih-alih konstruktor default, tetapi kemudian saya tidak akan memiliki kemampuan untuk secara terprogram mencegah inisialisasi langsung dari kelas turunan.

Jika saya menyetel error di ObsoleteAttribute ke false, saya malah mendapatkan peringatan kompilasi, tetapi saya ingin mendapat keputusasaan yang lebih kuat daripada peringatan...

Apakah ada cara agar saya dapat secara terprogram mencegah pemanggilan langsung konstruktor default untuk Foo dan kelas turunan ketika konstruktor default harus dideklarasikan public?


person Anders Gustafsson    schedule 04.09.2017    source sumber
comment
Buat konstruktor internal ke perpustakaan dan hapus batasan new(). Ini berarti Anda bertanggung jawab atas pembuatan semua tipe Foo   -  person Nkosi    schedule 04.09.2017
comment
Kalau tidak salah, persyaratan new() hanya berfungsi jika konstruktornya dideklarasikan public.   -  person Anders Gustafsson    schedule 04.09.2017
comment
Anda harus menghilangkan batasan tersebut   -  person Nkosi    schedule 04.09.2017
comment
Itu bukanlah pilihan...   -  person Anders Gustafsson    schedule 04.09.2017
comment
nah begitulah cara kerja kendalanya. Anda tidak bisa mendapatkan apa yang Anda minta.   -  person Nkosi    schedule 04.09.2017
comment
Saat Anda berada di dalam lubang, berhentilah menggali. Buat konstruktor Foo melakukan pelacakan instance, dengan memanggil bidang statis dan this.GetType().   -  person Jeroen Mostert    schedule 04.09.2017
comment
Jika Anda memiliki kendali penuh atas Foo dan tipe turunannya, Anda dapat menjadikannya internal dan mengekspos Antarmuka yang diimplementasikannya. Metode Create Anda dapat menggunakan antarmuka sebagai parameter dan melalui refleksi menentukan kelas mana yang akan dibuat instance-nya. Namun: jika melakukannya, Anda kehilangan waktu kompilasi untuk memeriksa tipe parameter untuk Buat.   -  person Johan Donne    schedule 04.09.2017
comment
Poin bagus, Jeroen. Saya awalnya melakukan ini, tetapi menghapusnya ketika saya memfaktorkan ulang kelas. Saat memeriksa ulang kode sumber saat ini, saya melihat bahwa masuk akal untuk memperkenalkan kembali pendaftaran di konstruktor. Terima kasih!   -  person Anders Gustafsson    schedule 04.09.2017
comment
Bagus, karena seperti yang dinyatakan, tuntutan Anda secara harfiah tidak mungkin dipenuhi jika tidak. Secara khusus, tidak mungkin untuk melarang mendeklarasikan Baz yang dapat dibuat secara langsung -- pembuatnya selalu menyediakan konstruktor publik untuk pengguna yang mengabaikan permintaan Anda, terlepas dari bagaimana Foo disiapkan. Jika Anda menganggap penulis selalu bersekongkol dan berasumsi mereka akan berperilaku baik, saran kedua saya adalah menghilangkan batasan new() dan meneruskan panggilan konstruktor sebagai Func<T> ke metode internal. Hal ini menjaga keamanan tipe tetapi rumit bagi penulis.   -  person Jeroen Mostert    schedule 04.09.2017
comment
@JohanDonne Terima kasih atas saran Anda, tapi ini tidak sesuai dengan apa yang saya cari. Salah satu alasan untuk tetap menggunakan spesialisasi new() adalah karena spesialisasi ini portabel di seluruh platform. Refleksi, khususnya terhadap metode non-public, tidak selalu berjalan baik di semua platform.   -  person Anders Gustafsson    schedule 04.09.2017
comment
Apakah pihak ketiga harus dapat membuat subkelas Foo? Jika itu bukan suatu keharusan maka akan lebih mudah untuk menyelesaikan masalah Anda.   -  person Eric Lippert    schedule 04.09.2017
comment
Terima kasih telah menyelidiki ini, Eric. Ya, niat saya adalah pihak ketiga harus dapat membuat subkelas Foo.   -  person Anders Gustafsson    schedule 04.09.2017


Jawaban (1)


Anda tidak dapat mewarisi dari kelas dengan konstruktor pribadi saja tetapi Anda dapat menjadikan konstruktor kosong Anda pribadi dan menambahkan konstruktor dengan properti seperti createdInsideFooFactory. Anda juga harus menghapus batasan baru() di pabrik Anda. Ini adalah peretasan tetapi akan memberi tahu pengembang bahwa instance harus dibuat di dalam pabrik foo.

public class Foo
{
    private Foo()
    {
    }

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

        throw new MyException();
    }
}
person Alex Zaitsev    schedule 04.09.2017
comment
Pernyataan Anda bahwa Anda tidak dapat mewarisi dari kelas dengan konstruktor pribadi adalah salah. Bisakah Anda mengetahui cara menulis program yang memiliki kelas dengan konstruktor pribadi dan kelas turunan? Setidaknya ada dua cara untuk melakukannya. - person Eric Lippert; 04.09.2017
comment
@EricLippert: Saya tidak bisa, mohon pencerahannya. Cara jelas yang dapat saya pikirkan adalah 1) menyediakan konstruktor tambahan yang bukan pribadi (tetapi jawabannya hanya menentukan konstruktor pribadi , jadi itu curang dan 2) menjadikan kelas turunan sebagai kelas bersarang (tetapi yang memerlukan kontrol atas definisi Foo, yang juga merupakan kecurangan). - person Jeroen Mostert; 05.09.2017
comment
Ya hanya ctor swasta. Kelas bersarang adalah cara mudah. Ada juga jalan yang sulit. - person Eric Lippert; 05.09.2017
comment
Petunjuk: apakah kelas turunan harus memanggil ctor kelas dasar? - person Eric Lippert; 05.09.2017
comment
@EricLippert: jika kita hanya menggunakan spesifikasi bahasa C#, maka ya. Ini bisa memanggil konstruktor kelas dasar (secara eksplisit atau implisit) atau konstruktor lain dari kelas yang sama, dan loop (secara langsung atau tidak langsung) dilarang, sehingga pada akhirnya, bcc harus dipanggil. Tentu saja, pada saat runtime Anda dapat menghindari pemanggilannya melalui pengecualian, tetapi hal itu tidak dapat menghindari deklarasi. Bahkan jika ada cara yang mengerikan untuk menyelesaikan ini, saya yakin Anda akan berakhir di kelas tanpa instance. Sekarang ungkapkan rahasianya, beberapa dari kita tidak menulis kompilernya. :-P - person Jeroen Mostert; 05.09.2017
comment
@AlexZaitsev, karena beberapa alasan, bukanlah pilihan bagi saya untuk menghapus batasan new(). Apa yang Anda usulkan akan memungkinkan untuk mendefinisikan konstruktor default subkelas, itu benar, tetapi saya tidak melihat cara untuk mengontrol apakah konstruktor default itu dipanggil secara langsung atau melalui pabrik? Yaitu, Anda dapat memiliki public Bar() : Foo(true) atau public Bar() : Foo(false), tetapi saya tidak melihat cara untuk mengontrol agar konstruktor ini dipanggil baik dari pabrik atau secara langsung. - person Anders Gustafsson; 05.09.2017
comment
@JeroenMostert: Ah, teknik kedua sudah dihilangkan. Dulunya legal untuk memiliki loop dalam panggilan konstruktor ini. Itu memungkinkan Anda memanggil ctor dasar, yang memungkinkan program dikompilasi. Kemudian Anda menyebabkan pengecualian dalam konstruksi, menangkap pengecualian, dan menghidupkan kembali objek melalui finalizer, dan Anda memiliki turunan dari kelas turunan yang tidak pernah disebut ctor kelas dasar. - person Eric Lippert; 05.09.2017
comment
@EricLippert: astaga, tentu saja, sekarang saya merasa konyol karena tidak memikirkan hal itu. :-P Fakta bahwa Anda dapat menghidupkan kembali objek yang dibangun sebagian di finalizer sudah cukup menakutkan. Saya dapat memastikan bahwa itu masih berfungsi dengan baik. - person Jeroen Mostert; 05.09.2017