Słowo polimorfizm ma wiele znaczeń. W programowaniu obiektowym jest to jeden z paradygmantów, który najczęściej jest wyrażany jako „jeden interfejs, wiele funkcji”.

Polimorfizm może być statyczy i dynamiczny. W polimorfizmie statycznym odpowiedź funkcji jest określna w trakcie kompilowania. W polimorfizmie dynamicznym odpowiedź ta jest podejmowana w czasie wykonywania programu.

Polimorfizm statyczny

Mechanim łączenia metody z obiektem w trakcie kompilacji jest nazywany wczesnym wiązaniem lub też statycznym wiązaniem. C# udostępnia dwa sposoby implementowania statycznego polimorfizmu:
  • przeciążanie metod;
  • przeciązanie operatorów.

Przeciążanie metod

W tej samej definicji klasy może znajdować się wiele funkcji o tej samej nazwie. Definicja metod musi się różnić od siebie typem i/lub liczbą parametrów. Nie można przeciążyć metod, które różnią się tylko zwracanym typem.
Przykład:
using System;
namespace PolimorfizmPrzeciazanieMetod
{
    class Program
    {
        static void Main(string[] args)
        {
            WyswietlanieDanych wd = new WyswietlanieDanych();
            wd.Wyswietl(4);
            wd.Wyswietl(4.5);
            wd.Wyswietl("4.5");
            Console.ReadKey();
            // Wynik działania programu
            //Wyswietlana liczba: 4
            //Wyswietlana liczna: 4.5
            //Wyswietlany tekst: 4.5
        }
    }
    class WyswietlanieDanych
    {
        public void Wyswietl(int i)
        {
            Console.WriteLine("Wyswietlana liczba: {0}", i);
        }
        public void Wyswietl(double d)
        {
            Console.WriteLine("Wyswietlana liczna: {0}", d);
        }
        public void Wyswietl(string s)
        {
            Console.WriteLine("Wyswietlany tekst: {0}", s);
        }
    }
}

Przeciążanie operatorów

C# pozwala na zmianę lub przeciążenie większości wbudowanych operatorów. Programista może używać operatorów z typami zdefiniowanymi również przez użytkownika. Przeciążone operatory to metody z nazwą, słowem kluczowym operator, po którym występuje symbol operatora, który chcemy zdefiniować. Przeciążony operator ma typ zwracany oraz listę parametrów.

Przejdziemy od razu do obszernego przykładu gdyż tak łatwiej będzie zrozumieć jak w praktyce wygląda przeciązanie operatorów.
using System;
namespace PolimorfizmPrzeciazanieOperatorow
{
    class Program
    {
        static void Main(string[] args)
        {
            double objetosc = 0;
            Pudelko p1 = new Pudelko();
            Pudelko p2 = new Pudelko();
            Pudelko p3 = new Pudelko();
            // specyfikacja 1
            p1.PobierzDlugosc(3.5);
            p1.PobierzSzerokosc(4.0);
            p1.PobierzWysokosc(5.5);
            // specyfikacja 2
            p2.PobierzDlugosc(2.5);
            p2.PobierzSzerokosc(5.0);
            p2.PobierzWysokosc(4.5);
            // specyfikacja 3
            p3.PobierzDlugosc(12.5);
            p3.PobierzSzerokosc(15.0);
            p3.PobierzWysokosc(14.5);
            // objetosc 1
            objetosc= p1.ObliczObjetosc();
            Console.WriteLine("Objetosc 1: {0}", objetosc);
            // objetosc 2
            objetosc = p2.ObliczObjetosc();
            Console.WriteLine("Objetosc 2: {0}", objetosc);
            // Dodanie 2 obiektów
            p3 = p1 + p2;
            // objetosc 3
            objetosc = p3.ObliczObjetosc();
            Console.WriteLine("Objetosc 3: {0}", objetosc);
            Console.ReadKey();
            // Wynik działania programu
            //Objetosc 1: 77
            //Objetosc 2: 56.25
            //Objetosc 3: 540
        }
    }
    class Pudelko
    {
        private double dlugosc;
        private double szerokosc;
        private double wysokosc;
        public void PobierzDlugosc(double d)
        {
            dlugosc = d;
        }
        public void PobierzSzerokosc(double s)
        {
            szerokosc = s;
        }
        public void PobierzWysokosc(double w)
        {
            wysokosc = w;
        }
        public double ObliczObjetosc()
        {
            return (dlugosc * szerokosc * wysokosc);
        }
        // Przeciążenie operatora +
        // Dodanie do siebie dwóch typów
        public static Pudelko operator+(Pudelko a, Pudelko b)
        {
            Pudelko pud = new Pudelko();
            pud.wysokosc = a.wysokosc + b.wysokosc;
            pud.szerokosc = a.szerokosc + b.szerokosc;
            pud.dlugosc = a.dlugosc + b.dlugosc;
            return pud;
        }
    }
}

