Mockowanie - interfejs vs virtual

0

Kiedy powinno się utworzyć interfejs do klasy a kiedy oznaczyć metody jako wirtualne w celu umożliwienia mockowania?

1

Jak ze wszystkim - to zależy. W praktyce osobiście stosuję metody wirtualne tylko jeśli z jakiegoś powodu nie mogę ekstrahować interfejsu.

0

@john_klamka: Ale dlaczego? Chodzi o wydajność metod wirtualnych? O ile się nie myle, to @Wibowit pisał kiedyś, że metody wirtualne obniżają wydajność.

1
nobody01 napisał(a):

@john_klamka: Ale dlaczego? Chodzi o wydajność metod wirtualnych? O ile się nie myle, to @Wibowit pisał kiedyś, że metody wirtualne obniżają wydajność.

Wydajność? a to nie są jakieś tam nanosekundy per setki milionów wywołań?

Interface Performance. Are interface calls faster than virtual ones?

Według mnie metody wirtualne = wchodzimy głębiej w OOP = zaczynają się problemy i wypadałoby przemyśleć swoje życie :D

Should I use virtual methods to make mocking possible?

How would virtual methods help? The idea of mocking is that you rip out a class completely from your application and plug in a completely different mocked class, with the only thing in common that they both implement the same interface. Inheritance doesn't come into the game at all.


Virtual methods and overriding them is not a good way to make mocks compared to interfaces.
Because you will have to reference the underlying class to create your mock. So your tests will be reliant on a reference to that particular library
And also because you will expose methods that you otherwise might not want to.


If youre needing to make your methods virtual in order to test them (i.e. youre expanding the public API of your class purely to accommodate your unit tests), then its a good sign that your class has too much work to do.

2

Jeśli mockujesz klasę z metodami wirtualnymi to wciąż uruchamiasz jej konstruktor, więc jeśli masz tam np. jakąś walidację to wymusza na Tobie zbudowanie poprawnego obiektu do testów, chociaż niekoniecznie to ma sens.

0

@mad_penguin: Nie wystarczy po prostu utworzyć bezparametrowy konstruktor?

2

Jeśli nie masz żadnego konstruktora to automatycznie masz bezparametrowy, a jeśli masz już jakiś inny, to tworzenie konstruktora, który nie konstruuje poprawnego obiektu specjalnie do testów jest złym pomysłem, bo ktoś go może prędzej czy później użyć poza testami :)
Swoją drogą moje doświadczenie z testami jest niewielkie, więc to tylko moje 3gr.

0

Właściwie to chodzi mi o to, że ludzie piszą, aby nie pisać niepotrzebnych interfejsów, nie bać się własnego kodu itd. Ale z drugiej strony, preferowanym podejściem przy mockowaniu wydaje się być właśnie tworzenie interfejsów.

1

Nie zdażyło mi się jeszcze spotkać sytuacji, w której metody wirtualne powodowałyby problemy wydajnościowe - ale z kolei mało w życiu widziałem. Bardziej chodziło mi o to, że interfejs jest zwyczajnie wygodniejszy w mockowaniu - nie masz, tak jak wyżej zostało napisane, żadnego konstruktora, w którym może być wszystko ("tu żyją smoki" ;)).

0

Właściwie to chodzi mi o to, że ludzie piszą, aby nie pisać niepotrzebnych interfejsów

Kto pisze i w jakim kontekście? Jeśli chodzi o wydajność to nie ma to znaczenia, a jeśli będzie miało to sam będziesz o tym wiedział.

0
nobody01 napisał(a):

@john_klamka: Ale dlaczego? Chodzi o wydajność metod wirtualnych? O ile się nie myle, to @Wibowit pisał kiedyś, że metody wirtualne obniżają wydajność.

W starym .NETu w zasadzie wszystko obniża wydajność. Interfejsy, metody wirtualne, tworzenie krótko żyjących obiektów, itd .NET Core ma już wbudowaną dewirtualizację. Nie testowałem jej, ale możliwe, że w końcu .NET doścignął Javę 6u24 z 2011 roku.

Okazuje się, że wszystkie metody z intefejsów są wirtualne:
https://stackoverflow.com/a/3621456

Quoting Jeffrey Ritcher from CLR via CSharp 3rd Edition here
The CLR requires that interface methods be marked as virtual. If you do not explicitly mark the method as virtual in your source code, the compiler marks the method as virtual and sealed; this prevents a derived class from overriding the interface method. If you explicitly mark the method as virtual, the compiler marks the method as virtual (and leaves it unsealed); this allows a derived class to override the interface method. If an interface method is sealed, a derived class cannot override the method. However, a derived class can re-inherit the same interface and can provide its own implementation for the interface’s methods.

