Posts tagged ‘Java’

Obiekt na chwilę

2010-12-02 21:58

Swego rodzaju miejską legendą pewnego forum nt. programowania jest “algorytm” znany pod intrygującą nazwą sortowania przez ListBox. Adept kodowania postawiony przed problemem posortowania listy obiektów postanowił mianowicie, że akceptowalnym rozwiązaniem dla niego jest następująca sekwencja czynności:

  1. Stworzenie obiektu kontrolki typu listbox, czyli typowej przewijalnej listy elementów, która byłaby niewidoczna, niepodłączona pod hierarchię kontrolek w interfejsie lub po prostu miała zerowe rozmiary.
  2. Wypełnienie jej zawartości na napisy odpowiadające elementom do posortowania.
  3. Ustawienie właściwości Sorted tej kontrolki na true, dzięki czemu jej elementy (wcześniej dodane) będą zawsze posortowane.
  4. Pobranie zawartości kontrolki, którą są posortowane elementy.

Genialne, nieprawdaż? :) Zauważmy, że to kreatywne rozwiązanie trudnego problemu wymagało zaledwie jednego dużego i skomplikowanego obiektu, który zasadniczo jest też z zupełnie innej bajki (bo z warstwy GUI) niż kod logiki programu, który to zapewne potrzebował rozwiązania. No ale w końcu programowanie premiuje myślenie wybiegające poza schematy, więc nie ma chyba żadnego powodu, żeby powyższy sposób uznać na niewłaściwy, czyż nie? ;-)

Żarty żartami, ale… No właśnie, w rzeczywistości zastosowane tu “podejście” nie jest wcale tak obce niektórym faktycznym rozwiązaniom rzeczywistych problemów programistycznych. Na wiele pytań odpowiedzią jest bowiem często utworzenie obiektu “tylko na chwilę” – zazwyczaj jedynie dla wywołania jednej jego metody.
Weźmy na przykład pobranie aktualnego zrzutu stosu (stacktrace) w Javie. Prawdopodobnie najmniej kłopotliwą metodą jest utworzenie obiektu wyjątkopodobnego (Throwable) i i wywołanie jego metody getStackTrace:

  1. StackTraceElement[] stacktrace = new Throwable().getStackTrace();

Jest to na tyle nietypowe, że pewnie sporo osób odruchowo otoczyłoby instrukcję konstruującą obiekt nawiasami, chociaż nie jest to składniowo wymagane.
Inny przykład? Dokonanie HTML-owego escape‘owania tekstu w JavaScripcie wspomaganym biblioteką jQuery. W tym celu tworzy się nigdzie nieprzyłączony węzeł DOM, ustawia jego zawartość tekstową i natychmiast ją pobiera:

  1. function htmlEscape(str) {
  2.     return $("<div/>").text(str).html();
  3. ]

I wreszcie rzecz ze znanego i lubianego podwórka C++: konwersja liczb do i z formatu binarnego przy pomocy chwilowych obiektów typu bitset:

  1. const size_t BITS = 8 * sizeof(unsigned long);
  2. unsigned long bin2long(const string& str) { return bitset<BITS>(str).to_ulong(); }
  3. string long2bin(unsigned long n)
  4.     { return bitset<BITS>(n).to_string<char, char_traits<char>, allocator<char> >(); }

Podobnych przykładów dałoby się znaleźć oczywiście znacznie więcej. Fakt ten nie jest zupełnie obojętny dla programistów, jako że przynajmniej w niektórych językach utworzenie obiektu może być operacją dość kosztowną. Nie dotyczy to akurat C++, gdzie możliwe jest alokowanie dowolnych obiektów na stosie, lecz raczej tych platform, gdzie obiekty tworzy się na stercie. Często mają też one stały, dodatkowy narzut na sam fakt swego istnienia, spowodowany odziedziczeniem funkcjonalności po klasie “najbardziej bazowej” (zwanej zwykle Object) – narzut zarówno czasowy, jak i pamięciowy. Dlatego też wielokrotne tworzenie małych i krótko żyjących obiektów może być poważnym uszczerbkiem wydajnościowym. Można jednak liczyć na to, że z biegiem czasu sytuacja pod tym względem będzie się poprawiać – tak jak chociażby w przypadku Javy i wprowadzonych optymalizacji w obsłudze obiektów typu String.

