Polimorfizm jest jednym z filarów programowania obiektowego z greckiego oznacza wielopostaciowość. Używając tego mechanizmu możemy stworzyć metody o tej samej nazwie ale różnym zachowaniu. Działanie tej metody będzie zależne od typu obiektu dla którego będzie zastosowana. Pozwala ona na wywołanie funkcji klas pochodnych używając zmiennej referencyjnej klasy bazowej wskazującej na obiekt klasy pochodnej.
Polimorfizm można podzielić na dwa typy:

Polimorfizm statyczny

Występuje gdy element polimorficzny wybrany jest już w trakcie kompilacji programu. Dobrym przykładem polimorfizmu statycznego są przeciążone metody, które mają taką samą nazwę, mogą wykonywać różne operacje, a rozpoznawane są po typie i ilości parametrów. Przykład:

using System;
class Program {

    public static double Metoda() {
        return 100;
    }
    public static double Metoda(int a) {
        return 100 + a;
    }

    public static double Metoda(int a, int b) {
        return a + b;
    }
    public static double Metoda(int a, double b) {
        return b-a;
    }
    public static double Metoda(int a, int b, int c) {
        return a + b + c;
    }

    static void Main() {
        Console.WriteLine("Metoda Metoda() zwraca:{0} ", Program.Metoda());
        Console.WriteLine("Metoda Metoda(int a) zwraca:{0} ", Program.Metoda(5));
        Console.WriteLine("Metoda Metoda(int a,int b) zwraca:{0} ", Program.Metoda(5, 9));
        Console.WriteLine("Metoda Metoda(int a, double b) zwraca:{0} ", Program.Metoda(5, 9.8));
        Console.WriteLine("Metoda Metoda(int a,int b,int c) zwraca:{0} ", Program.Metoda(5, 9, 8));
      
        Console.ReadLine();
    }
 
}

Rezultat działania aplikacji:

polimorfizm1

Rys. Wyświetlenie działania przeciążonej metody

Już w trakcie kompilacji wiadome jest, która funkcja zostanie wybrana. Ponieważ wywołujemy ją z konkretną ilością i typami parametrów.

Polimorfizm dynamiczny

Występujący wtedy element polimorficzny, wybierany jest w trakcie działania aplikacji. Przykładem tego mechanizmu są nadpisane funkcje wirtualne. Zobaczmy przykład:
Stwórzmy najpierw klasę bazową: pracownik oraz jej klasy pochodne:

using System;
public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy Pracownik");       
    }
}

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
}

public class PracownikSystemPremiowy : Pracownik {
    public float Premia;
}

public class PracownikSystemAkordowy : Pracownik {
    public float Akord;
    

}
class Program {
   static void Main() {
       PracownikSystemCzasowy pracownik = new PracownikSystemCzasowy(); 
pracownik.WyswietlPodstawoweDane();        
       Console.ReadLine();
    }
}

Rezultat działania aplikacji:

polimorfizm2

Rys. Wywołanie metody

Została wywołana metoda z klasy bazowej. Dopiszmy metody dla każdej z klas pochodnych:

public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy Pracownik");       
    }
}

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemCzasowy");
    }  
}

public class PracownikSystemPremiowy : Pracownik {
    public float Premia;
    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemPremiowy");
    } 
 }

public class PracownikSystemAkordowy : Pracownik {
    public float Akord;
    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemAkordowy");
    } 

}

W ten sposób przesłoniliśmy metodę. Jednak kompilator zgłasza ostrzeżenie.

Rys. Ostrzeżenie kompilatora o przysłonięciu metody.

Jeżeli jesteśmy pewni, że chcemy świadomie przesłonić metodę i ostrzeżenia kompilatora nas denerwują należy użyć słowa kluczowego new po modyfikatorze dostępu danej metody a przed typem zwracanym, zobaczmy:

using System;
public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy Pracownik");       
    }
}

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
    public new void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemCzasowy");
    }  
}

public class PracownikSystemPremiowy : Pracownik {
    public float Premia;
    public new void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemPremiowy");
    } 
 }

public class PracownikSystemAkordowy : Pracownik {
    public float Akord;
    public new void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemAkordowy");
    } 

}
class Program {
    static void Main() {
        PracownikSystemAkordowy pracownik= new PracownikSystemAkordowy();
        pracownik.WyswietlPodstawoweDane();     
        Console.ReadLine();
    }
}

Gdy wywołam przesłoniętą metodę  bezpośrednio z obiektu klasy podrzędnej dostanę jej nową wersję, zobaczmy:

Rezultat działania aplikacji:

polimorfizm3

Rys. Wywołanie metody

