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ć.
Co do deklaracji bazowego typu, nadal mówimy o ‘structach’?
Jeśli tak, jak miałoby to wyglądać?
Mały szczegół fizyczny – w żadnej ze standardowych skal (°K, °C, °F) niedopuszczalna jest tak niska wartość temperatury (-2000).
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 😉
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 😉
Co do czepiania się szczegółów: w artykule napisano “jakiś operacji” miast “jakichś operacji” bądź “jakiejś operacji”.
IMO jeżeli mamy do czynienia z obliczeniami fizycznymi warto pomyśleć o włączeniu do solucji programu F#, wtedy z jednostkami przychodzą nam Unit of Measure:
https://msdn.microsoft.com/en-us/library/dd233243.aspx
Mam takie pytanie, dlaczego w konstruktorze Temperature sprawdzamy czy wartość jest poprawna dopiero po jest przypisaniu a nie przed? Ma to jakieś znaczenie?
@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.