Stary .NET miał jakieś mikrooptymalizacje dla domyślnych typów metod w interfejsach (virtual + sealed), więc ekstrahowanie interfejsów dla zysków w wydajności mogło się przerodzić w cargo cult.

Bardziej chodziło mi o to, że interfejs jest zwyczajnie wygodniejszy w mockowaniu - nie masz, tak jak wyżej zostało napisane, żadnego konstruktora, w którym może być wszystko ("tu żyją smoki" ;)).

Konstruktor z efektami ubocznymi to w ogólności rak. Zamiast przykrywać problem tworząc zbędny interfejs można przenieść efekty uboczne do metody fabrykującej. Ale co kto lubi :)

Polecam lekturę tego: http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/

0

Konstruktor z efektami ubocznymi to w ogólności rak. Zamiast przykrywać problem tworząc zbędny interfejs można przenieść efekty uboczne do metody fabrykującej. Ale co kto lubi :)

Sugerujesz, ze lubię raka? Nie zawsze istnieje możliwość dowolnej modyfikacji zastanego kodu (np. gdy chcesz zamockować moduł pisany przez inny team w innym projekcie w innej organizacji na innym kontynencie).

0

Sugerujesz, ze lubię raka? Nie zawsze istnieje możliwość dowolnej modyfikacji zastanego kodu (np. gdy chcesz zamockować moduł pisany przez inny team w innym projekcie w innej organizacji na innym kontynencie).

Jeśli nie masz kontroli nad klasą to tym bardziej nie dorzucisz jej interfejsu.

How would virtual methods help? The idea of mocking is that you rip out a class completely from your application and plug in a completely different mocked class, with the only thing in common that they both implement the same interface. Inheritance doesn't come into the game at all.

Virtual methods and overriding them is not a good way to make mocks compared to interfaces.
Because you will have to reference the underlying class to create your mock. So your tests will be reliant on a reference to that particular library
And also because you will expose methods that you otherwise might not want to.

If youre needing to make your methods virtual in order to test them (i.e. youre expanding the public API of your class purely to accommodate your unit tests), then its a good sign that your class has too much work to do.

Pitu pitu. Jak to się ma do robienia ekstra interfejsu na wzór zewnętrznej klasy + ekstra wewnętrznej klasy która jedynie deleguje do zewnętrznej? Gdzie tu jest "too much work" skoro jedynym zadaniem wewnętrznej klasy jest delegowanie do innej? Pamiętaj, że każda metoda z intefejsu jest wirtualna.

1

Interfejsów się w gruncie rzeczy używa najczęściej w 2 sytuacjach:

  1. Jako kontrakt użycia zewnętrznego modułu,
  2. Jeśli planujemy wiele implementacji.

A metod wirtualnych z kolei w zupełnie innych przypadkach.

Większości rzeczy nie musisz mockować, nie ma za bardzo sensu tego robić, a jeśli już musisz to użyj interfejsu.

5
nobody01 napisał(a):

Kiedy powinno się utworzyć interfejs do klasy

Wtedy, gdy klasa jest elementem jakiegoś wzorca projektowego bazującego na wspólnym interfejsie (np. strategia albo chain of responsibility) albo służy do komunikacji ze światem zewnętrznym (dostęp do bazy danych, czytanie z pliku, pobieranie danych z webserwisu, itp.).

a kiedy oznaczyć metody jako wirtualne w celu umożliwienia mockowania?

Wtedy gdy:

  1. chce się testować kod napisany specjalnie dla potrzeb testów zamiast faktycznego;
  2. lubi się naprawiać dziwne bugi, których testy nie pokryły;
  3. pisze ogromne klasy łamiące SRP;
  4. uważa się za dobry pomysł zmianę kodu produkcyjnego tylko po to, aby spełnić jakieś ograniczenia narzędzi testowych.
  5. jest się programistą Javy, który nie rozumie, że C# jest inaczej skonstruowany, i próbuje w nim emulować cechy Javy w celu sprowadzenia na siebie problemów z punktów wyżej.

Celem virtual jest umożliwienie polimorficznych wywołań, a nie testowania!

john_klamka napisał(a):

Jak ze wszystkim - to zależy. W praktyce osobiście stosuję metody wirtualne tylko jeśli z jakiegoś powodu nie mogę ekstrahować interfejsu.

Nie istnieją takie przypadki, że się nie da. Co najwyżej nie da się jednym kliknięciem, i trzeba napisać kawałek kodu, który jakoś opakuje ten nieekstrahowalny twór, albo w ogóle trzeba się wreszcie zastanowić nad architekturą, bądź sensem testowania.

nobody01 napisał(a):