Tags: , , , ,
Author: Xion, posted under Programming » 1 comment

Małe kruczki

2010-11-22 8:31

Przeskakując między wieloma językami programowania, bibliotekami czy frameworkami po pewnym czasie można wyrobić sobie zdolności “uogólniające”, dzięki którym takie zmiany nie są specjalnie kłopotliwe. Jednak nawet jeśli potrafimy płynnie przechodzić od jednego języka do drugiego, to zawsze istnieje szansa, że zapomnimy przy okazji o jakimś szczególe, drobnej rzeczy specyficznej dla konkretnego narzędzia – tytułowym małym kruczku.
A przynajmniej mi się tak nierzadko zdarza. Dlatego tę notkę dedykuję pojedynczym, drobnym a “dziwnym” cechom poszczególnych języków programowania, o których często zdarza mi się zapominać. A są one następujące:

  • W C++ nie dodaje się stałych tekstowych. W dziesiątkach języków programowania (i nie tylko programowania) zapis w stylu "ala" + " ma " + "kota" jest najzupełniej oczywistą konkatenacją trzech napisów. Ale w C++ nie. Ponieważ stałe dosłowne, takie jak teksty w cudzysłowach, są – w przybliżeniu – typu const char*, ich łączenie operatorem + jest interpretowane jako dodawanie dwóch wskaźników. Serio.
    Wniosek: W C++ w takich sytuacjach plus jest zbędny i należy go pominąć "ala" " ma " "kota", o ile oczywiście nie łączymy obiektów klasy string.
  • W Javie łańcuchy znaków porównujemy… inaczej. Otóż Java jest bardzo obiektowa i w związku z tym nawet dla typu java.lang.String nie robi wyjątku i obsługuje go tak, jak każdą inną klasę. A to oznacza, że zwykły operator == działa dla napisów tak samo, jak dla innych obiektów (inaczej niż choćby w C#), tj. porównuje ich referencje w celu sprawdzenia, czy mamy do czynienia z tym samym obiektem. Tym samym, nie zaś takim samym, czyli równym co do wartości – a o to nam zwykle chodzi, gdy pracujemy z napisami.
    Wniosek: W Javie do porównywania napisów korzystamy z metody equals. To pewnie dlatego wciąż nie ma w niej switcha dla napisów ;-)
  • Słowniki w Pythonie domyślnie “iterują się” po kluczach. Żeby oszczędzić nadmiernie rozbudowanych wyjaśnień, rzucę poniższym kawałkiem kodu:
    1. for k, v in dictionary: print k, " => ", v

    który na pierwszy rzut oka wygląda na zupełnie poprawną iterację po zawartości słownika dictionary, w której zmienna k dostaje klucz, a v wartość. Jest on dowodem, że oczami należy rzucać zawsze przynajmniej dwa razy. W istocie bowiem k i v będą kolejno otrzymywały, uwaga, po dwa elementy klucza (potraktowanego jako para). Po prostu konwersja słownik → coś_po_czym_można_iterować daje w Pythonie kolekcję kluczy, a nie par klucz-wartość. Nie pytajcie, dlaczego.
    Wniosek: Albo iterujemy po kluczach i wartość uzyskujemy zwykłym indeksowaniem, albo korzystamy z metody items w celu uzyskania kolekcji par klucz-wartość.

  • W Javascripcie zmienne – nawet te użyte po raz pierwszy w funkcjach – są domyślnie globalne. Nawet nie śmiem dociekać, co jest uzasadnieniem dla tego faktu…
    Wniosek: Pierwsze przypisanie do nowej zmiennej wewnątrz funkcji należy opatrywać słowem kluczowym var.
Tags: , , , ,
Author: Xion, posted under Programming » 7 comments

