Câu hỏi Cuộc gọi thành viên ảo trong một hàm tạo


Tôi nhận được một cảnh báo từ ReSharper về một cuộc gọi đến một thành viên ảo từ constructor đối tượng của tôi.

Tại sao đây lại là thứ không nên làm?


1151
2017-09-23 07:11


gốc


@ m.edmondson, Nghiêm túc .. bình luận của bạn phải là câu trả lời ở đây. Trong khi Greg giải thích là chính xác tôi đã không hiểu nó cho đến khi tôi đọc blog của bạn. - Rosdi Kasim
@ m.edmondson miền của bạn đã hết hạn. - Robert Noack
Bạn có thể tìm bài viết từ @ m.edmondson tại đây ngay bây giờ: codeproject.com/Articles/802375/… - SpeziFish


Các câu trả lời:


Khi một đối tượng được viết trong C # được xây dựng, điều xảy ra là khởi tạo chạy theo thứ tự từ lớp dẫn xuất nhất đến lớp cơ sở, và sau đó các hàm tạo chạy theo thứ tự từ lớp cơ sở đến lớp dẫn xuất nhất (xem blog của Eric Lippert để biết chi tiết về lý do tại sao).

Cũng trong các đối tượng .NET không thay đổi kiểu khi chúng được xây dựng, nhưng bắt đầu như là kiểu được sinh ra nhiều nhất, với bảng phương thức là kiểu được sinh ra nhiều nhất. Điều này có nghĩa rằng các cuộc gọi phương thức ảo luôn luôn chạy trên loại có nguồn gốc cao nhất.

Khi bạn kết hợp hai sự kiện này, bạn còn lại với vấn đề nếu bạn thực hiện một lời gọi phương thức ảo trong một hàm khởi tạo, và nó không phải là kiểu dẫn xuất nhất trong hệ thống phân cấp thừa kế của nó, nó sẽ được gọi trên một lớp mà hàm tạo của nó không được chạy, và do đó có thể không ở trong trạng thái phù hợp để gọi phương thức đó.

Tất nhiên, vấn đề này là giảm nhẹ nếu bạn đánh dấu lớp của bạn là bị đóng dấu để đảm bảo rằng nó là kiểu được sinh ra nhiều nhất trong hệ thống phân cấp thừa kế - trong trường hợp đó là hoàn toàn an toàn để gọi phương thức ảo.


1041
2017-09-23 07:21



Greg, xin hãy cho tôi biết tại sao mọi người lại có một lớp SEALED (không thể được INHERITED) khi nó có các thành viên VIRTUAL [đó là để ghi đè lên các lớp DERIVED]? - Paul Pacurar
Nếu bạn muốn chắc chắn rằng một lớp dẫn xuất không thể được tiếp tục xuất phát, nó hoàn toàn có thể chấp nhận để đóng dấu nó. - Øyvind
@Paul - Vấn đề là đã hoàn thành việc phát sinh các thành viên ảo của căn cứ lớp [es], và do đó đang đánh dấu lớp như được bắt nguồn đầy đủ như bạn muốn. - ljs
@Greg Nếu hành vi của phương thức ảo không liên quan gì đến các biến mẫu, thì điều này có ổn không? Nó có vẻ như có lẽ chúng ta sẽ có thể tuyên bố rằng một phương pháp ảo sẽ không sửa đổi các biến dụ? (tĩnh?) Ví dụ, nếu bạn muốn có một phương pháp ảo có thể được ghi đè để khởi tạo một loại có nguồn gốc hơn. Điều này có vẻ an toàn với tôi và không đảm bảo cảnh báo này. - Dave Cousineau
@PaulPacurar - Nếu bạn muốn gọi một phương thức ảo trong lớp dẫn xuất nhất, bạn vẫn nhận được cảnh báo trong khi bạn biết nó sẽ không gây ra vấn đề gì. Trong trường hợp đó, bạn có thể chia sẻ kiến ​​thức của mình với hệ thống bằng cách niêm phong lớp đó. - Revolutionair