Właściwie to chodzi mi o to, że ludzie piszą, aby nie pisać niepotrzebnych interfejsów, nie bać się własnego kodu itd. Ale z drugiej strony, preferowanym podejściem przy mockowaniu wydaje się być właśnie tworzenie interfejsów.

Ale to się w niczym nie kłóci. Po prostu twórz interfejsy tylko do tych klas, które jest sens mockować, a reszty nie mockuj.

0

Ja jeszcze tylko pokażę, że wyodrębnienie interfejsu na potrzeby testowania (co za dziwny pomysł) wcale nie zabezpiecza przed nadpisywaniem metod, gdyż metody w interfejsach są wirtualne. Przykładzik:

using System;
 
public class Test
{
	interface Interfejs {
		void metodaWirtualna();
	}
 
	class Klasa1 : Interfejs {
		public virtual void metodaWirtualna() {
			Console.WriteLine("Klasa1");
		}
	}
 
	class Klasa2 : Klasa1 {
		public override void metodaWirtualna() {
			Console.WriteLine("Klasa2");
		}
	}
 
	static void Wypisz(Interfejs obiekt) {
		obiekt.metodaWirtualna();
	}
 
	public static void Main()
	{
		Wypisz(new Klasa1());
		Wypisz(new Klasa2());
	}
}

https://www.ideone.com/fJIHXH
Wypisuje:

Klasa1
Klasa2

Dopiero jeśli ktoś nie da virtual przy metodzie w klasie to zabezpiecza przed jej nadpisywaniem (bez virtual metoda nadal będzie wirtualna, ale kompilator automatycznie dopisze sealed). Podobny efekt można uzyskać w Javie dopisując do metody słówko final.

Mała poprawka: w C# istnieje coś takiego jak przesłanianie metod!!!

public class A
{
   public virtual void One();
   public void Two();
}

public class B : A
{
   public override void One();
   public new void Two();
}

Powyższa konstrukcja wygrywa konkurs na największego raka :] Nie jestem w stanie pojąć co autor miał na myśli dodając taką funkcjonalność do języka C#.

PS: Popsutą hierarchię dziedziczenia zawsze można zastąpić popsutą hierarchią kompozycji (delegacji).

0

Chciałbym wiedzieć jak to wszystko pod maską działa co obniża wydajność. Interfejs to jest jakaś funkcjonalność , którą mogą posiadać różne klasy i struktury.
We frameworku masz przykłady interfejsów. A metody wirtualne to są metody które posiadają implementacje i można ją nadpisywać w klasach pochodnych .
Gdzieś kiedyś czytałem ale nie pamiętam już w której książce , że jak się zapieczętuje taką metodę to coś działa szybciej ( override sealed )

0

Interfejs to jest jakaś funkcjonalność , którą mogą posiadają różne klasy i struktury. We frameworku masz przykłady interfejsów. A metody wirtualne to są metody które posiadają implementacje i można ją nadpisywać w klasach pochodnych .

Wszystkie metody w interfejsach są wirtualne i możesz je nadpisywać dowolną ilość razy w hierarchii klas pochodnych. Pokazałem to przecież post wyżej.

Gdzieś kiedyś czytałem ale nie pamiętam już w której książce , że jak się zapieczętuje taką metodę to coś działa szybciej ( override sealed )

To, że coś 10 lat temu wolno działało nie znaczy, że wolno działa dzisiaj. W .NET Core MS w końcu implementuje dewirtualizację, escape analysis i inne optymalizacje:
https://www.infoq.com/news/2017/12/Devirtualization
https://github.com/dotnet/coreclr/issues/9908

W Javie dla przykładu od zawsze gettery i settery są metodami wirtualnymi niefinalnymi (czyil dopuszczającymi nadpisywanie) i odkąd pamiętam nie miało to żadnego wpływu na wydajność. Taka sama szybkość była przy stosowaniu getterów i setterów jak i przy bezpośrednim operowaniu na polach klas. .NET też powoli dochodzi do takiego stanu (w sensie wydajności, nie stylu programowania).

2
Wibowit napisał(a):

Ja jeszcze tylko pokażę, że wyodrębnienie interfejsu na potrzeby testowania (co za dziwny pomysł) wcale nie zabezpiecza przed nadpisywaniem metod, gdyż metody w interfejsach są wirtualne.

O to chodzi, że metody interfejsów są wirtualne, bo dzięki tamu można w ogóle mockować, a tego przecież dotyczy ten wątek.

Mała poprawka: w C# istnieje coś takiego jak przesłanianie metod!!!

Są też wskaźniki, typy ze znakiem i wiele innych równie ekscytujących konstrukcji!!!

Powyższa konstrukcja wygrywa konkurs na największego raka :] Nie jestem w stanie pojąć co autor miał na myśli dodając taką funkcjonalność do języka C#.