Odpalanie klasy

2010-08-31 12:09

Niskopoziomowe, wewnętrzne klasy logiki mają tę wadę (jeśli można to tak nazwać…), iż są właśnie niskopoziomowe i wewnętrzne – a przez to trudne do testowania w powiązaniu z całą aplikacją. Między nimi a interfejsem może znajdować się wiele warstw, które utrudniają debugowanie.
To sprawia, że przydatne stają się testy jednostkowe (unit tests). Do ich tworzenia i uruchamiania potrzeba jednak odpowiednich frameworków. Na szczęście te są już dostępne dla niemal każdego sensownego języka programowania i nierzadko są wręcz częścią jego lub jego środowiska.
Nie zawsze jednak tak było. Alternatywą dla ręcznego tworzenia specjalnych aplikacji przeznaczonych wyłącznie do testowania klas były (i właściwie nadal są) interesujące mechanizmy “uruchamiania klas” jako takich, które oferują niektóre języki programowania.

Dotyczy to przede wszystkim – jeśli nie wyłącznie – tych spośród nich, w których koncepcja programu czy aplikacji nie jest wyraźnie zakreślona. Coś jakiego jak entry point (“punkt wejścia”), od którego zaczyna się wykonywanie kodu, musi jednak istnieć i trzeba mieć jakiś sposób na jego określenie. Jeżeli zamiast aplikacji mamy tylko mniej lub bardziej luźny zbiór klas, to siłą rzeczy musi się on znajdować w którejś z nich.
A jeśli znajduje się w jednej, to czemu nie dodać go też do innych – także tych, które z założenia nie mają pojęcia o interakcji z użytkownikiem? Tworzymy w ten sposób pewnego rodzaju back-end, a owe dodatkowe punkty wejścia mogą posłużyć do testów. Oto prosty przykład w języku Java:
public class Sumator
{
private int sum = 0;
public void add(int x) { sum += x; }
public int getSum() { return sum; }

// testowy punkt wejścia
public static void main(String[] args) {
int n = args.length > 1 ? Integer.parseInt(args[1]) : 100;

Sumator s = new Sumator();
for (int i = 1; i <= n; ++i) s.add(i); System.out.println ("Spodziewana suma: " + (n * (n + 1) / 2)); System.out.println ("Otrzymana suma: " + s.getSum()); } }[/java] Statyczna metoda main to w Javie sposób na określenie punktu wejścia. Typowym miejscem dla niego jest główna klasa aplikacji, względnie całkiem osobna klasa przeznaczona do zarządzania samym uruchamianiem programu. Tutaj umieszczamy metodę main w klasie logiki (bardzo zaawansowanej zresztą ;-]), przez co możemy “odpalić” samą tę klasę i wykonać jakiś kod testujący jej funkcjonalność.

Z popularnych języków mających taki feature można jeszcze wspomnieć Pythona. W nim obiektowość nie jest obowiązkowa, więc to skrypt (plik .py) jest podstawową jednostką kodu, którą można uruchamiać:

  1. import sys
  2.  
  3. class Sumator:
  4.     def __init__(self): self.sum = 0
  5.     def add(self, x): self.sum += x
  6.  
  7. if __name__ == "__main__":
  8.     n = int(sys.argv[1]) if len(sys.argv) > 1 else 100
  9.     s = Sumator()
  10.     for i in range(1, n+1): s.add(i)
  11.     print "Spodziewana suma: ", (n * (n + 1) / 2)
  12.     print "Otrzymana suma: ", s.sum

Daje się w nim też umieścić “swobodny” kod, co na pierwszy rzut oka wydaje się dobrym miejscem na instrukcje testowe. Trzeba tylko otoczyć je pokazanym wyżej ifem, aby były one uruchamiane wyłącznie przy wywoływaniu skryptu z wiersza poleceń, nie zaś przy imporcie zawartego w nim kodu (instrukcją import).

Tags: , , , ,
Author: Xion, posted under Programming » Comments Off on Odpalanie klasy

Niestatyczne klasy wewnętrzne w praktyce

