Tại sao trình biên dịch C # phàn nàn rằng "các kiểu có thể thống nhất" khi chúng bắt nguồn từ các lớp cơ sở khác nhau?


77

Mã không biên dịch hiện tại của tôi tương tự như sau:

public abstract class A { }

public class B { }

public class C : A { }

public interface IFoo<T>
{
    void Handle(T item);
}

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Trình biên dịch C # từ chối biên dịch điều này, trích dẫn quy tắc / lỗi sau:

'MyProject.MyFoo <TA>' không thể triển khai cả 'MyProject.IFoo <TA>' và 'MyProject.IFoo <MyProject.B>' vì chúng có thể thống nhất đối với một số thay thế tham số kiểu

Tôi hiểu lỗi này có nghĩa là gì; nếu TAcó thể là bất cứ thứ gì thì về mặt kỹ thuật, nó cũng có thể là một Bthứ sẽ dẫn đến sự mơ hồ về hai cách Handletriển khai khác nhau .

Nhưng TA không thể là gì cả. Dựa trên hệ thống phân cấp kiểu, TA không thể là B- ít nhất, tôi không nghĩ nó có thể. TAphải bắt nguồn từ A, không bắt nguồn từ B, và rõ ràng là không có nhiều lớp kế thừa trong C # /. NET.

Nếu tôi xóa tham số chung và thay thế TAbằng C, hoặc thậm chí A, tham số đó sẽ biên dịch.

Vậy tại sao tôi lại gặp lỗi này? Đó có phải là một lỗi trong hoặc sự thiếu thông minh chung của trình biên dịch, hay là tôi đang thiếu thứ gì khác?

Có cách giải quyết nào không hay tôi sẽ phải triển khai lại MyFoolớp chung chung như một lớp không chung chung riêng biệt cho mọi TAkiểu dẫn xuất có thể có?


2
Tôi nghĩ TItem nên đọc TA, không?
Jeff

Nó không có khả năng là một lỗi trong trình biên dịch. Công bằng mà nói, thông báo lỗi sử dụng các từ "có thể thống nhất", tôi đoán là do bạn sử dụng cả hai giao diện.
Security Hound

Chỉnh sửa: Đừng bận tâm, tôi đọc rằng B là một tham số kiểu. <strike> Điều gì ngăn bạn chuyển cùng một thứ để nhập tham số Bkhi bạn truyền đến TA? </strike>
Josh

1
@JoshEinstein: Bkhông phải là một tham số kiểu, nó là một kiểu thực tế. Tham số kiểu duy nhất là TA.
Aaronaught

1
@Ramhound Tôi không hiểu tại sao nó "không chắc" là lỗi trình biên dịch. Tôi không thể thấy lời giải thích nào khác, thực sự. Đó có vẻ là một sai lầm dễ mắc phải và tình huống không mấy khi xảy ra.
kmkemp

Câu trả lời:


50

Đây là hệ quả của mục 13.4.2 của đặc tả C # 4, trong đó nêu rõ:

Nếu bất kỳ kiểu được xây dựng nào có thể được tạo ra từ C, sau khi các đối số kiểu được thay thế thành L, khiến hai giao diện trong L giống hệt nhau, thì việc khai báo C không hợp lệ. Khai báo ràng buộc không được xem xét khi xác định tất cả các kiểu cấu trúc có thể có.

Lưu ý rằng câu thứ hai ở đó.

Do đó, nó không phải là một lỗi trong trình biên dịch; trình biên dịch là chính xác. Người ta có thể cho rằng đó là một lỗ hổng trong đặc tả ngôn ngữ.

Nói chung, các ràng buộc bị bỏ qua trong hầu hết mọi tình huống trong đó một thực tế phải được suy ra về một loại chung chung. Các ràng buộc chủ yếu được sử dụng để xác định lớp cơ sở hiệu quả của một tham số kiểu chung, và một số ít khác.

Thật không may, điều đó đôi khi dẫn đến những tình huống mà ngôn ngữ khắt khe một cách không cần thiết, như bạn đã phát hiện.


Nói chung, việc thực thi giao diện "giống nhau" hai lần sẽ rất tệ, theo một cách nào đó, chỉ được phân biệt bằng các đối số kiểu chung. Chẳng hạn, thật kỳ lạ khi có class C : IEnumerable<Turtle>, IEnumerable<Giraffe>- C là gì khi nó vừa là chuỗi rùa, vừa là chuỗi hươu cao cổ, cùng một lúc ? Bạn có thể mô tả điều thực tế mà bạn đang cố gắng làm ở đây không? Có thể có một mô hình tốt hơn để giải quyết vấn đề thực sự.


Nếu thực tế giao diện của bạn đúng như bạn mô tả:

interface IFoo<T>
{
    void Handle(T t);
}

Sau đó, đa kế thừa của giao diện lại xuất hiện một vấn đề khác. Bạn có thể quyết định một cách hợp lý để làm cho giao diện này trở nên trái ngược:

interface IFoo<in T>
{
    void Handle(T t);
}

Bây giờ giả sử bạn có

interface IABC {}
interface IDEF {}
interface IABCDEF : IABC, IDEF {}

class Danger : IFoo<IABC>, IFoo<IDEF>
{
    void IFoo<IABC>.Handle(IABC x) {}
    void IFoo<IDEF>.Handle(IDEF x) {}
}

Và bây giờ mọi thứ trở nên thực sự điên rồ ...

IFoo<IABCDEF> crazy = new Danger();
crazy.Handle(null);

Quá trình triển khai nào của Xử lý được gọi ???

Xem bài viết này và các bình luận để biết thêm suy nghĩ về vấn đề này:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx


1
@asawyer: Đúng. Nó được xác định bởi việc triển khai mà CLR thực hiện trong trường hợp này. Và trên thực tế, bạn có thể xây dựng các tình huống phức tạp hơn trong đó nó được xác định bởi CLI spec mà việc triển khai sẽ thực hiện, và CLR nhận một "sai". Tôi không chắc liệu nhóm CLR và chủ sở hữu thông số kỹ thuật CLI đã giải quyết tranh chấp đó chưa; ý kiến ​​cá nhân của tôi là các quy tắc CLI thực sự cho kết quả ít tốt hơn các quy tắc CLR trong các trường hợp có thể xảy ra nhất. (Không phải là bất kỳ trường hợp rất phổ biến; họ là tất cả các trường hợp góc khá lạ.)
Eric Lippert

3
Chắc chắn, không có ý nghĩa gì khi các giao diện là các tham số kiểu; Tôi đã giả định không chính xác rằng việc sử dụng các lớp cụ thể sẽ giải quyết được vấn đề. Đối với kịch bản, lớp là một Saga, lớp này phải triển khai một số trình xử lý thông báo (một trình xử lý cho mỗi thông báo là một phần của saga). Có khoảng 10 sagas gần giống nhau mà điểm khác biệt duy nhất là kiểu cụ thể chính xác của một trong các thông báo, nhưng tôi không thể có trình xử lý thông báo trừu tượng, vì vậy tôi nghĩ tôi sẽ cố gắng sử dụng một lớp cơ sở chung và chỉ sử dụng bó các lớp sơ khai; giải pháp thay thế duy nhất là sao chép và dán nhiều.
Aaronaught

22
@Eric: Tình huống triển khai cùng một giao diện chung với nhiều lần có nhiều khả năng xảy ra hơn nếu bạn coi một giao diện giống IComparable<T>hoặc IEquatable<T>đúng hơn IEnumerable<T>. Thật hợp lý khi có một đối tượng có thể được so sánh với nhiều loại ... trên thực tế, tôi đã gặp phải vấn đề thống nhất loại vài lần cho trường hợp chính xác này.
LBushkin

4
@Eric, LBushkin là chính xác. Rằng một số cách sử dụng là không hợp lý, không có nghĩa là tất cả các cách sử dụng đều không hợp lý. Vấn đề này cũng làm tôi khó chịu vì tôi đang triển khai giao diện phân tích cú pháp trừu tượng IFoo <T0, T1, ..> triển khai một giao diện IParsable <T0>, IParsable <T1>, ... với một tập hợp các phương thức mở rộng được định nghĩa trên IParsable , nhưng bây giờ điều đó dường như là không thể. C # / CLR có nhiều trường hợp góc khó chịu như thế này.
naasking

1
@EricLippert Tôi có đúng khi giả định rằng đã có thêm các tính năng để cho phép sự mơ hồ trong các điều kiện "nhất định" không? Có vẻ như bây giờ bạn (.NET 4.5) được phép thể hiện ví dụ trên, trong khi phiên bản chung vẫn không được phép, tức class Danger : IFoo<IABC>, IFoo<DEF> {...}là được cho phép, trong khi class Danger<T1,T2> : IFoo<T1>, IFoo<T2>vẫn không được phép. Có vẻ như việc phân định được thực hiện bởi một trọng tài xác định, nơi mà việc triển khai cụ thể nhất được xác định đầu tiên được sử dụng (nếu áp dụng nhiều lần)?
micdah 22/10/12

8

Rõ ràng nó là do thiết kế như đã thảo luận tại Microsoft Connect:

Và cách giải quyết là, xác định một giao diện khác là:

public interface IIFoo<T> : IFoo<T>
{
}

Sau đó, thực hiện điều này thay thế như:

public class MyFoo<TA> : IIFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Bây giờ nó biên dịch tốt, bằng đơn âm .


Được ủng hộ cho liên kết Connect; rất tiếc, giải pháp thay thế không biên dịch với trình biên dịch của Microsoft.
Aaronaught

@Aaronaught: Hãy thử làm IIFoomột abstract classlúc sau.
Nawaz