To jest możliwość dana przez twórców języka programistom. Ma swoje potencjalne zastosowanie, ale raczej nie korzysta się z tego mechanizmu często.

0

Są też wskaźniki

No wypas. Wracamy do wielodniowych sesyjek debugowania? Z tym mi się kojarzą wskaźniki. Biorąc pod uwagę rozkminy czy szybszy jest foreach czy for na indeksach to nie zdziwiłbym się, gdyby ktoś w biznesowym kodzie C# zaczął używać wskaźników. Każda pomoc VMce na wagę złota.

W C# 8 Microsoft jedzie po bandzie i implementuje metody domyślne w interfejsach, żywcem wzięte z Javy: https://www.infoq.com/articles/default-interface-methods-cs8 Jest jednak pewien smaczek: można zrobić hierarchię dziedziczenia interfejsów z zawartą w nich dowolną ilością nadpisywania metod wirtualnych, a to wszystko to bez użycia słówek virtual czy override. Na 100% nie wiadomo czy DIM wejdą do C#8, ale szansa jest bardzo wysoka (nie ma sygnałów, by MS się miał wycofać).

Domyślne metody w interfejsach (DIM - default interface methods) mają pewne zalety w porównaniu do metod rozszerzających (EM - extension methods), gdy chcemy rozszerzyć funkcjonalność już istniejących interfejsów:

  • DIM nie wymagają importowania czegokolwiek by używać nowych metod na instancjach danego interfejsu,
  • DIM są wirtualne, a więc np konkretne klasy (np moja własna implementacja listy) czy podinterfejsy mogą nadpisywać domyślną implementację czymś zoptymalizowanym,
0
Wibowit napisał(a):

Wszystkie metody w interfejsach są wirtualne i możesz je nadpisywać dowolną ilość razy w hierarchii klas pochodnych. Pokazałem to przecież post wyżej.

A ja chciałbym tylko zauważyć że to że w CIL metody zaimplementowane z interfejsów dostają modyfikatory virtual abstract czy final virtual jest wyłącznie szczegółem implantacyjnym. Z punktu widzenia języka dopóki w naszej klasie/strukturze metody nie dostaną modyfikatora virtual, to zachowują się dokładnie tak samo jak metody nie wirtualne. I też je można dowolną ilość razy przesłaniać za pomocą opcjonalnego modyfikatora "new".

somekind napisał(a):

O to chodzi, że metody interfejsów są wirtualne, bo dzięki tamu można w ogóle mockować, a tego przecież dotyczy ten wątek.

Więc twierdzenie że metody zaimplementowane z interfejsów są wirtualne jest sporym nadużyciem, dopiero jak użyjemy modyfikatora virtual to stają się one wirtualne.
Interfejsy są preferowane przez biblioteki tworzące mocki właśnie dlatego że tworzą dynamicznie typ który implementuje interfejs i żadnej potrzeby używania virtulanych metod nie ma, bo obiekt utworzony (mock) jest pierwszą implementacją w hierarchii dziedziczenia.

Metody wirtualne są/były wolniejsze głównie przez to że nie są inlinowane (używanie v-table do ustalenia z której klasy wywołać metody ma mniejszy narzut), na dodatek odwołanie się przez interfejs do obiektu typu wartościowego powoduje boxing (boxingu można uniknąć wywołując metodę bezpośrednio z typu). Modyfikator sealed w żaden sposób nie jest obecnie brany pod uwagę przez kompilator w kwestii metod virtualnych.
Tak jak zauważył Wibowit, dopiero trwają pracę nad wprowadzeniem dewirtualizacji i ona ma poradzić sobie z powyższymi problemami.

Natomiast jeśli chodzi o metody implantowane z interfejsu, dopóki nie dostaną modyfikatora virtual, to mogę zostać inlinowane, co po raz kolejny pokazuje że blizej im do zwykłych metod niz virtualnych ;)

1
Wibowit napisał(a):

No wypas. Wracamy do wielodniowych sesyjek debugowania? Z tym mi się kojarzą wskaźniki. Biorąc pod uwagę rozkminy czy szybszy jest foreach czy for na indeksach to nie zdziwiłbym się, gdyby ktoś w biznesowym kodzie C# zaczął używać wskaźników. Każda pomoc VMce na wagę złota.

Nie każdy ficzer języka ma zastosowanie w każdym rodzaju aplikacji. Dobry programista wie, które mechanizmy są pomocne w danym problemie, a które nie.

neves napisał(a):