Để trả lời câu hỏi của bạn, hãy xem xét câu hỏi này: mã dưới đây sẽ in ra gì khi Child đối tượng được khởi tạo?

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());
    }
}

Câu trả lời là trong thực tế, NullReferenceException sẽ bị ném, bởi vì foo là null. Một hàm tạo cơ sở của đối tượng được gọi trước hàm tạo riêng của đối tượng. Bằng cách có một virtual gọi trong hàm tạo của đối tượng, bạn đang giới thiệu khả năng rằng các đối tượng kế thừa sẽ thực thi mã trước khi chúng được khởi tạo hoàn toàn.


480
2017-09-23 07:17



Câu trả lời tốt hơn câu trả lời được đánh dấu. - Hele
Điều này rõ ràng hơn câu trả lời ở trên. Mã mẫu có giá trị một nghìn chữ. - Novice in.NET


Các quy tắc của C # rất khác với các quy tắc của Java và C ++.

Khi bạn đang ở trong hàm tạo cho một số đối tượng trong C #, đối tượng đó tồn tại trong một biểu mẫu được khởi tạo hoàn toàn (không phải là "được xây dựng"), như là kiểu có nguồn gốc đầy đủ của nó.

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"
}

Điều này có nghĩa rằng nếu bạn gọi một hàm ảo từ hàm tạo của A, nó sẽ giải quyết cho bất kỳ ghi đè nào trong B, nếu một hàm được cung cấp.

Ngay cả khi bạn cố ý thiết lập A và B như thế này, hoàn toàn hiểu được hành vi của hệ thống, bạn có thể bị sốc sau này. Giả sử bạn gọi các hàm ảo trong hàm tạo của B, "biết" chúng sẽ được B hoặc A xử lý khi thích hợp. Sau đó, thời gian trôi qua và người khác quyết định họ cần định nghĩa C và ghi đè một số chức năng ảo tại đó. Tất cả các nhà xây dựng B bất ngờ đều gọi mã trong C, điều này có thể dẫn đến hành vi khá đáng ngạc nhiên.

Nó có lẽ là một ý tưởng hay để tránh các hàm ảo trong các hàm tạo, vì các quy tắc  rất khác nhau giữa C #, C ++ và Java. Lập trình viên của bạn có thể không biết những gì mong đợi!


154
2017-09-23 07:36



Câu trả lời của Greg Beech, trong khi tiếc là không bình chọn cao như câu trả lời của tôi, tôi cảm thấy là câu trả lời tốt hơn. Nó chắc chắn có một vài chi tiết có giá trị, giải thích chi tiết mà tôi không dành thời gian để đưa vào. - Lloyd
Trên thực tế, các quy tắc trong Java là như nhau. - OlegYch
@ JoãoPortela C ++ là rất khác nhau thực sự. Các cuộc gọi phương thức ảo trong các hàm tạo (và các trình phá hủy!) Được giải quyết bằng cách sử dụng kiểu (và vtable) hiện đang được xây dựng, không phải kiểu có nguồn gốc nhiều nhất là Java và C # đều làm. Đây là mục FAQ có liên quan. - Jacek Sieka
@ JacekSieka bạn hoàn toàn chính xác. Đã được một thời gian kể từ khi tôi mã hóa trong C + + và tôi bằng cách nào đó nhầm lẫn tất cả điều này. Tôi có nên xóa nhận xét để tránh gây nhầm lẫn cho bất kỳ ai khác không? - João Portela
Có một cách đáng kể trong đó C # khác với cả Java và VB.NET; trong C #, các trường được khởi tạo tại điểm khai báo sẽ được khởi tạo của chúng được xử lý trước cuộc gọi hàm tạo cơ bản; điều này đã được thực hiện với mục đích cho phép các đối tượng lớp dẫn xuất có thể sử dụng được từ hàm tạo, nhưng tiếc là khả năng này chỉ hoạt động cho các tính năng lớp có nguồn gốc mà không được kiểm soát bởi bất kỳ tham số lớp dẫn xuất nào. - supercat


