Helpex - Trao đổi & giúp đỡ Đăng nhập
45

Chương trình C # hoàn chỉnh này minh họa vấn đề:

public abstract class Executor<T>
{
    public abstract void Execute(T item);
}

class StringExecutor : Executor<string>
{
    public void Execute(object item)
    {
        // why does this method call back into itself instead of binding
        // to the more specific "string" overload.
        this.Execute((string)item);
    }

    public override void Execute(string item) { }
}

class Program
{
    static void Main(string[] args)
    {
        object item = "value";
        new StringExecutor()
            // stack overflow
            .Execute(item); 
    }
}

Tôi đã gặp phải một StackOverlowException mà tôi đã truy tìm lại mẫu cuộc gọi này, nơi tôi đang cố gắng chuyển tiếp các cuộc gọi đến một quá tải cụ thể hơn. Trước sự ngạc nhiên của tôi, tuy nhiên, lời gọi không chọn quá tải cụ thể hơn, mà gọi lại chính nó. Nó rõ ràng có liên quan đến kiểu cơ sở là chung chung, nhưng tôi không hiểu tại sao nó không chọn quá tải Thực thi (chuỗi).

Có ai có bất cứ cái nhìn sâu sắc vào điều này?

Đoạn mã trên đã được đơn giản hóa để hiển thị mẫu, cấu trúc thực tế phức tạp hơn một chút, nhưng vấn đề là như nhau.

45 hữu ích 4 bình luận 3.0k xem chia sẻ
31

Có vẻ như điều này được đề cập trong đặc tả C # 5.0, 7.5.3 Độ phân giải quá tải:

Độ phân giải quá tải chọn thành viên hàm để gọi trong các ngữ cảnh riêng biệt sau trong C #:

  • Lời gọi của một phương thức được đặt tên trong một biểu thức gọi (§7.6.5.1).
  • Gọi một phương thức khởi tạo cá thể có tên trong biểu thức-tạo-đối tượng (§7.6.10.1).
  • Gọi người truy cập trình lập chỉ mục thông qua quyền truy cập phần tử (§7.6.6).
  • Lời gọi của toán tử được xác định trước hoặc do người dùng xác định được tham chiếu trong một biểu thức (§7.3.3 và §7.3.4).

Mỗi ngữ cảnh này xác định tập hợp các thành viên hàm ứng cử viên và danh sách các đối số theo cách riêng của nó, như được mô tả chi tiết trong các phần được liệt kê ở trên. Ví dụ: tập hợp các ứng cử viên cho một lệnh gọi phương thức không bao gồm các phương thức được đánh dấu ghi đè (§7.4) và các phương thức trong lớp cơ sở không phải là ứng cử viên nếu bất kỳ phương thức nào trong lớp dẫn xuất có thể áp dụng được (§7.6.5.1).

Khi chúng ta nhìn vào 7.4:

Việc tra cứu thành viên của tên N với K tham số kiểu T được xử lý như sau:

• Đầu tiên, một tập hợp các thành viên có thể truy cập có tên N được xác định:

  • Nếu T là một tham số kiểu, thì tập hợp là tập hợp các
    thành viên có thể truy cập được có tên N trong mỗi kiểu được chỉ định làm ràng buộc chính hoặc ràng buộc phụ (§10.1.5) cho T, cùng với tập các thành viên có thể truy cập. tên N trong đối tượng.

  • Mặt khác, tập hợp bao gồm tất cả các thành viên có thể truy cập (§3.5) có tên N trong T, bao gồm các thành viên kế thừa và các thành viên có thể truy cập có tên N trong đối tượng. Nếu T là một kiểu được xây dựng, tập hợp các phần tử thu được bằng cách thay thế các đối số kiểu như được mô tả trong §10.3.2. Các thành viên bao gồm một công cụ sửa đổi ghi đè sẽ bị loại trừ khỏi tập hợp.

Nếu bạn loại bỏ overridetrình biên dịch sẽ chọn Execute(string)quá tải khi bạn truyền mục.

31 hữu ích 5 bình luận chia sẻ
24

Như đã đề cập trong bài viết của Jon Skeet về nạp chồng , khi gọi một phương thức trong một lớp cũng ghi đè một phương thức có cùng tên từ một lớp cơ sở, trình biên dịch sẽ luôn sử dụng phương thức trong lớp thay vì ghi đè, bất kể "tính cụ thể "loại, miễn là chữ ký" tương thích ".

Jon tiếp tục chỉ ra rằng đây là một lý lẽ tuyệt vời để tránh quá tải qua các ranh giới thừa kế, vì đây chính xác là loại hành vi không mong muốn có thể xảy ra.

24 hữu ích 3 bình luận chia sẻ
17

Như các câu trả lời khác đã lưu ý, đây là do thiết kế.

Hãy xem xét một ví dụ ít phức tạp hơn:

class Animal
{
  public virtual void Eat(Apple a) { ... }
}
class Giraffe : Animal
{
  public void Eat(Food f) { ... }
  public override void Eat(Apple a) { ... }
}

Câu hỏi đặt ra là tại sao giraffe.Eat(apple)giải quyết được Giraffe.Eat(Food)và không phải là ảo Animal.Eat(Apple).