2010-08-16 20:35

Postawiłbym tezę, że każdy szerzej używany język programowania ma ok. jedną wyróżniającą cechę, definiującą w znacznym stopniu sposób, w jaki się go używa. Innymi słowy, każdy język z czymś się kojarzy. I tak myśląc o C, przypominamy sobie od razu wskaźniki; w Pythonie przychodzi nam na myśl składnia oparta na wcięciach; Pascal kojarzy nam się natychmiast z begin/end; w LISP-ie mamy miliardy nawiasów; w C++ przeciążanie operatorów; w PHP kod przeplatany wstawkami HTML – i tak dalej. Chyba tylko C# jest – przynajmniej dla mnie – swego rodzaju wyjątkiem w tej kwestii (co niekoniecznie musi być złe, bo oznacza również brak wyraźnie irytujących feature‘ów).

Dzisiaj jednak chciałem napisać o Javie z tego względu, że przyszło mi ostatnio kodować nieco w tym języku. Wrażenie, jakie w związku z tym odnoszę, jest takie, iż Java to obecnie chyba najbardziej “klasycznie obiektowy” język ze wszystkich, które mają w branży jakieś znaczenie. Tu nie ma żadnych udziwnień, które łamałyby paradygmat OOP-u, na który składają się m.in. obiekty dostępne przez referencje, komunikacja za pomocą interfejsów i metod, wyraźny podział na proste typy wbudowane i złożone klasy, jawne tworzenie kopii obiektów, i tak dalej. Z jakichś powodów “wynalazki” w rodzaju typów generycznych czy wyrażeń lambda przebijają się do Javy bardzo, bardzo powoli.
Do tej listy trzeba też dopisać jakąkolwiek formę wskaźników na funkcje, przydatną w implementacji callbacków, czy też delegatów przeznaczonych do obsługi zdarzeń. Zamiast tego javowe API muszą uciekać się do (znów wybitnie OOP-owego) implementowania ustalonych interfejsów i polimorficznych wywołań metod. Do tego jednak język posiada unikalny feature, który – przynajmniej w założeniu – ma ten proces wydatnie ułatwiać. To właśnie tytułowe niestatyczne klasy wewnętrzne (inner classes).

Idea jest prosta. Jeśli klasę B umieścimy wewnątrz klasy A, to zabieg ten w Javie będzie miał skutek nie tylko dla widoczności tej pierwszej. Klasa B będzie wtedy klasą wewnętrzną A także w tym sensie, że każdy jej obiekt będzie zawsze związany z jakimś obiektem klasy A. Tą związanie odbywa się automatycznie i objawia dostępem do składników klasy “otaczającej”:

  1. public class Foo {
  2.     class Bar {
  3.         public void Oyey() { Boo{}; }
  4.     }
  5.     private Bar bar = new Bar();
  6.     public void Boo() { }
  7. }

To w sumie nie jest aż tak imponujące. Wprawnym okiem można łatwo zauważyć, że to w gruncie rzeczy tylko cukierek składniowy dla przekazywania this do konstruktora klasy wewnętrznej i późniejszego odwoływania się do niego. Ciekawiej jest wtedy, gdy mała, wewnętrzna, pomocnicza klasa jest na tyle mała i pomocnicza, że nie warto nawet nadawać jej nazwy, bo potrzeba nam tylko jednego jej obiektu:

  1. public class Foo {
  2.     private ISomeInterface si = new ISomeInterface() {
  3.         @Override
  4.         public void someMethod() { }
  5.     };
  6. }

Wówczas wszystko co o tej klasie wiemy to to, że implementuje ona pewien interfejs i właśnie za jego pośrednictwem możemy się do jej jedynego obiektu odwoływać.