Więc twierdzenie że metody zaimplementowane z interfejsów są wirtualne jest sporym nadużyciem, dopiero jak użyjemy modyfikatora virtual to stają się one wirtualne.
Interfejsy są preferowane przez biblioteki tworzące mocki właśnie dlatego że tworzą dynamicznie typ który implementuje interfejs i żadnej potrzeby używania virtulanych metod nie ma, bo obiekt utworzony (mock) jest pierwszą implementacją w hierarchii dziedziczenia.

Masz rację. Ja po prostu dostosowałem język do dyskusji, bo nie podejmuję się tłumaczenia Javowcowi, że w biegu można utworzyć dynamicznie typ. Jeszcze by we mnie rzucił dzidą czy jakimś innym bumerangiem.

0

@WeiXiao: myślisz, że słabe typowanie też przeniosą z Javy do C#? - somekind 52 minuty temu

Cały czas nie jestem w stanie się domyślić o co chodzi z tym słabym typowaniem.

@neves:
Wytłumacz mi w takim razie jak działa poniższy kod, a konkretnie linjka z komentarzem:

using System;
 
public class Test
{
	interface Interfejs {
		void metodaWirtualna();
	}
 
	class Klasa1 : Interfejs {
		public void metodaWirtualna() {
			Console.WriteLine("Klasa1");
		}
	}
 
	class Klasa2 : Interfejs {
		public virtual void metodaWirtualna() {
			Console.WriteLine("Klasa2");
		}
	}
 
	class Klasa3 : Klasa2 {
		public override void metodaWirtualna() {
			Console.WriteLine("Klasa3");
		}
	}
 
	static void Wypisz(Interfejs obiekt) {
		obiekt.metodaWirtualna(); // wirtualna czy nie wirtualna, oto jest pytanie!
	}
 
	public static void Main()
	{
		Wypisz(new Klasa1());
		Wypisz(new Klasa2());
		Wypisz(new Klasa3());
	}
}

https://www.ideone.com/Pb1IyN
Wynik:

Klasa1
Klasa2
Klasa3

Wrzuciłem ten kod w https://sharplab.io/ i pokazał takie mniej więcej takie coś:

    .method private hidebysig static 
        void Wypisz (
            class Test/Interfejs obiekt
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 7 (0x7)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: callvirt instance void Test/Interfejs::metodaWirtualna()
        IL_0006: ret
    } // end of method Test::Wypisz

Jak byk mamy callvirt.

1

To że w CIL metoda jest virtualna nie oznacza że w definicji C# też jest virtualna, to jest szczegół implementacyjny.
Tu masz dwa dowody, w obu wypadkach są wyniki Klasa1, Klasa1, a to oznacza że to nie są na poziomie języka metody virtualne, nie ma polimorfizmu.
Więc jeszcze raz w CIL są to metody virtualne, ale nie w języku C#.

using System;

public class Test
{
    interface Interfejs
    {
        void metodaWirtualna();
    }

    class Klasa1 :Interfejs
    {
        public void metodaWirtualna()
        {
            Console.WriteLine("Klasa1");
        }
    }

    class Klasa2 :Klasa1, Interfejs
    {
        public  void metodaWirtualna()
        {
            Console.WriteLine("Klasa2");
        }
    }

   

    static void Wypisz(Klasa1 obiekt)
    {
        obiekt.metodaWirtualna(); // wirtualna czy nie wirtualna, oto jest pytanie!
    }

    public static void Main()
    {
        Wypisz(new Klasa1());
        Wypisz(new Klasa2());        
    }
}
using System;

public class Test
{
    interface Interfejs
    {
        void metodaWirtualna();
    }

    class Klasa1 :Interfejs
    {
        public void metodaWirtualna()
        {
            Console.WriteLine("Klasa1");
        }
    }

    class Klasa2 : Klasa1
    {
        public void metodaWirtualna()
        {
            Console.WriteLine("Klasa2");
        }
    }

   

    static void Wypisz(Interfejs obiekt)
    {
        obiekt.metodaWirtualna(); // wirtualna czy nie wirtualna, oto jest pytanie!
    }

    public static void Main()
    {
        Wypisz(new Klasa1());
        Wypisz(new Klasa2());        
    }
}
0

Powiem tak:

  1. Nie odpowiedziałeś na moje pytanie.
  2. Pokazałeś 2 dowody na to, że C# jest takim samym WTFem jak C++. Zmiana typu referencji (nie obiektu na który wskazuje) skutkuje zmianą działania programu? W Javce mogę zamienić typ każdej referencji na typ wyższy w hierarchii (dopóki kod mi się kompiluje) i program zawsze będzie działał tak samo (no chyba, że ktoś robi instancja.metodaStatyczna() ale to samo w sobie jest WTFem).