Lý do cảnh báo đã được mô tả, nhưng bạn sẽ khắc phục cảnh báo như thế nào? Bạn phải đóng dấu một trong hai lớp hoặc thành viên ảo.

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

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

Bạn có thể niêm phong lớp A:

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

Hoặc bạn có thể đóng dấu phương thức Foo:

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

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

77
2017-09-23 13:20





Trong C #, một hàm tạo của lớp cơ sở chạy trước constructor của lớp dẫn xuất, vì vậy bất kỳ trường instance nào mà một lớp dẫn xuất có thể sử dụng trong thành viên ảo có thể bị ghi đè chưa được khởi tạo.

Lưu ý rằng đây chỉ là một cảnh báo để làm cho bạn chú ý và đảm bảo rằng nó là tất cả các quyền. Có những trường hợp sử dụng thực tế cho kịch bản này, bạn chỉ cần ghi lại hành vi của thành viên ảo mà nó không thể sử dụng bất kỳ trường thể hiện nào được khai báo trong lớp dẫn xuất bên dưới, nơi hàm tạo gọi nó là.


16
2017-09-23 07:21





Có những câu trả lời rõ ràng ở trên vì lý do bạn sẽ không muốn làm điều đó. Dưới đây là ví dụ phản biện có lẽ bạn sẽ muốn làm điều đó (được dịch sang C # từ Thiết kế hướng đối tượng thực tế trong Ruby bởi Sandi Metz, trang. 126).

Lưu ý rằng GetDependency() không chạm vào bất kỳ biến mẫu nào. Nó sẽ là tĩnh nếu phương pháp tĩnh có thể là ảo.

(Để công bằng, có thể có cách thông minh hơn để thực hiện việc này thông qua các thùng chứa phụ thuộc hoặc các bộ khởi tạo đối tượng ...)

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



Tôi sẽ xem xét sử dụng phương pháp nhà máy cho việc này. - Ian Ringrose
Tôi muốn .NET Framework có, thay vì bao gồm chủ yếu là vô ích Finalize là thành viên mặc định của Object, đã sử dụng khe cắm vtable đó cho ManageLifetime(LifetimeStatus) phương thức sẽ được gọi khi một hàm tạo trả về mã máy khách, khi một hàm khởi tạo ném, hoặc khi một đối tượng được tìm thấy bị bỏ qua. Hầu hết các kịch bản đòi hỏi gọi một phương thức ảo từ một hàm tạo lớp cơ sở có thể được xử lý tốt nhất bằng cách sử dụng cấu trúc hai giai đoạn, nhưng việc xây dựng hai giai đoạn sẽ hoạt động như một chi tiết thực hiện, thay vì yêu cầu khách hàng gọi giai đoạn thứ hai. - supercat
Tuy nhiên, các vấn đề có thể nảy sinh với mã này giống như bất kỳ trường hợp nào khác được hiển thị trong luồng này; GetDependency không được đảm bảo an toàn khi gọi trước MySubClass constructor đã được gọi. Ngoài ra, có các phụ thuộc mặc định được khởi tạo theo mặc định không phải là những gì bạn gọi là "pure DI". - Groo
Ví dụ này "từ chối phụ thuộc". ;-) Đối với tôi, đây là một ví dụ ngược lại tốt cho một lời gọi phương thức ảo từ một hàm tạo. SomeDependency không còn được khởi tạo trong các dẫn xuất MySubClass dẫn đến hành vi bị hỏng cho mọi tính năng MyClass phụ thuộc vào SomeDependency. - Nachbars Lumpi


Có, nói chung là xấu khi gọi phương thức ảo trong hàm tạo.

Tại thời điểm này, các objet có thể không được xây dựng đầy đủ được nêu ra, và các bất biến dự kiến ​​của phương pháp có thể không giữ được nêu ra.


5
2017-09-23 07:15