Đây là hệ quả của hai quy tắc:

(1) Kiểu của người nhận quan trọng hơn kiểu của bất kỳ đối số nào khi giải quyết quá tải.

Tôi hy vọng nó là rõ ràng tại sao điều này phải như vậy. Người viết lớp dẫn xuất có nhiều kiến ​​thức hơn người viết lớp cơ sở, bởi vì người viết lớp dẫn xuất đã sử dụng lớp cơ sở chứ không phải ngược lại.

Người đã viết Giraffecho biết "Tôi có một cách Giraffeđể ăn bất kỳ loại thức ăn nào " và điều đó đòi hỏi kiến ​​thức đặc biệt về cơ quan tiêu hóa của hươu cao cổ. Thông tin đó không có trong phần triển khai lớp cơ sở, lớp chỉ biết ăn táo.

Vì vậy, việc giải quyết quá tải phải luôn ưu tiên chọn một phương thức áp dụng của lớp dẫn xuất hơn là chọn phương thức của lớp cơ sở, bất kể mức độ tốt hơn của các chuyển đổi kiểu đối số.

(2) Việc chọn ghi đè hoặc không ghi đè một phương thức ảo không phải là một phần của diện tích bề mặt công khai của một lớp. Đó là một chi tiết triển khai riêng tư. Do đó, không phải đưa ra quyết định nào khi thực hiện giải quyết quá tải, điều này sẽ thay đổi tùy thuộc vào việc một phương thức có bị ghi đè hay không.

Giải quyết quá tải không bao giờ được nói "Tôi sẽ chọn ảo Animal.Eat(Apple) vì nó đã bị ghi đè ".

Bây giờ, bạn có thể nói "OK, giả sử tôi đang ở trong Giraffe khi tôi thực hiện cuộc gọi." Code bên trong Giraffe có tất cả các kiến ​​thức về các chi tiết triển khai riêng tư, phải không? Vì vậy, nó có thể đưa ra quyết định gọi ảo Animal.Eat(Apple)thay vì Giraffe.Eat(Food)khi đối mặt giraffe.Eat(apple), phải không? Bởi vì nó biết rằng có một thực hiện hiểu được nhu cầu của hươu cao cổ ăn táo.

Đó là một cách chữa bệnh tồi tệ hơn căn bệnh. Bây giờ chúng ta có một tình huống mà mã giống hệt nhau có hành vi khác nhau tùy thuộc vào nơi nó được chạy! Bạn có thể tưởng tượng có một cuộc gọi đến giraffe.Eat(apple)bên ngoài lớp, cấu trúc lại nó để nó ở bên trong lớp và hành vi có thể quan sát được đột ngột thay đổi!

Hoặc, bạn có thể nói, này, tôi nhận ra rằng logic Con hươu cao cổ của tôi thực sự đủ chung chung để chuyển sang lớp cơ sở, nhưng không phải là Động vật, vì vậy tôi sẽ cấu trúc lại Giraffemã của mình thành:

class Mammal : Animal 
{
  public void Eat(Food f) { ... } 
  public override void Eat(Apple a) { ... }
}
class Giraffe : Mammal
{
  ...
}

Và bây giờ tất cả các cuộc gọi đến giraffe.Eat(apple) bên trong Giraffe đột nhiên có hành vi giải quyết quá tải khác nhau sau khi cấu trúc lại? Điều đó sẽ rất bất ngờ!

C # là một ngôn ngữ thành công; chúng tôi rất muốn đảm bảo rằng các cấu trúc lại đơn giản như thay đổi vị trí trong hệ thống phân cấp mà một phương thức bị ghi đè sẽ không gây ra những thay đổi tinh vi trong hành vi.

Tổng hợp:

  • Độ phân giải quá tải ưu tiên người nhận hơn các đối số khác bởi vì việc gọi mã chuyên dụng biết bên trong của người nhận sẽ tốt hơn so với việc gọi nhiều mã chung hơn mà không.
  • Việc một phương thức bị ghi đè không được xem xét trong quá trình giải quyết quá tải hay không và ở đâu; tất cả các phương thức được xử lý như thể chúng không bao giờ bị ghi đè cho mục đích giải quyết quá tải. Đó là một chi tiết triển khai, không phải là một phần của bề mặt của loại.
  • Vấn đề giải quyết quá tải đã được giải quyết - tất nhiên là khả năng truy cập modulo! - theo cùng một cách bất kể vấn đề xảy ra ở đâu trong mã. Chúng tôi không có một thuật toán để phân giải trong đó người nhận thuộc loại mã chứa và một thuật toán khác cho khi cuộc gọi ở một lớp khác.

Bạn có thể tìm thêm suy nghĩ về các vấn đề liên quan tại đây: https://ericlippert.com/2013/12/23/closer-is-better/ và tại đây https://blogs.msdn.microsoft.com/ericlippert/2007/09/ 04 / future-break-changes-part-ba /

17 hữu ích 0 bình luận chia sẻ
loading
Không tìm thấy câu trả lời bạn tìm kiếm? Duyệt qua các câu hỏi được gắn thẻ c# generics overload-resolution , hoặc hỏi câu hỏi của bạn.

Có thể bạn quan tâm

loading