Operatory przeciążalne i nieprzeciążalne

Poniżej lista opisująca możliwości przeciążania operatorów:
Operator Opis
+, -, !, ~, ++, -- operatory jednoargumentowe mogą zostać przeciążone
+, -, *, /, % operatory binarne mogą zostać przeciążone
==, !=, <,>, <=, >= operatory porównania mogą zostać przeciążone
&&, || operatory operacji logicznych nie mogą być przeciążone bezpośrednio
+=, -=, *=, /=, %= operatory przypisania nie mogą być przeciążone
=, ., ?:, ->, new, is, sizeof, typeof te operatory nie mogą być przeciążone
Przykład przeciążania różnych operatorów:
using System;
namespace PolimorfizmPrzeciazanieOperatorow
{
    class Program
    {
        static void Main(string[] args)
        {
            double objetosc = 0;
            Pudelko p1 = new Pudelko();
            Pudelko p2 = new Pudelko();
            Pudelko p3 = new Pudelko();
            // specyfikacja 1
            p1.PobierzDlugosc(3.5);
            p1.PobierzSzerokosc(4.0);
            p1.PobierzWysokosc(5.5);
            // specyfikacja 2
            p2.PobierzDlugosc(2.5);
            p2.PobierzSzerokosc(5.0);
            p2.PobierzWysokosc(4.5);
            // specyfikacja 3
            p3.PobierzDlugosc(12.5);
            p3.PobierzSzerokosc(15.0);
            p3.PobierzWysokosc(14.5);
            // Wyswietlenie danych wewnatrz kolejnych obiektow
            Console.WriteLine("Pudelko 1: {0}", p1.ToString());
            Console.WriteLine("Pudelko 2: {0}", p2.ToString());
            Console.WriteLine("Pudelko 3: {0}", p3.ToString());
            // objetosc 1
            objetosc = p1.ObliczObjetosc();
            Console.WriteLine("Objetosc 1: {0}", objetosc);
            // objetosc 2
            objetosc = p2.ObliczObjetosc();
            Console.WriteLine("Objetosc 2: {0}", objetosc);
            // Dodanie 2 obiektów
            p3 = p1 + p2;
            // objetosc 3
            objetosc = p3.ObliczObjetosc();
            Console.WriteLine("Objetosc 3: {0}", objetosc);
            // porównanie obiektów
            if (p1 == p2)
                Console.WriteLine("Pudełka p1 oraz p2 są identyczne");
            if (p1 != p2)
                Console.WriteLine("Pudełka p1 oraz p2 są różne");
            Console.ReadKey();
            // Wynik działania programu
            //Pudelko 1: (3.5, 4, 5.5)
            //Pudelko 2: (2.5, 5, 4.5)
            //Pudelko 3: (12.5, 15, 14.5)
            //Objetosc 1: 77
            //Objetosc 2: 56.25
            //Objetosc 3: 540
            //Pudelka p1 oraz p2 sa rózne
        }
    }
    class Pudelko
    {
        private double dlugosc;
        private double szerokosc;
        private double wysokosc;
        public void PobierzDlugosc(double d)
        {
            dlugosc = d;
        }
        public void PobierzSzerokosc(double s)
        {
            szerokosc = s;
        }
        public void PobierzWysokosc(double w)
        {
            wysokosc = w;
        }
        public double ObliczObjetosc()
        {
            return (dlugosc * szerokosc * wysokosc);
        }
        // Przeciążenie operatora +
        // Dodanie do siebie dwóch typów
        public static Pudelko operator +(Pudelko a, Pudelko b)
        {
            Pudelko pud = new Pudelko();
            pud.wysokosc = a.wysokosc + b.wysokosc;
            pud.szerokosc = a.szerokosc + b.szerokosc;
            pud.dlugosc = a.dlugosc + b.dlugosc;
            return pud;
        }
        // Przeciążenie operatora ==
        public static bool operator ==(Pudelko a, Pudelko b)
        {
            bool status = false;
            if (a.dlugosc == b.dlugosc && a.szerokosc == b.szerokosc && a.wysokosc == b.wysokosc)
                status = true;
            return status;
        }
        // Przeciążenie operatora !=
        public static bool operator !=(Pudelko a, Pudelko b)
        {
            bool status = false;
            if (a.dlugosc != b.dlugosc || a.szerokosc != b.szerokosc || a.wysokosc != b.wysokosc)
                status = true;
            return status;
        }
        public override string ToString()
        {
            return String.Format("({0}, {1}, {2})", dlugosc, szerokosc, wysokosc);
        }
    }
}

Polimorfizm dynamiczny

C# pozwala tworzyć klasy abstrakcyjne, które następnie są implementowane w klasach pochodnych. Klasa taka zawiera abstrakcyjne metody, których implementacja zależy od wykorzystania w poszczególnych klasach pochodnych.

