Pertanyaan Anggota virtual memanggil konstruktor


Saya mendapatkan peringatan dari ReSharper tentang panggilan ke anggota virtual dari konstruktor objek saya.

Mengapa ini menjadi sesuatu yang tidak boleh dilakukan?


1146
2017-09-23 07:11


asal


Jawaban:


Ketika sebuah objek yang ditulis dalam C # dibangun, apa yang terjadi adalah bahwa penginisialisasi berjalan dalam urutan dari kelas yang paling diturunkan ke kelas dasar, dan kemudian konstruktor berjalan dalam urutan dari kelas dasar ke kelas yang paling diturunkan (lihat blog Eric Lippert untuk perincian mengapa ini terjadi).

Juga di. NET objek tidak mengubah jenis karena mereka dibangun, tetapi mulai keluar sebagai tipe yang paling diturunkan, dengan tabel metode sedang untuk tipe yang paling diturunkan. Ini berarti bahwa pemanggilan metode virtual selalu berjalan pada jenis yang paling diturunkan.

Ketika Anda menggabungkan dua fakta ini, Anda ditinggalkan dengan masalah bahwa jika Anda membuat panggilan metode virtual dalam konstruktor, dan itu bukan tipe yang paling diturunkan dalam hierarki warisannya, bahwa itu akan dipanggil pada kelas yang konstruktornya belum jalankan, dan karena itu mungkin tidak dalam keadaan yang sesuai untuk memiliki metode yang disebut.

Masalah ini, tentu saja, dimitigasi jika Anda menandai kelas Anda sebagai disegel untuk memastikan bahwa itu adalah tipe yang paling diturunkan dalam hierarki warisan - dalam hal ini sangat aman untuk memanggil metode virtual.


1036
2017-09-23 07:21



Untuk menjawab pertanyaan Anda, pertimbangkan pertanyaan ini: apa yang akan dicetak kode di bawah ketika Child objek yang dipakai?

class Parent
{
    public Parent()
    {
        DoSomething();
    }

    protected virtual void DoSomething() 
    {
    }
}

class Child : Parent
{
    private string foo;

    public Child() 
    { 
        foo = "HELLO"; 
    }

    protected override void DoSomething()
    {
        Console.WriteLine(foo.ToLower());
    }
}

Jawabannya adalah bahwa sebenarnya a NullReferenceException akan dibuang, karena foo adalah null. Konstruktor dasar objek disebut sebelum konstruktor sendiri. Dengan memiliki virtual panggilan dalam konstruktor objek Anda memperkenalkan kemungkinan bahwa mewarisi objek akan mengeksekusi kode sebelum mereka sepenuhnya diinisialisasi.


478
2017-09-23 07:17



Aturan C # sangat berbeda dari Java dan C ++.

Ketika Anda berada di konstruktor untuk beberapa objek dalam C #, objek itu ada dalam bentuk yang diinisialisasi (hanya tidak "dibangun"), sebagai tipe turunan sepenuhnya.

namespace Demo
{
    class A 
    {
      public A()
      {
        System.Console.WriteLine("This is a {0},", this.GetType());
      }
    }

    class B : A
    {      
    }

    // . . .

    B b = new B(); // Output: "This is a Demo.B"
}

Ini berarti bahwa jika Anda memanggil fungsi virtual dari konstruktor A, itu akan memutuskan untuk mengganti apa pun di B, jika ada yang diberikan.

Bahkan jika Anda dengan sengaja mengatur A dan B seperti ini, sepenuhnya memahami perilaku sistem, Anda mungkin akan terkejut nantinya. Katakanlah Anda memanggil fungsi virtual dalam konstruktor B, "mengetahui" mereka akan ditangani oleh B atau A sebagaimana mestinya. Kemudian waktu berlalu, dan orang lain memutuskan mereka perlu mendefinisikan C, dan menimpa beberapa fungsi virtual di sana. Semua konstruktor B tiba-tiba berakhir dengan memanggil kode dalam C, yang dapat menyebabkan perilaku yang cukup mengejutkan.

Itu mungkin ide yang baik untuk menghindari fungsi virtual pada konstruktor, karena aturannya adalah sangat berbeda antara C #, C ++, dan Java. Programer Anda mungkin tidak tahu apa yang diharapkan!


154
2017-09-23 07:36



Alasan peringatan sudah dijelaskan, tetapi bagaimana Anda memperbaiki peringatan? Anda harus menutup kelas atau anggota virtual.

  class B
  {
    protected virtual void Foo() { }
  }

  class A : B
  {
    public A()
    {
      Foo(); // warning here
    }
  }

Anda bisa menyegel kelas A:

  sealed class A : B
  {
    public A()
    {
      Foo(); // no warning
    }
  }