Ale co w przypadku gdy użyję referencyjnej zmiennej klasy bazowej, która będzie wskazywać na obiekt klasy pochodnej? Zobaczmy:
Zmieńmy funkcję Main():

  static void Main() {
        Pracownik pracownik= new PracownikSystemAkordowy();
        pracownik.WyswietlPodstawoweDane 
        Console.ReadLine();
    }

Rezultat działania aplikacji:

polimorfizm4

Rys. Wywołanie metody

Została wywołana metoda klasy bazowej, a nie pochodnej.
Możliwość stworzenia zmiennej referencyjnej klasy bazowej wskazującej w pamięci na obiekt klasy potomnej daje duże możliwości w stworzeniu kolekcji, np. tablicy która składa się z kilku rodzajów pracowników, zobaczmy przykład:

Pracownik[] prac = new Pracownik[4];
prac[0]= new Pracownik();
prac[1] = new PracownikSystemCzasowy();
prac[2] = new PracownikSystemAkordowy();
prac[3] = new PracownikSystemPremiowy();

Jednak problem pojawia się gdy chcemy skorzystać z metody kasy pochodnej używając zmiennej referencyjnej klasy bazowej wskazującej na obiekt klasy pochodnej. Aby pozbyć się tego problemu możemy nadpisać metodę klasy pochodnej zamiast ją przesłaniać. Do nadpisywania metod służy słowo kluczowe override. Jednak aby móc je użyć metoda, którą chcemy zastąpić musi być wirtualna, w przypadku gdy nie będzie zobaczymy błąd. Zobaczmy przykład:

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
    public override void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemCzasowy");
    }  
}

Błąd zgłoszony przez kompilator:

polimorfizm5

Rys. Błąd zgłoszony przez kompilator.

Stwórzmy zatem metodę wirtualną w klasie bazowej Pracownik i pozbądźmy się tego błędu. Do tej czynności wykorzystamy słowo kluczowe virtual:

public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public virtual void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy Pracownik");       
    }
}

Użyjmy słowa kluczowego override w naszych klasach pochodnych i zmodyfikujmy metodę Main() tak, aby zawierała pętlę, w której będzie wywoływać się metoda WyswietlPodstawoweDane() dla każdego elementu tablicy. Tablica zawiera zmienne referencyjne typu Pracownik, które wskazują na obiekty klas pochodnych.
Kod:

using System;
public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public virtual void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy Pracownik");       
    }
}

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
    public override void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemCzasowy");
    }  
}

public class PracownikSystemPremiowy : Pracownik {
    public float Premia;
    public override void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemPremiowy");
    } 
 }

public class PracownikSystemAkordowy : Pracownik {
    public float Akord;
    public override void WyswietlPodstawoweDane() {
        Console.WriteLine("Wywołana zostałą metoda klasy PracownikSystemAkordowy");
    } 

}
class Program {
   static void Main() {

        Pracownik[] prac = new Pracownik[4];
        prac[0] = new Pracownik();
        prac[1] = new PracownikSystemCzasowy();
        prac[2] = new PracownikSystemAkordowy();
        prac[3] = new PracownikSystemPremiowy();

        foreach (Pracownik pr in prac) {           
            pr.WyswietlPodstawoweDane();
        }

        Console.ReadLine();
    }
}

Rezultat działania aplikacji:

polimorfizm6

Rys. Wywołanie metod z klas pochodnych.

Udało się nam wywołać interesujące nas metody. Ale co w przypadku gdy będziemy chcieć się odnieść do zmiennej znajdującej się w obiekcie klasy potomnej, używając zmiennej referencyjnej klasy bazowej, która na nią wskazuje, zobaczmy przykład.
Zmodyfikujmy metodę Main():

  static void Main() {

        Pracownik[] prac = new Pracownik[4];
        prac[0] = new Pracownik();
        prac[1] = new PracownikSystemCzasowy();
        prac[2] = new PracownikSystemAkordowy();
        prac[3] = new PracownikSystemPremiowy();
        prac[1].Godziny = 100;

        foreach (Pracownik pr in prac) {           
            pr.WyswietlPodstawoweDane();
        }

        Console.ReadLine();
    }

Kompilator zgłosi nam błąd:

polimorfizm7

Rys. Błąd zgłoszony przez kompilator

Kompilator zgłasza błąd. Możemy rozwiązać ten problem tworząc metodę wirtualną w klasie bazowej, a następnie nadpiszemy tą funkcję w klasach potomnych. Metodę tą stworzymy tak aby dawała nam możliwość przypisania wartości do zmiennych. Zobaczmy przykład:
Napiszmy algorytm, w którym będziemy mogli:
– Podać wartości dla zmiennych w klasach pochodnych od klasy Pracownik
– Obliczyć pensje pracownika w zależności od jego typu
– Wypisać pełne informacje o pracowniku
W programie zastosujemy polimorfizm. Zobaczmy kod:

using System;
public class Pracownik {
    public string Imie;
    public string Nazwisko;
    public float Pensja;

    public virtual void ObliczPensje() {
        Pensja = 2000;
    }
    public virtual void PodajDane() {
        Console.WriteLine("///////Wprowadzenie danych//////////");
        Console.WriteLine("Podaj imię pracownika");
        Imie = Console.ReadLine();
        Console.WriteLine("Podaj nazwisko pracownika");
        Nazwisko = Console.ReadLine();
    }
    public virtual void WyswietlPodstawoweDane() {
        Console.WriteLine("///////Informacje o pracowniku//////////");
        Console.WriteLine("Imię pracownika: " + Imie);
        Console.WriteLine("Nazwisko pracownika: " + Nazwisko);
        Console.WriteLine("Pensja pracownika: " + Pensja);

    }
}

public class PracownikSystemCzasowy : Pracownik {
    public float Godziny;
    public override void PodajDane() {
        base.PodajDane();
        Console.WriteLine("Podaj ilość godzin");
        float.TryParse(Console.ReadLine(), out Godziny);
    }

    public override void ObliczPensje() {
        Pensja = Godziny * 15;
    }
    public override void WyswietlPodstawoweDane() {
        base.WyswietlPodstawoweDane();
        Console.WriteLine("Pracownik system: czasowy");
    }
}

public class PracownikSystemPremiowy : Pracownik {
    public float Premia;

    public override void PodajDane() {
        base.PodajDane();
        Console.WriteLine("Podaj wartość premii:");
        float.TryParse(Console.ReadLine(), out Premia);
    }
    public override void ObliczPensje() {
        Pensja = 2000 + Premia;
    }
    public override void WyswietlPodstawoweDane() {

        base.WyswietlPodstawoweDane();
        Console.WriteLine("Pracownk System: premiowy");
    }
}

public class PracownikSystemAkordowy : Pracownik {
    public float Akord;

    public override void PodajDane() {
        base.PodajDane();
        Console.WriteLine("Podaj wartość akordu:");
        float.TryParse(Console.ReadLine(), out Akord);
    }
    public override void ObliczPensje() {
        Pensja = Akord;
    }
    public override void WyswietlPodstawoweDane() {
        base.WyswietlPodstawoweDane();
        Console.WriteLine("Pracownik System: akordowy");
    }

}
class Program {
    static void Main() {

        Pracownik[] prac = new Pracownik[4];
        prac[0] = new Pracownik();
        prac[1] = new PracownikSystemCzasowy();
        prac[2] = new PracownikSystemAkordowy();
        prac[3] = new PracownikSystemPremiowy();

        foreach (Pracownik pr in prac) {
            pr.PodajDane();
            pr.ObliczPensje();
            pr.WyswietlPodstawoweDane();
        }

        Console.ReadLine();
    }

}

Rezultat działania aplikacji:

polimorfizm8

Rys. Wprowadzenie pierwszego pracownika.

Wprowadziliśmy pierwszego pracownika, który był typu Pracownik.

polimorfizm9

Rys. Wprowadzenie drugiego pracownika.

Wprowadziliśmy drugiego pracownika, zmienna referencyjna jest typu Pracownik, wskazuje ona na obiekt typu PracownikSystemCzasowy. Wywołane zostały metody z klasy pochodnej, dzięki temu wprowadziliśmy interesujące nas dane i wyliczyliśmy odpowiednią kwotę dla pensji.
Analogiczna sytuacja będzie dla następnych iteracji pętli.
W kodzie klienta czyli w metodzie Main() podczas iteracji pętli nie wywoływałem konkretnej wersji metod, robiła to za nas aplikacja. To jest prawdziwa moc polimorfizmu.
Podsumowanie:
Polimorfizm pozwala na wywołanie metod klas pochodnych za pomocą referencyjnej zmiennej klasy bazowej wskazującej na obiekt klasy pochodnej. Możliwe jest to poprzez zastosowanie metod wirtualnych stosując słowo kluczowe virtual w klasie bazowej, a następnie nadpisanie ich w klasach potomnych za pomocą słowa kluczowego override . Przez co zostanie wywołana nadpisana wersja funkcji. Polimorfizm znacząco skraca kod programu. Używając tego mechanizmu aplikacja jest bardziej odporna na zmiany.