2009-05-28 56 views
6

Tôi đã gặp một vấn đề biên dịch ngày hôm nay khiến tôi bối rối. Hãy xem xét hai lớp container này.Generics, inheritance, và phương thức giải quyết thất bại của trình biên dịch C#

public class BaseContainer<T> : IEnumerable<T> 
{ 
    public void DoStuff(T item) { throw new NotImplementedException(); } 

    public IEnumerator<T> GetEnumerator() { } 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { } 
} 
public class Container<T> : BaseContainer<T> 
{ 
    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

Các định nghĩa cựu DoStuff(T item) và quá tải sau nó với DoStuff <Tother>(IEnumerable<Tother>) đặc biệt để có được xung quanh sự vắng mặt của covariance/contravariance của C# (cho đến 4 Tôi nghe).

Mã này

Container<string> c = new Container<string>(); 
c.DoStuff("Hello World"); 

lượt truy cập một lỗi biên dịch khá lạ. Lưu ý sự vắng mặt của <char> từ cuộc gọi phương thức.

Loại 'char' không thể được sử dụng làm thông số loại 'Tother' trong loại hoặc phương thức chung 'Container.DoStuff (System.Collections.Generic.IEnumerable)'. Không có chuyển đổi quyền anh từ 'char' thành 'string'.

Về cơ bản, trình biên dịch đang cố gắng để mứt kêu gọi của Mẹ để DoStuff(string) vào Container.DoStuff<char>(IEnumerable<char>)string cụ IEnumerable<char>, thay vì sử dụng BaseContainer.DoStuff(string).

Cách duy nhất tôi đã tìm thấy để làm biên dịch này là thêm DoStuff(T) đến lớp được thừa kế

public class Container<T> : BaseContainer<T> 
{ 
    public new void DoStuff(T item) { base.DoStuff(item); } 

    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

Tại sao trình biên dịch cố gắng giơ một chuỗi như IEnumerable<char> khi 1) nó biết nó có thể' t (với sự hiện diện của một lỗi biên dịch) và 2) nó có một phương thức trong lớp cơ sở biên dịch tốt? Tôi có hiểu nhầm điều gì đó về generics hoặc phương thức ảo trong C# không? Có một bản sửa lỗi nào khác ngoài việc thêm một số new DoStuff(T item) vào số Container không?

+3

Tôi đồng ý này có vẻ lạ, nhưng nó là đúng theo spec. Đây là kết quả của sự tương tác của hai quy tắc: (1) kiểm tra khả năng ứng dụng độ phân giải quá tải xảy ra TRƯỚC KHI kiểm tra ràng buộc, và (2) các phương pháp áp dụng trong các lớp dẫn xuất là ALWAYS tốt hơn các phương thức áp dụng trong các lớp cơ sở. Cả hai đều là các quy tắc hợp lý hợp lý; chúng chỉ xảy ra tương tác đặc biệt nặng trong trường hợp của bạn. –

+1

Để biết chi tiết, xem phần 7.5.5.1, cụ thể là các bit có nội dung: (1) "Nếu phương pháp tốt nhất là phương pháp chung, các đối số kiểu (được cung cấp hoặc suy luận) được kiểm tra với các ràng buộc ..." và (2) " tập hợp các phương pháp ứng cử viên được giảm xuống để chỉ chứa các phương thức từ các kiểu xuất phát nhất ... " –

+2

Cuối cùng vấn đề của bạn ở đây là một vấn đề thiết kế. Bạn đang quá tải một phương thức "DoStuff" để có nghĩa là cả hai "làm công cụ cho một giá trị duy nhất của loại T", và "làm công cụ để một chuỗi các giá trị của loại T". Điều này chạy vào các vấn đề nghiêm trọng "có chủ ý giải quyết" theo nhiều cách - ví dụ, khi "loại T" là một chuỗi. Bạn sẽ thấy rằng các lớp sưu tập hiện có trong BCL đã được thiết kế cẩn thận để tránh vấn đề này; các phương thức lấy một mục được gọi là "Frob", các phương thức lấy một chuỗi các mục được gọi là "FrobRange", ví dụ "Thêm" và "AddRange" trên danh sách. –

Trả lời

3

Sửa

Ok ... Tôi nghĩ rằng tôi nhìn thấy sự nhầm lẫn của bạn bây giờ. Bạn có thể đã dự kiến ​​DoStuff (string) đã giữ tham số như một chuỗi và đi theo BaseClass Method List trước tiên tìm kiếm một chữ ký phù hợp, và thất bại dự phòng đó để cố gắng truyền tham số tới một kiểu khác.

Nhưng nó đã xảy ra theo cách khác ... Thay vì Container.DoStuff(string) đã đi, meh "có một phương pháp lớp cơ sở ở đó phù hợp với hóa đơn, nhưng tôi sẽ chuyển sang một IEnumerable và có một cơn đau tim về những gì có sẵn trong lớp hiện tại thay vì ...

Hmmm ... tôi chắc chắn Jon hoặc Marc sẽ có thể kêu vang trong thời điểm này với đoạn C# Spec cụ thể bao gồm trường hợp này góc đặc biệt

gốc

Cả hai phương pháp đều mong đợi một đại diện có thể đếm được lection

Bạn đang chuyển một chuỗi riêng lẻ.

Trình biên dịch đang tiến hành chuỗi và đi,

Ok, tôi có một chuỗi, Cả hai phương pháp mong đợi một IEnumerable<T>, Vì vậy, tôi sẽ biến chuỗi này thành một IEnumerable<char> ... Xong

phải, Kiểm tra phương pháp đầu tiên ... hmmm ... lớp này là một Container<string> nhưng tôi có một IEnumerable<char> vì vậy đó là không đúng.

Kiểm tra phương pháp thứ hai, hmmm .... I có số IEnumerable<char> nhưng char không triển khai chuỗi sao cho cũng không phải là .

biên dịch LỖI

Vì vậy, những gì # s sửa chữa, cũng nó hoàn toàn phụ thuộc những gì bạn đang cố gắng để đạt được ... cả những điều sau đây sẽ có giá trị, về cơ bản, việc sử dụng các loại của bạn chỉ là không chính xác trong hóa thân của bạn.

