Code review: method extensions oraz call\callvirt

Zaczynamy od próbki kodu:

static class StringExtensions
{
    public static void SayHello(this string str, string message)
    {
        Console.WriteLine(string.Format("Hello:{0}", message));
    }
}
internal class Program
{
    private static void Main(string[] args)
    {
        string str = null;
        str.SayHello("Piotr");
    }
}

Co według Was wydarzy się po uruchomieniu programu? Na pierwszy rzut oka może wydawać się, że wystąpi NullReferenceException ponieważ wywołujemy metodę na nieistniejącym obiekcie.  Nie doświadczymy tego jednak a to ze względu na użycie instrukcji call a nie callvirt.

W IL istnieją dwa sposoby wykonania metod: call oraz callvirt. W zdecydowanej większości wykorzystywany jest callvirt, który może wywołać również metody wirtualne (co sprowadza się do przeszukiwania tablicy wskaźników). Warto podkreślić, że w C# nawet metody niewirtualne wywoływane są przez callvirt a nie przez call. Instrukcja call z kolei wykorzystywana jest przez wszystkie metody statyczne oraz value type (struktury danych). Ważną różnicą między call a callvirt jest fakt, że callvirt zawsze sprawdzi czy dana instancja jest różna od NULL. Ze względu, że powyższa klasa jest statyczna, to kompilator wyemitował instrukcję call, która nie sprawdza czy instancja jest NULL’em. Podobny kod, ale wywołujący zwykłą metodę, zakończyłby się wyjątkiem ponieważ w c# wszystkie inne metody typu instance na klasach wykorzystują callvirt ( a nie call).

Ktoś może zadać pytanie: dlaczego callvirt jest również wykorzystywany dla niewirtualnych metod? Odpowiedzią jest powyższy fragment kodu, który zachowuje się dziwnie. Gdy programista widzi wywołanie metody na obiekcie NULL myśli, że zakończy się to NullReferenceException. Team c# uznał, że lepiej użyć callvirt i sprawdzać za każdym razem czy instancja jest różna od NULL. Niestety nie wzięli pod uwagę rozszerzeń metod, które tak naprawdę są statycznymi klasami.

Nie wszystkie języki jednak emitują callvirt dla metod niewirtualnych. Niektóre korzystają z call i dlatego taki kod może w niektórych językach zadziałać:

internal class Program
{
    private void SayHello()
    {
        Console.WriteLine("Hello");
        
    }
    private static void Main(string[] args)
    {
        Program p = null;
        p.SayHello();
    }
}

W C# wywoła NullReferenceException ponieważ SayHello jest instance-method i wyemitowana zostanie instrukcja callvirt. Należy jednak zdawać sobie sprawę jakie ryzyko istnieje gdy piszemy biblioteki. Załóżmy, że mamy klasę z niewirtualną metodą w bibliotece A. Następnie biblioteka B wywołuję tą metodę. Wszystko na tym etapie działa. Ale po kilku miesiącach zmieniamy implementację w bibliotece A i metoda od teraz jest wirtualna. Tutaj pojawia się problem jeśli nie skompilujemy ponownie biblioteki B. Biblioteka B w końcu w niektórych językach mogła wyemitować po prostu call a nie callvirt. Z tego względu, bez rekompilacji, metoda będzie wywoływana zawsze niewirtualnie pomimo, że została już zmieniona na wirtualną. Oczywiście w c# taki problem nie istnieje ponieważ tam zawsze emitowany jest callvirt.

2 thoughts on “Code review: method extensions oraz call\callvirt”

  1. Przykład zmiany metody w A ze zwykłej na wirtualną pachnie złym stylem programowania, bo narusza wsteczną kompatybilność ustalonego interfejsu. No ale reguły regułami (są po to aby je łamać) a życie życiem (racja musi być po naszej stronie).

  2. @Pawel:
    Jest to zla praktyka i nie nalezy jej robic – a dlaczego, to wlasnie pisalem w poscie:)

Leave a Reply

Your email address will not be published.