2009-07-30 16 views
32

Biorąc pod uwagę następujące struct:Błąd kompilatora C#? Dlaczego ta niejawna kompilacja konwersji zdefiniowana przez użytkownika?

public struct Foo<T> 
{ 
    public Foo(T obj) { } 

    public static implicit operator Foo<T>(T input) 
    { 
     return new Foo<T>(input); 
    } 
} 

Ten kod kompiluje:

private Foo<ICloneable> MakeFoo() 
{ 
    string c = "hello"; 
    return c; // Success: string is ICloneable, ICloneable implicitly converted to Foo<ICloneable> 
} 

ale ten kod nie kompiluje - dlaczego?

private Foo<ICloneable> MakeFoo() 
{ 
    ICloneable c = "hello"; 
    return c; // Error: ICloneable can't be converted to Foo<ICloneable>. WTH? 
} 

Odpowiedz

30

Najwyraźniej niejawne konwersje zdefiniowane przez użytkownika nie działają, gdy jeden z typów jest interfejsem. Od C# Specyfikacja:


6.4.1 Dozwolone zdefiniowanych przez użytkownika konwersje

C# pozwala jedynie pewne konwersje zdefiniowane przez użytkownika, aby zostać uznane. W szczególności nie jest możliwe ponowne zdefiniowanie już istniejącej niejawnej lub jawnej konwersji. Dla danego źródła typu S i typu docelowego T, jeśli S lub T są typu zerowego, niech S0 i T0 odnoszą się do ich podstawowych typów, w przeciwnym razie S0 i T0 są równe odpowiednio S i T. Klasa lub struktura może zadeklarować konwersję ze źródła typu S na docelowy typ T tylko wtedy, gdy spełnione są wszystkie następujące warunki:

  • S0 i T0 to różne typy.
  • S0 lub T0 jest klasą lub typem struktury, w których odbywa się deklaracja operatora.
  • Ani S0, ani T0 nie są typu interfejsu.
  • Bez konwersji zdefiniowane przez użytkownika, konwersja nie istnieje z S na T lub T w pozycji S.

w pierwszym sposobie, nie są oba rodzaje interfejsu typów, więc użytkownik zdefiniowane ukryte konwersja działa.

Specyfikacja nie jest bardzo jasna, ale wydaje mi się, że jeśli jednym z typów jest typ interfejsu, kompilator nawet nie próbuje wyszukać żadnych niejawnych konwersji zdefiniowanych przez użytkownika.

+0

Wow. Cóż za dziwne wymaganie. Chciałbym usłyszeć od Lipperta, Skeeta lub innego eksperta od C#, dlaczego typy interfejsu nie działają w tym celu; z pewnością musi być dobry powód tego dziwactwa. –

+0

Po kilku eksperymentach, wydaje mi się, że kluczowe są tutaj interfejsy. Co dziwne, wydaje się, że nie jest to mentalny skok, aby kompilator mógł wymyślić, co robić często. Hmm. –

+0

Byłbym bardzo zainteresowany wyjaśnieniem, w jaki sposób kompiluje się pierwsza próbka. Biorąc pod uwagę zasady w sekcji 6.4.4, nie widzę sposobu, w jaki wybrał on 'Foo ', ponieważ 'ICloneable' nie obejmuje' ciągu' (ponieważ 'ICloneable' jest interfejsem) i' Foo 'jest nieobjęte "Foo " (ponieważ nie ma żadnej niejawnej konwersji z 'Foo ' na 'Foo '). Może 6.1.9 "Niejawne konwersje z udziałem parametrów typu" wchodzi w grę w jakiś sposób? – zinglon

24

(Nawiązując do uwag zaakceptowanej odpowiedzi.)

Tak, to jest bardzo, bardzo mylące część spec. Cały fragment dotyczący "obejmujących typy" jest w szczególności głęboko błędny. Od kilku lat próbuję znaleźć czas, aby całkowicie przepisać całą tę sekcję w coś bardziej spójnego, ale nigdy nie było to wystarczająco priorytetowe.

Zasadniczo mamy tutaj sprzeczność; my powiedzieć, że nie ma żadnych niejawnych konwersji zdefiniowanych przez użytkownika z udziałem interfejsów, ale wyraźnie, że nie jest to prawdą w tym przypadku; istnieje niejawna konwersja z IC na zdefiniowaną przez użytkownika do Foo<IC>, o czym świadczy fakt, że ciąg przechodzi do Foo<IC> przez tę konwersję.

Co tak naprawdę powinien być podkreślając lepsze jest to, że linia cytowany:

W szczególności nie jest możliwe przedefiniować już istniejącego niejawny lub wyraźnej konwersji.

To właśnie motywuje tę całość; pragnienie, aby nie pozwolić ci kiedykolwiek myśleć, że robisz test typu zachowującego reprezentację, gdy w rzeczywistości wywołujesz metodę zdefiniowaną przez użytkownika. Rozważmy na przykład tę odmianę:

interface IBar {} 
interface IFoo : IBar {} 
class Foo<T> : IFoo 
{ 
    public static explicit operator Foo<T>(T input) { whatever } 
} 
class Blah : Foo<IBar> {} 
... 
IBar bar = new Blah(); 
Foo<IBar> foo = (Foo<IBar>)bar; 

Teraz czy to zadzwonić do zdefiniowanego przez użytkownika wyraźnej konwersji, czy nie? Obiekt naprawdę pochodzi od Foo, więc możesz mieć nadzieję, że tak się nie stanie; powinien to być prosty test typu i przypisanie referencji, a nie wywołanie metody pomocniczej. Rzut na wartość interfejsu jest zawsze traktowany jako test typu, ponieważ prawie zawsze jest możliwe, że obiekt rzeczywiście jest tego typu i naprawdę implementuje ten interfejs. Nie chcemy odmówić ci możliwości zrobienia taniej konwersji zachowującej reprezentację.

+0

Ok, specjalne zasady dotyczące rzutowania na interfejsy i przyczyny tych reguł pomagają mi to zrozumieć. Dzięki za wyjaśnienie, Eric. –

+0

Powinienem dodać, moja próbka kodu "czuje się" tak, jak powinna działać. Coś w rodzaju kowariancji, to jedna z tych rzeczy, które twój umysł mówi, że powinna działać, ale nie. Heh. –

+0

Czy to oznacza, że ​​w powyższym przykładzie (Foo ) pasek jest traktowany jako "rzut" zamiast wyraźnej konwersji? –

Powiązane problemy