Atau Anda bisa menyegel metode Foo:

  class A : B
  {
    public A()
    {
      Foo(); // no warning
    }

    protected sealed override void Foo()
    {
      base.Foo();
    }
  }

77
2017-09-23 13:20



Dalam C #, konstruktor kelas dasar berjalan sebelum konstruktor kelas turunan, jadi setiap bidang contoh yang mungkin digunakan kelas turunan dalam anggota virtual yang mungkin dikesampingkan belum diinisialisasi.

Harap dicatat bahwa ini hanya a PERINGATAN untuk membuat Anda memperhatikan dan memastikan semuanya baik-baik saja. Ada kasus penggunaan sebenarnya untuk skenario ini, Anda hanya perlu melakukannya mendokumentasikan perilaku dari anggota virtual yang tidak dapat menggunakan bidang contoh apa pun yang dideklarasikan di kelas turunan di bawah di mana konstruktor menyebutnya.


16
2017-09-23 07:21



Ada jawaban yang ditulis dengan baik di atas untuk alasan Anda tidak akan ingin melakukan itu. Berikut contoh tandingan di mana mungkin Anda akan ingin melakukan itu (diterjemahkan ke C # dari Desain Berorientasi Objek Praktis di Ruby oleh Sandi Metz, hal. 126).

Perhatikan itu GetDependency() tidak menyentuh variabel instan apa pun. Akan statis jika metode statis bisa virtual.

(Agar adil, mungkin ada cara yang lebih cerdas untuk melakukan hal ini melalui wadah injeksi ketergantungan atau penginisialisasi objek ...)

public class MyClass
{
    private IDependency _myDependency;

    public MyClass(IDependency someValue = null)
    {
        _myDependency = someValue ?? GetDependency();
    }

    // If this were static, it could not be overridden
    // as static methods cannot be virtual in C#.
    protected virtual IDependency GetDependency() 
    {
        return new SomeDependency();
    }
}

public class MySubClass : MyClass
{
    protected override IDependency GetDependency()
    {
        return new SomeOtherDependency();
    }
}

public interface IDependency  { }
public class SomeDependency : IDependency { }
public class SomeOtherDependency : IDependency { }

11
2017-12-28 01:19



Ya, biasanya buruk untuk memanggil metode virtual dalam konstruktor.

Pada titik ini, objek mungkin belum sepenuhnya terkonstruksi, dan invariant yang diharapkan oleh metode mungkin belum berlaku.


5
2017-09-23 07:15



Konstruktor Anda mungkin (kemudian, dalam perpanjangan perangkat lunak Anda) dipanggil dari konstruktor subkelas yang mengesampingkan metode virtual. Sekarang bukan implementasi subclass dari fungsi, tetapi implementasi dari kelas dasar akan dipanggil. Jadi tidak masuk akal untuk memanggil fungsi virtual di sini.

Namun, jika desain Anda memenuhi prinsip Pergantian Liskov, tidak ada kerusakan yang akan terjadi. Mungkin itu sebabnya itu ditoleransi - peringatan, bukan kesalahan.


5
2017-09-23 07:25



Satu aspek penting dari pertanyaan ini yang belum dijawab oleh jawaban lain adalah bahwa aman bagi kelas dasar untuk memanggil anggota virtual dari dalam konstruktornya. jika itu yang diharapkan oleh kelas turunan. Dalam kasus seperti itu, perancang kelas turunan bertanggung jawab untuk memastikan bahwa setiap metode yang dijalankan sebelum konstruksi selesai akan berperilaku selayaknya mereka dapat dalam keadaan seperti itu. Sebagai contoh, di C ++ / CLI, konstruktor dibungkus dalam kode yang akan dipanggil Dispose pada objek yang dibangun sebagian jika konstruksi gagal. Panggilan Dispose dalam kasus seperti itu sering diperlukan untuk mencegah kebocoran sumber daya, tetapi Dispose metode harus disiapkan untuk kemungkinan bahwa objek yang mereka jalankan mungkin belum sepenuhnya dibangun.


5
2017-10-25 20:33



Karena sampai konstruktor telah selesai mengeksekusi, objek tidak sepenuhnya dipakai. Setiap anggota yang direferensikan oleh fungsi virtual mungkin tidak diinisialisasi. Di C ++, ketika Anda berada di konstruktor, this hanya mengacu pada tipe statis dari konstruktor yang Anda gunakan, dan bukan jenis dinamis sebenarnya dari objek yang sedang dibuat. Ini berarti bahwa panggilan fungsi virtual bahkan mungkin tidak pergi ke tempat yang Anda harapkan.


4
2017-09-23 07:14