Tak w skrócie wygląda teoria. Z praktycznego punktu widzenia mogę natomiast powiedzieć, że dwojga złego lepiej już te wątpliwej świeżości cukierki składniowe mieć, skoro innego wyjścia nie ma… Mam tu na myśli oczywiście fakt, że wobec braku delegatów/zdarzeń/domknięć/itp. jedynym sposobem na komunikację zwrotną w aplikacjach javowych jest wywoływanie metod z ustalonych interfejsów, implementowanych przez obiekty, które następnie podaje się jako “słuchacze” (listeners). Niestatyczne klasy wewnętrzne zapewniają przynajmniej łatwe połączenie między tymi sztucznie wprowadzonymi obiektami i resztą programu.
Trzeba jednak uważać, by nie zamienić swojego kodu w spaghetti, co może się łatwo zdarzyć, jeśli radośnie wpleciemy definicje klas w treść funkcji. Jest to szczególnie niepożądane w kodzie inicjalizującym np. elementy UI, w którym należy po kolei poustawiać wszystkie listenery dla wszystkich kontrolek. Zdefiniujmy je wszystkie w locie i będziemy mieli całą obsługę zdarzeń w jednej metodzie. Fuj!

Dlatego lepiej już definiować takie pomocnicze obiekty w podobny sposób, jak wyżej – tj. jako pola, którym przypisujemy klasy stworzone ad hoc, zawierające metody z reakcjami na zdarzenia. To oczywiście tylko nędzna imitacja składni prawdziwych procedur zdarzeniowych, ale przynajmniej jest to podróbka akceptowalna.

Tags: , , ,
Author: Xion, posted under Programming » 5 comments

Split

2010-07-02 17:44

Tytuł dzisiejszej notki nie ma nic wspólnego z miejscowością wypoczynkową na południu Chorwacji, chociaż pewnie obecne temperatury nasuwają takie skojarzenia :) Zamiast tego chodzi o split łańcucha znaków, czyli bardzo często potrzebną w praktyce operację na stringach.
Danymi dla niej są najczęściej dwa napisy, zaś wynikiem jest tablica podciągów pierwszego z nich, powstała poprzez podzielenie go względem wystąpień drugiego (tzw. separatora). Ponieważ jak zwykle przykład będzie mówił najwięcej, niniejszym podaję nawet kilka:

  1. Split("Ala ma kota", " ") == { "Ala", "ma", "kota" };
  2. Split("1, 2, 3, 4", ",") == { "1", " 2", " 3", "4" };
  3. Split("<point x='23' y='14' z='-3' />", " ")
  4.     == { "<point", "x='23'", "y='14'", "z='-3'", "/>" }

Zwłaszcza ostatni pokazuje, że split jest rzeczywiście użyteczną funkcją, mogącą ułatwiać parsowanie formatów tekstowych (zwłaszcza, jeśli daje się ją zastosować wielokrotnie). Warto więc mieć takową w swoim języku/bibliotece. Jak więc przedstawia się jej dostępność na różnych platformach?

Tradycyjnie w .NET i Javie jest dobrze, a nawet lepiej. W obu przypadkach funkcja (a właściwie metoda) Split/split klasy String dodaje nawet trochę więcej możliwości niż to opisałem wyżej. I tak w .NET można podać więcej niż jeden oddzielacz, natomiast w Javie domyślnie może być nim również wyrażenie regularne.
Niektóre języki skryptowe i skryptopodobne też mają się w tym względnie całkiem dobrze. W Pythonie jest metoda split w klasie napisu, natomiast PHP ma funkcję explode, która mimo innej nazwy działa bardzo podobnie.

Ale nie wszędzie funkcja typu split jest od razu dostępna; niekiedy trzeba ją sobie samemu napisać. Przykładem języka, gdzie może być to koniecznie, jest Lua oraz – jakżeby inaczej – C++ :) Ze względu na użyteczność splita często znajdowałem się w sytuacji, gdzie konieczne/wygodne było jego napisanie. Po kilku(nastu?) takich przypadkach doszedłem wreszcie do czegoś podobnego do poniższego kodu:
typedef std::vector StringArray;
StringArray Split(const std::string& text, const std::string& delim)
{
StringArray res;
if (delim.empty()) { res.push_back(text); return res; }

std::string::size_type i = 0, j;
while (i < text.length() && (j = text.find(delim, i)) != std::string::npos) { res.push_back (text.substr(i, j - i)); i = j + delim.length(); } res.push_back (text.substr(i)); return res; }[/cpp] Na koniec zwrócę jeszcze uwagę na to, że czasami trzeba ostrożnie postępować z rezultatem splitu. Zdecydowana większość wersji tej operacji dopuszcza, by w wynikowej tablicy występowały puste ciągi. Odpowiadają one kilku kolejnym wystąpieniom separatora lub jego obecnością na początku bądź końcu ciągu. Jeśli nie są one nam potrzebne (a rzadko są), to należy je zignorować lub usunąć.