Poniżej lista zasad o których należy pamiętać tworząć klasy abstrakcyjne:
  • nie można utworzyć instancji klasy abstrakcyjnej;
  • nie można zadeklarować metody abstrakcyjnej poza klasą abstrakcyjną;
  • kiedy klasa opatrzona jest modyfikatorem dostępu sealed nie może być dziedziczona. Dodatkowo, klasa abstrakcyjna nie może być zdefinowana jakas sealed.
Przykład:
using System;
namespace PolimorfizmKlasaAbstrakcyjna
{
    class Program
    {
        static void Main(string[] args)
        {
            Kwadrat kw = new Kwadrat(4,5);
            double pow = kw.Powierzchnia();
            Console.WriteLine("Powierzchnia figury: {0}", pow);
            Console.ReadKey();
            // Wynik działania programu
            //Powierzchnia figury: 20
        }
    }
    abstract class Ksztalt
    {
        public abstract int Powierzchnia();
    }
    class Kwadrat : Ksztalt
    {
        // klasa pochodna musi implementować metody klasy bazowej
        private int wysokosc;
        private int szerokosc;
        public Kwadrat(int a, int b)
        {
            wysokosc = a;
            szerokosc = b;
        }
        public override int Powierzchnia()
        {
            return (wysokosc * szerokosc);
        }
    }
}
Jeżeli masz zdefiniowaną metodę w klasie bazowej, ale chcesz, żeby została zaimplementowana w klasach pochodnych możesz do tego celu zastosować metody virtualne. Metody te mogą mieć różne implementacje w klasach pochodnych, ale nie muszą. Jeżeli w klasie pochodnej nie będzie implementacji metody wirtualnej z klasy bazowej to użyta zostanie domyślna implementacja z klasy bazowej. Wybór odnośnie wywołania metody podejmowany jest w czasie wykonywania programu.

Polimorfizm dynamiczny jest realizowany za pomocą klas abstrakcyjnych oraz metod wirtualnych.

Przykład:
using System;
namespace PolimofrizmMetodyWirtualne
{
    class Program
    {
        static void Main(string[] args)
        {
            WywolanieKlass wk = new WywolanieKlass();
            Prostokat pr = new Prostokat(4, 5);
            Trojkat tr = new Trojkat(4, 5);
            BezImplementacji bi = new BezImplementacji(4, 5);
            wk.WywolajKlase(pr);
            wk.WywolajKlase(tr);
            wk.WywolajKlase(bi);
            // Zamiast powyższego wywołnia można byłoby użyć poniższego zapisu:
            //pr.Powierzchnia();
            //tr.Powierzchnia();
            //bi.Powierzchnia();
            // chciałem jednak pokazać możliwości, jakie niesie za sobą dziedziczenie
            Console.ReadKey();
            // Wynik działania programu
            //Powierzchnia kwadratu:
            //Powierzchnia: 20
            //Powierzchnia trójkata:
            //Powierzchnia: 10
            //Domyslna powierzchnia figury:
            //Powierzchnia: 0
        }
    }
    class Ksztalt
    {
        protected int wysokosc, szerokosc;
        public Ksztalt(int a, int b)
        {
            wysokosc = a;
            szerokosc = b;
        }
        public virtual int Powierzchnia()
        {
            Console.WriteLine("Domyślna powierzchnia figury: ");
            return 0;
        }
    }
    class Prostokat : Ksztalt
    {
        public Prostokat(int a, int b) : base(a, b)
        {
        }
        public override int Powierzchnia()
        {
            Console.WriteLine("Powierzchnia kwadratu: ");
            return (wysokosc * szerokosc);
        }
    }
    class Trojkat : Ksztalt
    {
        public Trojkat(int a, int b) : base(a, b)
        {
        }
        public override int Powierzchnia()
        {
            Console.WriteLine("Powierzchnia trójkąta: ");
            return (wysokosc * szerokosc) / 2;
        }
    }
    class BezImplementacji : Ksztalt
    {
        public BezImplementacji(int a, int b) : base(a, b)
        {
        }
    }
    // Co za konstrukcja?
    // Każda z klas pochodnych w programie dziedziczy po klasie bazowej
    // Posiada jedynie inne implementacje metody Powierzchnia()
    // W poniżej konstrukcji tworzymy klasę, która jako parametr przyjmuje klasę Ksztalt
    // Klasa Ksztalt - nasza klasa bazowa, która następnie wywołuje metodę Powierzchnia()
    // dla typu danych jaki został przekazany
    class WywolanieKlass
    {
        public void WywolajKlase(Ksztalt k)
        {
            int a;
            a = k.Powierzchnia();
            Console.WriteLine("Powierzchnia: {0}", a);
        }
    }
}