Definiowanie własnych typów danych w C# (statyczne typowanie)

Zwykle programiści korzystają z podstawowych typów dostarczonych przez C#, takich jak String, Int32 czy Double. W świecie programowania obiektowego można jednak pójść o krok dalej i budować własne typy danych. Przeważnie programiści korzystają z nich wyłącznie, gdy do zaimplementowania jest jakaś logika. Dlaczego nie tworzyć ich nawet w sytuacjach, gdy mają one przechowywać wyłącznie dane?

Problem z podstawowymi typami takimi jak String Czy Int32 to fakt, że stanowią one wyłącznie fizyczną, a nie logiczną reprezentację danych. Int32 opisuje każdą liczbę całkowitą, co jest dość szerokim określeniem. Załóżmy, że w kodzie mamy dwie jednostki do opisania:
– prędkość (km/h)
– temperatura (C)

Standardowe rozwiązanie to użycie typów podstawowych, np. Int32:

int velocity = 85;
int temperature = 20;

Niestety powyższy kod nie będzie mógł korzystać z zalet statycznego typowania. Poniższy kod skompiluje się i o błędzie dowiemy się dopiero przy wykonywaniu jakiś operacji:

int velocity = 85;
int temperature = 20;

int result = velocity * temperature;

Prawdopodobnie nie ma sensu mnożyć prędkości przez temperaturę. Analogicznie typy podstawowe nie dostarczają żadnej walidacji logicznej reprezentacji. Co prawda w przypadku liczb, możemy zadeklarować velocity jako uint co ma większy sens ponieważ unikniemy wtedy błędów z związanych z ujemną prędkością. Problem jednak pojawi się w przypadku string:

string email="test@test.com";

W tym przypadku zdecydowanie potrzebna jest walidacja.

Zdefiniujmy zatem typy opisujące logiczną reprezentację danych:

    struct Velocity
    {
        private readonly uint _value;

        public Velocity(uint value)
        {
            _value = value;
        }
    }

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {
            _value = value;
        }
    }

Od tej pory, niemożliwe jest popełnienie następującego błędu:

var velocity = new Velocity(43);
var temperature = new Temperature(5);

var result = velocity + temperature;

Kod zakończy się błędem kompilacji: “Operator ‘+’ cannot be applied to operands of type ‘Velocity’ and ‘Temperature'”. Błąd wykryty na etapie kompilacji jest oczywiście dużo bardziej pożądany niż te spotykane już w trakcie działania aplikacji.

Oprócz korzyści z związanych z bezpiecznym typowaniem, kod jest łatwiejszy w zrozumieniu. Typ nadaje kontekst danej wartości.
Dodanie walidacji sprowadza się teraz wyłącznie do:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }
    }

Oczywiście to dopiero początek. W praktyce chcemy mieć do dyspozycji pewne operatory, np.:

            var temperature1 = new Temperature(42);
            var temperature2 = new Temperature(42);
            var temperature3 = new Temperature(41);

            Console.WriteLine(temperature1 == temperature2); // true
            Console.WriteLine(temperature1 == temperature3); // false

Jeśli chcemy wspierać ==, wtedy wystarczy napisać:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }

        public static bool operator ==(Temperature t1, Temperature t2)
        {
            return t1.Equals(t2);
        }

        public static bool operator !=(Temperature t1, Temperature t2)
        {
            return !t1.Equals(t2);
        }
    }

W praktyce warto zadeklarować bazowy typ, który implementuje najczęściej używane operacje (==,!=,>,<,+,-). Nie można również przesadać w drugą stronę i każdą zmienną owijać w typ. Moim zdaniem bardzo szybko taki kod stanie się przykładem overengineering. Jeśli jakiś typ danych występuje bardzo często w kodzie (np. money), wtedy warto o tym pomyśleć.

8 thoughts on “Definiowanie własnych typów danych w C# (statyczne typowanie)”

  1. Co do deklaracji bazowego typu, nadal mówimy o ‘structach’?
    Jeśli tak, jak miałoby to wyglądać?

  2. Mały szczegół fizyczny – w żadnej ze standardowych skal (°K, °C, °F) niedopuszczalna jest tak niska wartość temperatury (-2000).

  3. Jak już czepiamy się szczegółów fizycznych, to nie ma czegoś takiego jak °K. Ta jednostka nie wyraża się w stopniach, tylko po prostu K 😉

  4. Jak już czepiamy się szczegółów fizycznych, to nie ma czegoś takiego jak °K. Ta jednostka nie jest wyrażana w stopniach, tylko po prostu K 😉

  5. Co do czepiania się szczegółów: w artykule napisano “jakiś operacji” miast “jakichś operacji” bądź “jakiejś operacji”.

  6. Mam takie pytanie, dlaczego w konstruktorze Temperature sprawdzamy czy wartość jest poprawna dopiero po jest przypisaniu a nie przed? Ma to jakieś znaczenie?

  7. @Pawel:
    Chyba bardziej elegancko byloby sprawdzac przed, a nie po. W praktyce jednak nie ma to znaczenia bo wyjatek zostanie wyrzycony i obiekt nie zostanie przypisany potem.

Leave a Reply

Your email address will not be published.