Tags: , , , , ,
Author: Xion, posted under Programming » 5 comments

Co się rzuca, czyli natywne klasy wyjątków

2010-05-16 13:46

W wielu językach wyjątki to niemal obowiązkowa metoda zgłaszania i obsługi błędów. W innych (tj. w C++) jest ona w dużym stopniu opcjonalna. W obu przypadkach istnieją zwykle dostępne out-of-the-box klasy wyjątków, których obiekty można wyrzucać, bez tworzenia swoich własnych. Ich używanie jest zdecydowanie wskazanie. Warto więc przyjrzeć się, jakie predefiniowane obiekty wyjątków przewidują standardowej biblioteki różnych języków.

I tak w C++ mamy bazową klasę exception, która zawiera w sumie niewiele więcej ponad komunikat o błędzie. Zresztą dotyczy to tak naprawdę wszystkich wbudowanych w C++ klas wyjątków; tym, co je rozróżnia, jest po prosty typ. Mamy tu prostą hierarchię dziedziczenia, wyróżniającą klasy bazowe: runtime_error dla niskopoziomowych błędów czasu wykonania (jak na przykład overflow_error) oraz logic_error dla błędów logicznych aplikacji. Z tej drugiej, bardziej dla nas interesującej, wyprowadzone są też wyjątki nieco bardziej szczegółowe, jak np.: invalid_argument, range_error, domain_error czy lenght_error. Wszystkie cztery są zresztą podobne do siebie pojęciowo i wydają się służyć głównie do sygnalizowania nieprawidłowych wartości argumentów funkcji. Do innego rodzaju błędów sensowniej jest używać klas bazowych lub niestety napisać własne (najlepiej dziedziczące z nich).

Na platformie .NET i w Javie jest z tym dużo lepiej – tam typów wyjątków jest niemal za dużo. Nie ma tu miejsca na omawianie ich wszystkich i nie jest to zresztą konieczne, gdyż nazwy klas są zwykle wystarczające do zorientowania się, z jakim rodzajem błędu mamy tu do czynienia. Według mnie najczęstszymi sytuacjami, w których można wyrzucić obiekt którejś z natywnych klas, są:

  • ogólnie pojęte niepoprawne wartości argumentów funkcji – System.ArgumentException / java.lang.IllegalArgumentException
  • argument będący null-em w sytuacji, gdy wymagany jest obiekt – System.ArgumentNullException
  • wykroczenie poza zakres argumentu działającego jak indeks – System.IndexOutOfRangeException / java.lang.IndexOutOfBoundsException
  • próba wykonania operacji na obiekcie, którego stan jest niepoprawny – System.InvalidOperationException / java.lang.IllegalStateException
  • wywołanie niezaimplementowanej metody wirtualnej – System.NotImplementedException
  • próba wykonania operacji, której obiekt nie wspiera – System.NotSupportedException / java.lang.UnsupportedOperationException
  • błąd podczas operacji wejścia/wyjścia – System.IO.IOException / java.io.IOException
  • przekroczenie czasu operacji (timeout) – System.TimeoutException / java.util.concurrent.TimeoutException

Można zauważyć, że dla większości pozycji odpowiadające klasy wyjątków istnieją na obu platformach i nawet nazywają się podobnie. Zapewne to kolejny znak wzajemnej inspiracji ich twórców ;)

Tags: , , , ,
Author: Xion, posted under Programming » 4 comments