Chodzi mi o semantykę wywołania funkcji z interfejsu, a nie o semantykę jej implementowania. To dwie różne sprawy. Wywoływanie funkcji z interfejsu to przechodzenie po v-table, natomiast implementowanie funkcji z interfejsu bądź nadpisywanie funkcji to konstruowanie v-table. Pokazałeś, że da się w dziwaczny sposób konstruować v-table, ale nie przeanalizowałeś przechodzenia po v-table.

Albo inaczej mówiąc, już przechodząc do sedna tego co chciałem przekazać: wywołując metodę na referencji typu interfejsu nie masz pewności czy wywołujesz metodę, która bezpośrednio implementuje metodę z interfejsu czy może metodę która nadpisuje inną metodę (która potencjalnie też nadpisuje inną metodę i tak dalej). Wywołując metodę z interfejsu nie masz pojęcia jak długo musisz przechodzić po v-table dopóki nie dostaniesz konkretnego obiektu, a więc działa to dokładnie tak samo jak z metodami wirtualnymi jawnie oznaczonymi jako wirtualne.

to oznacza że to nie są na poziomie języka metody virtualne, nie ma polimorfizmu

Polimorfizm nie wymaga metod wirtualnych. Haskell w ogóle nie ma metod czy funkcji wirtualnych, a pomorfizm to w nim podstawa.

0

Bo Twoje pytanie jest podchwytliwe :), zacznijmy od tego że w C# póki co nie ma metod w interfejsach, są tylko kontrakty które typy implementujący dany interfejs muszą spełnić. Biorąc pod uwagę że nie mamy tych metod w interfejsach tylko w typach to pytanie jest trochę bez sensu. No ale dobrze, z punktu widzenia języka dla Klasy1 to wywołanie nie będzie virtualne, dla Klasy2 i Klasy3 to będzie wywołanie virtualne. Z poziomu ILA wszystkie będą wirtualne, bo tak akurat zostały interfejsy zaimplementowane w CLR.

To nie są żadne WTF, po prostu implementacja interfejsu nie powoduje że te metody z miejsca stają sie implicit virtualne, dlatego dostajemy dla Klasy2 dwa razy Klasa1, Klasa1, zamiast Klasa2, Klasa2 jakie byśmy dostali gdyby interfejsy dodawały implicit modyfikator virtual do implementujących je metod.

0

Tak wygląda metoda interfejsu : .method public hidebysig newslot virtual final - ma atrybut newsolt czyli ukrywa metodę która ma taką samą nazwę i parametry i jest zapieczętowana - nie można już jej przesłonić w klasie pochodnej

Tak wygląda metoda wirtualna w klasie bazowej : .method public hidebysig newslot virtual
a tak wygląda w klasie pochodnej : .method public hidebysig virtual - nie ma atrybutu newslot czyli przesłania a nie ukrywa .

Taką metodę możemy również zapieczętować : .method public hidebysig virtual final , żeby nie mogła być już więcej przesłaniana w kolejnych klasach dziedziczących po niej . (Kolejne klasy będą mogły jedynie odziedziczyć taką metodę albo ją ukryć )

namespace ConsoleApp

{
    public class Program
    {
         public static void Main(string[] args)
        {
            KlasaA ob = new KlasaB(); // niejawna konwersja typu B na typ A ,ponieważ typ B dziedziczy po typie A
            
            ob.Metoda1("Ukrywanie metody");
            ob.Metoda2("Przesłanianie metody");
        }
    }


    class KlasaA
    {
        public virtual void Metoda1(string str)
        {
            Console.WriteLine($"{str} - KlasaA");
        }

        public virtual void Metoda2(string str)
        {
            Console.WriteLine($"{str} - KlasaA");
        }
    }

    class KlasaB : KlasaA
    {
        public new virtual void Metoda1(string str)  // ukrywanie - musi być ta sama nazwa i parametry
        {
                                                                            // np. public new string Metoda1(string str)
            Console.WriteLine("{str} - KlasaB");
        }

        public override void Metoda2(string str) // przesłanianie - musi być ten sam typ zwracany, ta sama nazwa i parametry
        {
            Console.WriteLine($"{str} - KlasaB");
        }
    }
}

A virtual method is introduced in the inheritance hierarchy by defining a virtual method . The definition can be marked newslot to always create a new virtual method for the defining class and any classes derived from it:  If the definition is marked newslot, the definition always creates a new virtual method, even if a base class provides a matching virtual method. A reference to the virtual method via the class containing the method definition, or via a class derived from that class, refers to the new definition (unless hidden by a newslot definition in a derived class). Any reference to the virtual method not via the class containing the method definition, nor via its derived classes, refers to the original definition.  If the definition is not marked newslot, the definition creates a new virtual method only if there is not virtual method of the same name and signature inherited from a base class. It follows that when a virtual method is marked newslot, its introduction will not affect any existing references to matching virtual methods in its base classes