 Container<char> c1 = new Container<char>(); 
     c1.DoStuff("Hello World"); 

     Container<string> c2 = new Container<string>(); 
     c2.DoStuff(new List<string>() { "Hello", "World" }); 
+0

Vì vậy, trình biên dịch chọn một quá tải bởi chỉ các loại tham số và không phải là các ràng buộc đặt trên chúng (không phải là toàn bộ các tham số + ràng buộc)? Điều đó có vẻ khá yếu và nửa nướng. Nó * có thể * tìm một phương thức mà nó * có thể * sử dụng nhưng nó chọn * không *. –

+0

không, không thể nào ... không phải phương thức nào của bạn thỏa mãn kiểu mà bạn đi vào. Vùng chứa hiện chỉ có thể thực hiện một DoStuff (IEnumerable ) HOẶC DoStuff (IEnumerable ) <- không có gì vì chuỗi là một lớp kín. –

+0

Nó không thể? Điều gì đã xảy ra với BaseContainer.DoStuff (T)? Về chỉnh sửa sửa lỗi của bạn. Tôi muốn một container chuỗi và chuỗi "Hello World" được DoStuff'ed. Nếu tôi không thể sửa class thì DoStuff (new string [] {"Hello World"}); là những gì tôi muốn, nhưng đó là một API thực sự đáng sợ để cung cấp (tôi nghĩ). –

1

Tôi nghĩ rằng nó có một cái gì đó để làm với thực tế là char là một loại giá trị và chuỗi là một loại tài liệu tham khảo. Có vẻ như bạn đang xác định

TOther : T 

và char không lấy được từ chuỗi.

+0

Do đó tôi bối rối vì sao nó chọn DoStuff (IEnumerable ) làm phương pháp tôi đang gọi. Tôi không chỉ định DoStuff không phải là char bắt nguồn từ chuỗi. Nó không có ý nghĩa trong bất kỳ cách nào. –

+0

Tôi tự hỏi nếu chuỗi có thể được đúc thành IEnumerable và trình biên dịch được suy ra Tother là char? – n8wrl

+0

Tôi nghĩ đó là chính xác những gì nó đang làm nhưng tôi không gọi DoStuff , chỉ cần DoStuff. Tôi chưa bao giờ thấy giả định chung như thế. –

2

Trình biên dịch sẽ cố gắng khớp thông số với IEnumerable <T>. Kiểu String thực hiện IEnumerable <char>, do đó, nó giả định rằng T là "char".

Sau đó, trình biên dịch sẽ kiểm tra điều kiện khác "nơi OtherT: T" và điều kiện đó không được đáp ứng. Do đó lỗi trình biên dịch.

2

GUESS của tôi, và đây là một đoán bởi vì tôi không thực sự biết, là nó đầu tiên trông trong lớp dẫn xuất để giải quyết cuộc gọi phương thức (vì đối tượng của bạn là của loại có nguồn gốc). Nếu, và chỉ khi nó không thể, nó chuyển sang xem xét các phương thức lớp cơ sở để giải quyết nó. Trong trường hợp của bạn, vì nó CÓ THỂ giải quyết vấn đề này bằng cách sử dụng

DoStuff <Tother>(IEnumerable<Tother> collection) 

quá tải, nó đã cố gắng giải quyết vấn đề đó. Vì vậy, nó có thể giải quyết nó như xa như tham số có liên quan, nhưng sau đó nó chạm một snag trên các ràng buộc. Tại thời điểm đó, nó đã được giải quyết quá tải của bạn, vì vậy nó không nhìn xa hơn, nhưng chỉ ném một lỗi. Có lý?

+0

Trừ khi nó ngầm giả định Tother = char bất kể thực tế là tôi không xác định điều đó. Tôi chưa bao giờ thấy chung chung trên một phương pháp chỉ đơn giản là giả định bởi vì nó có thể gây nhiễu các đối số thành một. –

+1

Bạn chưa từng sử dụng LINQ chưa? Hầu hết LINQ được xây dựng trên ý tưởng rằng tham số kiểu có thể được giả định nếu kiểu được cung cấp trên một trong các tham số của phương thức. –

+2

Bạn đang xác định rằng Tother là char, bằng cách chuyển qua một số điện thoại IE2 có thể là , còn gọi là "chuỗi". Bạn có thể không quen thuộc với tính năng "phương pháp suy luận kiểu" của C#, nhưng nó đã được khoảng từ C# 2.0. Xem phần "suy luận kiểu" của blog của tôi nếu bạn muốn biết chi tiết về cách các thuật toán suy luận kiểu phương thức hoạt động; chúng khá hấp dẫn. –

3

Khi trình biên dịch chọn phương pháp DoStuff<Tother>(IEnumerable<Tother>) where Tother : T {} vì phương pháp chọn phương pháp trước khi kiểm tra ràng buộc. Vì chuỗi có thể làm IEnumerable<>, trình biên dịch khớp với chuỗi đó với phương thức lớp con đó. Trình biên dịch đang hoạt động chính xác như được mô tả trong đặc tả C#.

Thứ tự phân giải phương pháp bạn mong muốn có thể bị ép buộc bằng cách triển khai DoStuff dưới dạng extension method. phương pháp mở rộng được kiểm tra sau phương pháp lớp cơ sở, vì vậy nó sẽ không cố gắng để phù hợp với string chống DoStuff 's IEnumerable<Tother> cho đến khi sau nó đã cố gắng để phù hợp với nó chống lại DoStuff<T>.

Mã sau thể hiện thứ tự độ phân giải phương pháp mong muốn, hiệp phương sai và thừa kế. Vui lòng sao chép/dán nó vào một dự án mới.

Điều này lớn nhất nhược điểm Tôi có thể nghĩ đến thời điểm này là bạn không thể sử dụng base trong các phương pháp ghi đè, nhưng tôi nghĩ có những cách xung quanh đó (hỏi xem bạn có quan tâm không).

using System; 
using System.Collections.Generic; 

namespace MethodResolutionExploit 
{ 
    public class BaseContainer<T> : IEnumerable<T> 
    { 
     public void DoStuff(T item) { Console.WriteLine("\tbase"); } 
     public IEnumerator<T> GetEnumerator() { return null; } 
     System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return null; } 
    }   
    public class Container<T> : BaseContainer<T> { } 
    public class ContainerChild<T> : Container<T> { } 
    public class ContainerChildWithOverride<T> : Container<T> { } 
    public static class ContainerExtension 
    { 
     public static void DoStuff<T, Tother>(this Container<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainer.DoStuff<Tother>()"); 
     } 
     public static void DoStuff<T, Tother>(this ContainerChildWithOverride<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainerChildWithOverride.DoStuff<Tother>()"); 
     } 
    } 

