piątek, października 31, 2008

Czy polimorfizm w C# jest za słaby?

Polimorfizm jest bardzo przydatną cechą języków, która umożliwia skrócenie pisanego kodu, zwiększenie giętkości pisanych klas czy bibliotek, zwiększenie poziomu abstrakcji pisanego kodu - co w ostatecznym rozrachunku przekłada się na zmniejszenie nakładu pracy związanego z budową programu.

W C# mamy dostępne tzw. generics (link, link). Umożliwiają one np. tworzenie kontenerów dla obiektów dowolnego typu, podobnie jak wygląda to w C++:
[C#, 1]
Queue<int> kolejka_intow = new Queue<int>();

Wszystko wygląda fajnie, tylko że brakuje jednej rzeczy: możliwości "zaglądania" do obiektów polimorficznych. Nie możemy na przykład napisać funkcji:

public static T addGenerics<T>(T a, T b)
{
    return a + b;
}

Dlaczego? Kompilator słusznie wyrzuci nam:
Operator '+' cannot be applied to operands of type 'T' and 'T'

Z drugiej strony kod:

[C#, 2]
public static void testDoWithInstance()
{
     doWithInstance<int>(2);
     doWithInstance<float>(2);
     doWithInstance<string>("aaa");
     doWithInstance<int[]>(new int[] { 1, 2, 3 });
}

public static void doWithInstance<T>(T inst)
{
     System.Console.WriteLine(string.Format("<<{0}>>", inst));
}

Skompiluje się i wyświetli bez zarzutu:

<<2>>
<<2>>
<<aaa>>
<<System.Int32[]>>

Gdzie tkwi więc problem? Kod typu [2] skompiluje się, a w razie problemów (obiekt T nie może być przekształcony na napis) rzuci wyjątek. Oczywiście ktoś mógłby powiedzieć, że wszystko można jakoś wyświetlić, ale to nie jest sedno problemu. Sednem problemu jest to, że nie odkrycie czy dany obiekt obsługuje jakąś metodę odsuwamy do momentu wykonania programu. C# staje się więc w ten sposób efektywnie językiem dynamicznym.

Kod typu [1] natomiast wcale się nie będzie kompilować wcale. Kompilator nie wie z góry, czy obiekt typu T można dodać do drugiego obiektu typu T, innymi słowy czy obsługują one dodawanie. Można to obejść stosując mechanizm Reflection, który umożliwia analizowanie obiektów w trakcie wykonania programu. To rozwiązanie sprowadza się więc praktycznie do tego które mamy w [2], z tym, że nie możemy po prostu zrzutować danego obiektu na typ Object. Mechanizm Reflection ma dwie zasadnicze wady:
- jest dynamiczny, generuje wyjątki w trakcie działania programu, nie błędy kompilacji
- używanie go jest niewygodne i wymaga pisania dużych ilości dodatkowego kodu

Rozwiązaniem jest wprowadzenie kontekstów w postaci interfejsów do typów parametrycznych. Gwarantowałyby one implementację zadanych metod. Przykładowo:

[C#, 3]
public static T f<T implementing IPlus, IMinus>( T a, T b)
{
     return (a + a - b);
}
Z tego co się orientuje, nie ma jeszcze tego typu rozszerzenia w C#. A może się mylę?

2 komentarze:

  1. Takie zachowanie C# było jednym z głównych powodów dla którego w zeszłym roku bardzo narzekałem na ten język... Kto wie, może gdyby generyki działały sensownie to dzisiaj pisałbym właśnie w C# a nie w C++?

    W każdym bądź razie jak będziesz wiedział jak rozwiązać ten problem to podziel się rozwiązaniem ze mną ;-)

    Z tego co pamiętam Java ma podobne problemy... co często prowadzi do rzutowania na Object [fuj ;-/].

    OdpowiedzUsuń
  2. Anonimowy11:58 AM

    Pewnie że tak można tylko trzeba poszukać a nie od razu narzekać :P

    OdpowiedzUsuń