0
Wibowit napisał(a):

Wywoływanie funkcji z interfejsu to przechodzenie po v-table, natomiast implementowanie funkcji z interfejsu bądź nadpisywanie funkcji to konstruowanie v-table. Pokazałeś, że da się w dziwaczny sposób konstruować v-table, ale nie przeanalizowałeś przechodzenia po v-table.

Wywołując metodę z interfejsu nie masz pojęcia jak długo musisz przechodzić po v-table dopóki nie dostaniesz konkretnego obiektu, a więc działa to dokładnie tak samo jak z metodami wirtualnymi jawnie oznaczonymi jako wirtualne.

To nie działa tak samo w C# :), dla wszystkich par interfejs : typ implementujący jest definiowana w AppDomain globalna mapa IVMap. V-table jest używane dopiero wtedy kiedy dodasz modyfikator virtual czyli zrobisz z metody prawdziwą metodę wirtualną.

0

@neves:
Wkleiłem twój kod w https://dotnetfiddle.net/ i zauważyłem, że mnie oszukałeś. Zamiast zaimplementować metody z interfejsu to przesłoniłeś metody!

	class A
	{
		public virtual void m() {}
	}
	
	class B : A
	{
		public void m() {} // tu kompilator krzyczy, żeby dodać explicit "new", bo jak na razie sam sobie dodaje
	}

Abstrahując od tego, postanowiłem sprawdzić wydajność metod wirtualnych z interfejsów i z klas abstrakcyjnych. Oto kod (uwaga: długi i brzydki):

using System;
using System.Collections.Generic;
using System.Diagnostics;
					
public class Program
{	
	interface InterfejsA
	{
		long metodaA(long x);
	}
	
	class KlasaA1 : InterfejsA
	{
		public long metodaA(long x)
		{
			return x * 5 + (x >> 3) + 1;
		}
	}
	
	class KlasaA2 : InterfejsA
	{
		public long metodaA(long x)
		{
			return x * 3 + (x >> 5) + 1;
		}
	}
	
	interface InterfejsB
	{
		long metodaB(long x);
	}
	
	class KlasaB1 : InterfejsB
	{
		public virtual long metodaB(long x)
		{
			return x * 5 + (x >> 3) + 2;
		}
	}
	
	class KlasaB2 : InterfejsB
	{
		public virtual long metodaB(long x)
		{
			return x * 3 + (x >> 5) + 2;
		}
	}
	
	class KlasaB1plus : KlasaB1
	{
		public override long metodaB(long x)
		{
			return x * 5 + (x >> 3) + 3;
		}
	}
	
	class KlasaB2plus : KlasaB2
	{
		public override long metodaB(long x)
		{
			return x * 3 + (x >> 5) + 3;
		}
	}
	
	abstract class KlasaC
	{
		public abstract long metodaC(long x);
	}
	
	class KlasaC1 : KlasaC
	{
		public override long metodaC(long x)
		{
			return x * 5 + (x >> 3) + 4;
		}
	}
	
	class KlasaC2 : KlasaC
	{
		public override long metodaC(long x)
		{
			return x * 3 + (x >> 5) + 4;
		}
	}
	
	class KlasaC1plus : KlasaC1
	{
		public override long metodaC(long x)
		{
			return x * 5 + (x >> 3) + 5;
		}
	}
	
	class KlasaC2plus : KlasaC2
	{
		public override long metodaC(long x)
		{
			return x * 3 + (x >> 5) + 5;
		}
	}
	