Dlaczego nie lubię typów referencyjnych

2009-12-16 11:54

Jeśli chodzi o C++, to nietrudno zauważyć, że często pozwalam sobie na sporo uwag krytycznych pod adresem tego języka. Oczywiście zawsze jest to krytyka konstruktywna :) Tym niemniej wiele jest tu rzeczy, o których można powiedzieć, że w innych językach zostały pomyślane lepiej (łącznie z takimi, których w C++ nie ma, a przydałyby się).
Dlatego dzisiaj będzie trochę nietypowo. Chcę bowiem wspomnieć o problemie, który w językach pokroju C# czy Javy potrafi doprowadzić do powstawania trudnych do wykrycia błędów – i który jednocześnie w C++ w zasadzie nie występuje wcale.

Mam tu na myśli semantykę referencji, czyli pewien szczególny sposób odwoływania się do szeroko rozumianych obiektów w kodzie. Klasy, a właściwie to prawie wszystkie typy poza podstawowymi (jak liczby czy znaki), są w C# i Javie obsługiwane w ten właśnie sposób; dlatego czasami nazywa się je typami referencyjnymi.
Najważniejszą cechą takich typów jest fakt, że należące do nich zmienne nie zawierają bezpośrednio instancji obiektów. Jeśli na przykład Foo jest klasą, to deklaracja:

  1. Foo x;

nie sprawi, że pod nazwą x będzie siedział obiekt typu Foo. x będzie tutaj zaledwie odwołaniem do takiego obiektu – w tym przypadku zresztą odwołaniem pustym, niepokazującym na nic.
Jest to zachowanie diametralnie różne od typów podstawowych, jak choćby int. Ale idźmy dalej – skoro mamy zmienną mogącą trzymać odwołanie (czyli referencję) do obiektu, to pokażmy nią na jakiś obiekt, na przykład taki zupełnie nowy:

  1. x = new Foo();

A że w prawdziwym programie zmiennych i obiektów jest zawsze mnóstwo, to wprowadźmy na scenę jeszcze parę:

  1. Foo y = x;
  2. y.SomeValue = 4; // hmm...

No i zonk, można powiedzieć… Nikt aczkolwiek tego nie powie, bo dla każdego programisty C#, Javy itp. istnienie wielu referencji do tego samego obiektu jest rzeczą całkowicie naturalną. Jednak wiem, że podobny kod dla dowolnego typu liczbowego (zastąpiwszy ostatnią linijkę przez y += 4; lub coś tym w guście) zachowałby się zupełnie inaczej. Wiem też, że kiedyś byłem zmuszony wykonać kilka empirycznych testów, by się o tym naocznie przekonać; było to jeszcze w Delphi, a powodem były oczywiście jakieś “dziwne” błędy, na które natrafiłem w jednym ze swoich programów. Źle użyte typy referencyjne łatwo mogą być bowiem przyczyną takich błędów, które zresztą bywają potem trudne do wykrycia.

Bez jakiegoś rodzaju referencji nie da się rzecz jasna wyobrazić sobie użytecznego języka programowania. Sęk w tym, że w C# czy Javie używanie ich nie jest opcją do stosowania w tych przypadkach, które tego wymagają – jest koniecznością wymuszoną przez sam fakt programowania z użyciem klas i obiektów. To całkiem inaczej niż w C++, gdzie w tym celu trzeba wyraźnie zaznaczyć swoje intencje (najczęściej poprzez użycie typów wskaźnikowych).
W tworzeniu oprogramowania istnieje tzw. zasada najmniejszego zdziwienia (principle of least astonishment). Mówi ona, że przy alternatywie równoważnych przypadków powinno się wybrać ten, który u użytkownika końcowego będzie powodował mniejsze zdziwienie. Czy typy referencyjne zachowujące się zupełnie inaczej niż typy podstawowe i “same” zmieniające swoją zawartość nie są przypadkiem złamaniem tej reguły?…

Tags: , , ,
Author: Xion, posted under Programming, Thoughts » 12 comments
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.