Điều đó không biên dịch, mặc dù rõ ràng là nó ngăn tôi lấy từ một lớp cơ sở khác. Tôi nghĩ rằng tôi có thể thực sự có thể làm cho điều này thành công, mặc dù nó sẽ yêu cầu một cấp độ trung gian khác (hackish / vô dụng) trong hệ thống phân cấp lớp.
Aaronaught

Điều này thực sự vi phạm thông số kỹ thuật nếu tôi không nhầm. Phần mà Eric đã trích dẫn chỉ rõ rằng nó không nên có, phải không?
cấu hình

2
@Aaronaught: Là hợp pháp khi một loại triển khai cùng một giao diện hai lần thông qua chuỗi kế thừa của nó (để ẩn và giảm thiểu lớp cơ sở giòn). Nhưng việc triển khai cùng một giao diện hai lần tại cùng một nơi là không hợp pháp - bởi vì không có giao diện nào 'tốt hơn'.
cấu hình

4

Bạn có thể chụp lén nó dưới radar nếu bạn đặt một giao diện trên một lớp cơ sở.

public interface IFoo<T> {
}

public class Foo<T> : IFoo<T>
{
}

public class Foo<T1, T2> : Foo<T1>, IFoo<T2>
{
}

Tôi nghi ngờ điều này hoạt động vì nếu các kiểu "thống nhất" thì rõ ràng việc triển khai của lớp dẫn xuất sẽ thắng.


2

Xem câu trả lời của tôi cho câu hỏi cơ bản tương tự tại đây: https://stackoverflow.com/a/12361409/471129

Ở một mức độ nào đó, điều này có thể được thực hiện! Tôi sử dụng một phương pháp phân biệt, thay vì (các) định tính giới hạn các loại.

Nó không thống nhất, trên thực tế, nó có thể tốt hơn nếu nó đã làm vì bạn có thể tách rời các giao diện riêng biệt.

Xem bài đăng của tôi ở đây, với một ví dụ hoạt động đầy đủ trong ngữ cảnh khác. https://stackoverflow.com/a/12361409/471129

Về cơ bản, những gì bạn làm là thêm một tham số kiểu khác vào IIndexer , để nó trở thành IIndexer <TKey, TValue, TDifferentiator>.

Sau đó, khi bạn sử dụng nó hai lần, bạn chuyển "First" cho lần sử dụng đầu tiên và "Second" cho lần sử dụng thứ hai

Vì vậy, lớp Kiểm tra trở thành: lớp Test<TKey, TValue> : IIndexer<TKey, TValue, First>, IIndexer<TValue, TKey, Second>

Vì vậy, bạn có thể làm new Test<int,int>()

trong đó Thứ nhất và Thứ hai là tầm thường:

interface First { }

interface Second { }

1

Tôi biết nó đã được một thời gian kể từ khi chủ đề được đăng nhưng cho những người xuống chủ đề này thông qua công cụ tìm kiếm để được giúp đỡ. Lưu ý rằng 'Cơ sở' là viết tắt của lớp cơ sở cho TA và B ở bên dưới.

public class MyFoo<TA> : IFoo<Base> where TA : Base where B : Base
{
    public void Handle(Base obj) 
    { 
       if(obj is TA) { // TA specific codes or calls }
       else if(obj is B) { // B specific codes or calls }
    }

}

0

Hãy đoán ngay bây giờ ...

Không thể khai báo A, B và C trong các tập hợp bên ngoài, nơi mà hệ thống phân cấp kiểu có thể thay đổi sau khi biên dịch MyFoo <T>, đưa sự tàn phá vào thế giới?

Cách giải quyết dễ dàng chỉ là triển khai Xử lý (A) thay vì Xử lý (TA) (và sử dụng IFoo <A> thay vì IFoo <TA>). Dù sao thì bạn cũng không thể làm được nhiều hơn với Handle (TA) so với các phương thức truy cập từ A (do ràng buộc A: TA).

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}

Không với điều này >> Couldn't A, B and C be declared in outside assemblies, where the type hierarchy may change after the compilation of MyFoo<T>, bringing havoc into the world?.
Nawaz

Thay đổi cấu trúc phân cấp kiểu sẽ làm mất hiệu lực của bất kỳ chương trình nào ; Nó không có ý nghĩa gì đối với tôi rằng trình biên dịch sẽ đưa ra quyết định của nó dựa trên bất kỳ thứ gì khác ngoài hệ thống phân cấp kiểu hiện tại (mặc dù, tôi đoán nó không có ý nghĩa gì đối với tôi hơn chính lỗi tại thời điểm này. ..) Đối với giải pháp thay thế, tốt, điều đó sẽ biên dịch nhưng nó không còn chung chung nữa, vì vậy nó không thực sự giúp ích nhiều.
Aaronaught

0

Hmm, những gì về điều này:

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    void IFoo<TA>.Handle(TA a) { }
    void IFoo<B>.Handle(B b) { }
}

2
Không, lỗi là ở chính lớp đó; nó không phụ thuộc vào cách giao diện được triển khai.
Qwertie
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.