    class someBase { } 
    class someChild : someBase { } 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Console.WriteLine("BaseContainer:"); 
      var baseContainer = new BaseContainer<string>(); 
      baseContainer.DoStuff(""); 

      Console.WriteLine("Container:"); 
      var container = new Container<string>(); 
      container.DoStuff(""); 
      container.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChild:"); 
      var child = new ContainerChild<string>(); 
      child.DoStuff(""); 
      child.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChildWithOverride:"); 
      var childWithOverride = new ContainerChildWithOverride<string>(); 
      childWithOverride.DoStuff(""); 
      childWithOverride.DoStuff(new List<string>()); 

      //note covariance 
      Console.WriteLine("Covariance Example:"); 
      var covariantExample = new Container<someBase>(); 
      var covariantParameter = new Container<someChild>(); 
      covariantExample.DoStuff(covariantParameter); 

      // this won't work though :(
      // var covariantExample = new Container<Container<someBase>>(); 
      // var covariantParameter = new Container<Container<someChild>>(); 
      // covariantExample.DoStuff(covariantParameter); 

      Console.ReadKey(); 
     } 
    } 
} 

Đây là kết quả:

BaseContainer: 
     base 
Container: 
     base 
     Container.DoStuff<Tother>() 
ContainerChild: 
     base 
     Container.DoStuff<Tother>() 