	public static void Main()
	{
		var listA = new List<InterfejsA>();
		var listB = new List<InterfejsB>();
		var listBplus = new List<InterfejsB>();
		var listC = new List<KlasaC>();
		var listCplus = new List<KlasaC>();
		for (int i = 0; i < 12345; i++)
		{
			listA.Add(new KlasaA1());
			listB.Add(new KlasaB1());
			listBplus.Add(new KlasaB1plus());
			listC.Add(new KlasaC1());
			listCplus.Add(new KlasaC1plus());
			listA.Add(new KlasaA2());
			listB.Add(new KlasaB2());
			listBplus.Add(new KlasaB2plus());
			listC.Add(new KlasaC2());
			listCplus.Add(new KlasaC2plus());
			listA.Add(new KlasaA1());
			listB.Add(new KlasaB1());
			listBplus.Add(new KlasaB1plus());
			listC.Add(new KlasaC1());
			listCplus.Add(new KlasaC1plus());
		}
		Stopwatch sw;
		long result = -1;
		for (int i = 0; i < 12; i++)
		{
			sw = Stopwatch.StartNew();
			for (int n = 0; n < 123; n++)
			for (int j = 0; j < 12345 * 3; j++)
			{
				result += listA[j].metodaA(result);
			}
			sw.Stop();
			Console.WriteLine("AAA      Time: " + sw.ElapsedMilliseconds + " ms");
			sw = Stopwatch.StartNew();
			for (int n = 0; n < 123; n++)
			for (int j = 0; j < 12345 * 3; j++)
			{
				result += listB[j].metodaB(result);
			}
			sw.Stop();
			Console.WriteLine("BBB      Time: " + sw.ElapsedMilliseconds + " ms");
			sw = Stopwatch.StartNew();
			for (int n = 0; n < 123; n++)
			for (int j = 0; j < 12345 * 3; j++)
			{
				result += listBplus[j].metodaB(result);
			}
			sw.Stop();
			Console.WriteLine("BBB plus Time: " + sw.ElapsedMilliseconds + " ms");
			sw = Stopwatch.StartNew();
			for (int n = 0; n < 123; n++)
			for (int j = 0; j < 12345 * 3; j++)
			{
				result += listC[j].metodaC(result);
			}
			sw.Stop();
			Console.WriteLine("CCC      Time: " + sw.ElapsedMilliseconds + " ms");
			sw = Stopwatch.StartNew();
			for (int n = 0; n < 123; n++)
			for (int j = 0; j < 12345 * 3; j++)
			{
				result += listCplus[j].metodaC(result);
			}
			sw.Stop();
			Console.WriteLine("CCC plus Time: " + sw.ElapsedMilliseconds + " ms");
		}
		Console.WriteLine(result);
	}
}

Wyniki na .NET 4.7.2 odpalone wprost w dotnetfiddle:

AAA      Time: 98 ms
BBB      Time: 98 ms
BBB plus Time: 100 ms
CCC      Time: 85 ms
CCC plus Time: 84 ms
AAA      Time: 98 ms
BBB      Time: 94 ms
BBB plus Time: 91 ms
CCC      Time: 83 ms
CCC plus Time: 97 ms
AAA      Time: 114 ms
BBB      Time: 94 ms
BBB plus Time: 95 ms
CCC      Time: 80 ms
CCC plus Time: 80 ms
AAA      Time: 95 ms
BBB      Time: 94 ms
BBB plus Time: 94 ms
CCC      Time: 81 ms
CCC plus Time: 80 ms
AAA      Time: 93 ms
BBB      Time: 92 ms
BBB plus Time: 93 ms
CCC      Time: 84 ms
CCC plus Time: 80 ms
AAA      Time: 96 ms
BBB      Time: 101 ms
BBB plus Time: 97 ms
CCC      Time: 83 ms
CCC plus Time: 82 ms
AAA      Time: 92 ms
BBB      Time: 93 ms
BBB plus Time: 92 ms
CCC      Time: 82 ms
CCC plus Time: 80 ms
AAA      Time: 91 ms
BBB      Time: 92 ms
BBB plus Time: 95 ms
CCC      Time: 83 ms
CCC plus Time: 78 ms
AAA      Time: 92 ms
BBB      Time: 94 ms
BBB plus Time: 101 ms
CCC      Time: 85 ms
CCC plus Time: 82 ms
AAA      Time: 96 ms
BBB      Time: 95 ms
BBB plus Time: 91 ms
CCC      Time: 81 ms
CCC plus Time: 84 ms
AAA      Time: 92 ms
BBB      Time: 96 ms
BBB plus Time: 102 ms
CCC      Time: 83 ms
CCC plus Time: 84 ms
AAA      Time: 94 ms
BBB      Time: 96 ms
BBB plus Time: 93 ms
CCC      Time: 81 ms
CCC plus Time: 80 ms
-16423156767009094

Wychodzi na to, że:

  • dodanie jawnego słówka kluczowego virtual nie ma wpływu na wydajność
  • nadpisywanie zaimplementowanej metody nie ma wpływu na wydajność
  • wywołanie metody wirtualnej z abstrakcyjnej klasy jest ciut szybsze niż wywołanie metody wirtualnej z interfejsu

Dodatkowo:

  • wywalenie klas "plus" nie ma wpływu na wydajność pozostałych, a więc nie ma znaczenia ile razy metody są nadpisywane

Spróbuję jeszcze odpalić ten kod u siebie na .NET Core 2.1.503 na Ubuntu.
Aktualizacja:
OK, odpaliłem. Czasy rozkładają się tak samo, ale są jakieś 2.5x lepsze (pewnie mam 2.5x szybszego procka).

0

Spróbujcie zapieczętować te metody override dodając sealed i sprawdźcie czasy czy coś się zmieni . Sam jestem ciekawy

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.