ContainerChildWithOverride: 
     base 
     ContainerChildWithOverride.DoStuff<Tother>() 
Covariance Example: 
     Container.DoStuff<Tother>() 

Bạn có thấy bất kỳ vấn đề với công việc này xung quanh?

+0

Hmm, ý tưởng thú vị. Tôi đã không đặt những tình trạng quá tải trên BaseContainer bởi vì tôi đang cố gắng giữ cho lớp đó rất cơ bản. Tôi có thể dễ dàng đặt DoStuff trên BaseContainer nhưng loại đó đánh bại mục tiêu của tôi là giữ BaseContainer cơ bản. Mở rộng có nghĩa là tôi giữ kỹ thuật BaseContainer theo cách tôi muốn nhưng ... :) –

+0

Colin, nó * không * đặt quá tải trên BaseContainer. tức là nó không làm hỏng Intellisense của BaseContainer. Hãy thử nó. Nếu tôi hiểu lầm, xin vui lòng làm rõ. – dss539

+0

Lời xin lỗi của tôi, tôi đã đọc sai nguyên mẫu phương pháp. Tuy nhiên, việc sử dụng tiện ích mở rộng trên lớp chung có nghĩa là tôi phải gọi nó bằng c.DoStuff thay vì chỉ c.DoStuff , phải không? –

0

Tôi không thực sự rõ ràng về những gì bạn đang cố gắng để hoàn thành, những gì đang ngăn cản bạn từ chỉ sử dụng hai phương pháp, DoStuff(T item) and DoStuff(IEnumerable<T> collection)?

+0

Vấn đề là khi bản thân T là một IEnumerable vì trình biên dịch chốt vào DoStuff (IEnumerable ), không phải DoStuff (T). –

Các vấn đề liên quan