Do pisania programów wystarczy dowolny edytor tekstu i linia poleceń, ale dzisiaj trudno przecenić wygodę, jaką dają zintegrowane środowiska programistyczne (IDE). I chociaż każda porządna aplikacja tego typu – niezależnie od języka, dla którego jest przeznaczona – posiada pewną określoną i oczywistą funkcjonalność (jak choćby rozbudowane zarządzanie projektami), to w wielu można znaleźć drobne i interesujące dodatki.
Taki NetBeans (IDE do Javy) posiada na przykład pasek boczny, widoczny po prawej stronie paska przewijania w polu z kodem. Reprezentuje on cały otwarty plik, zaś naniesione na niego kreski odpowiadają linijkom, w których “dzieje się” coś potencjalnie interesującego. Mogą to być wiersze zawierające błędy kompilacji albo wystąpienia identyfikatora (nazwy), w którym aktualnie znajduje się kursor. Najważniejsza cechą tych wskaźników jest to, że można w nie klikać i w ten sposób przenosić się do odpowiednich miejsc w kodzie.
Według mnie to całkiem zmyślne rozwiązanie, przydatne zwłaszcza przy poprawianiu błędów, gdyż oszczędza przechodzenia do okienka z outputem kompilatora, a potem z powrotem do kodu. Jego użyteczność może się aczkolwiek ujawnić najlepiej tylko w przypadku, gdy IDE przeprowadza kompilację w tle i na bieżąco podkreśla i zaznacza napotkane błędy.
Wyświetlanie pojedynczych trójkątów – nawet najpiękniej pokolorowanych – to oczywiście dopiero początek. Niezależnie od tego, czy uczymy się obsługi jakiejś biblioteki 3D czy też piszemy własną, chcemy zająć się przede wszystkim wyświetlaniem brył. Rodzajów możliwych brył jest oczywiście sporo, ale te często używane i “regularne” ich rodzaje charakteryzują się przede wszystkim tym, że dają się opisać równaniami matematycznymi i zależnościami geometrycznymi. Wiemy na przykład, że sfera jest taką figurą, która składa się ze wszystkich punktów znajdujących się dokładnie w określonej odległości od danego – środka.
Problem w tym, że takich punktów jest “dość” dużo, bo nieprzeliczalnie wiele. Zarówno tą, jak i każdą inną bryłę w tradycyjnym renderingu zwykło się więc przybliżać automatycznie generowanymi trójkątami (jest to różnica np. w stosunku do raytracingu, czyli śledzenia promieni). Nazywa się to triangulacją i wymaga kilku operacji, takich jak:
I to w zasadzie wszystko. Warto oczywiście natychmiast sprawdzić rezultat przy pomocy renderowania w trybie wireframe, czyli rysowania tylko konturów figur. Jeśli wszystko dobrze pójdzie, to można się już pochwalić czymś więcej niż tylko jednym trójkątem, co też niniejszym czynię :)
Zawsze jest coś do zakodzenia. Nawet jeśli żadne potężne siły zewnętrzne niczego nam nie narzucają, to mimo to (czy raczej: właśnie dlatego) pod czaszką ciągle kołaczą się pomysły. Te co lepsze przejdą w końcu do stadium projektów, by wreszcie – w nielicznym gronie – dojść do tego momentu, w którym należy otworzyć swoje ulubione środowisko programistyczne i zacząć pisać.
Uważa się powszechnie, co poparto (zbyt) wieloma przykładami, że projekty ciężko jest kończyć. Jednak związana z tym aktywna czynność – czyli porzucenie – przychodzi oczywiście bardzo, bardzo łatwo. Tak naprawdę o wiele trudniej jest zacząć i choć wiele osób mówi, że wystarczy to zrobić “tak po prostu”, w istocie sprawa jest chyba znacznie bardziej skomplikowana.
Niezależnie bowiem od tego, czy wypływamy na szerokie wody kodu, mając w głowie jedynie pomysł, czy też posiadamy dokładny plan; i niezależnie od tego, czy już zabieramy się za programowanie, czy jeszcze zajmujemy się samym projektowaniem – w każdym przypadku musi nadać swojemu dziełu zupełnie nową formę. Opieramy się wprawdzie na notatkach, szkicach, w ostateczności tym co zostało na jeszcze w głowie – lecz musimy to wszystko przerobić na całkowicie inną postać.
Można więc powiedzieć, że zaczynamy from scratch – od zera. Ta pustka ma najczęściej bardzo konkretny wymiar: dużego, ascetycznego okienka z uporczywie migającym kursorem, żądającym, abyśmy te wolne miejsce czym prędzej zapełnili. Dla mnie jej widok jest zawsze rozpraszający i nawet jeśli akurat dokładnie wiem, co chcę napisać, zbija mnie to z tropu.
W takiej sytuacji przydatne jest posiadanie jakiegoś stałego, “magicznego” szablonu, nagłówka lub jakiekolwiek innego kawałka kodu – niekoniecznie komentarza – od którego możemy zacząć. Choćby nazwa pliku, doskonale nam znane imię autora, data, nazwa projektu, krótki opis pliku, niezbędne dyrektywy dla kompilatora… Coś, co na pewno nie sprawi, że nasz kod będzie lepszy, łatwiejszy do zrozumienia czy efektywniejszy. Ale przynajmniej pozwoli zacząć go pisać.
Gdyby zorganizować konkurs na jak największą ilość kodu napisaną celem osiągnięcia jak najprostszego efektu, to pewnie obrazek po lewej (i kod, który za nim stoi) mógłby zająć w nim całkiem dobrą lokatę. Na pierwszy rzut oka to tylko niebieski trójkąt na żółtym tle; na drugi, trzeci i każdy następny zresztą też :) Na tym arcyprostym obrazku nie widać całego, dość skomplikowanego mechanizmu, dzięki któremu możemy go oglądać.
Rzeczony trójkąt jest bowiem efektem pracy programowego renderera, którego to od jakiegoś czasu – z konieczności, acz nie bez pewnej przyjemności – staram się popełnić. Taki kawałek oprogramowania ma za zadanie robić mniej więcej to, co potrafią zaawansowane biblioteki graficzne w rodzaju DirectX i OpenGL. Są oczywiście istotne różnice, wśród których największą jest brak wykorzystania typowych możliwości współczesnych kart graficznych – czyli właśnie przetwarzania trójkątów. Wręcz przeciwnie: wszystkie obliczenia pracowicie wykonuje główny procesor, zajmując się po kolei nie tylko każdym wielokątem, ale także każdym pikselem. Ma więc wyjątkowo dużo roboty, z którą jednak potrafi sobie poradzić.
O czym świadczy więc pokazany tutaj trójkąt? Ano o tym, że podstawowy potok renderowana ma się całkiem dobrze. W jego skład wchodzi transformowanie trójkątów przekształceniami macierzowymi, oświetlenie per-vertex, sprawdzanie widoczności pikseli przy pomocy bufora Z oraz rzutowanie perspektywiczne i rasteryzacja wynikowej płaskiej geometrii. Zgadza się, to zupełne podstawy podstaw, nieobejmujące chociażby teksturowania, lecz i tak realizujący je kod nie wiadomo kiedy rozrósł się do ponad dwóch tysięcy linijek. Faktycznie więc to był dosyć pracochłonny trójkąt :)
Zabłysnę jeszcze przykładowym kodem wykorzystującym renderer, który to wyglądać może mniej więcej tak:
Inspiracje pewną popularną biblioteką w zakresie interfejsu są, jak sądzę, doskonale widoczne :)
Komentarze umieszczamy w kodzie, aby opisać jego działanie. Są one przeznaczone wyłącznie dla osób czytających go i jedynie programy typu javadoc czy doxygen – służące automatycznemu generowaniu dokumentacji – mogą się niektórymi komentarzami interesować. Na pewno jednak nie robi tego kompilator.
Wymyślono jednak, że niektóre elementy kodu potrzebują innych, specyficznych “komentarzy”, przeznaczonych dla kompilatora właśnie. Różnie się one nazywają w różnych językach, ale ich głównym celem jest przekazanie dodatkowych informacji odnośnie klasy, metody, typu czy innego elementu programu, bez potrzeby stosowania.
W .NET takie dodatki nazywa się atrybutami i umieszcza przed wybraną deklaracją, w nawiasach kwadratowych (lub kątowych w przypadku Visual Basica), oddzielone przecinkami. Atrybuty te mogą dotyczyć właściwie wszystkiego, począwszy od wskazania na klasę, która może być serializowana (i pola, które nie powinny być) po informacje dla Form Designera na temat danej właściwości niestandardowego komponentu:
Ogólnie dotyczą one jednak różnych funkcji samej platformy .NET i służą wskazaniu, które elementy pełnią określone role w różnych rozwiązaniach, które działają w jej ramach. Można aczkolwiek definiować także własne atrybuty, a potem w czasie działania programu wydobywać o nich informacje przy pomocy mechanizmu refleksji.
A jak to wygląda w Javie? Otóż tam od wersji 1.5 istnieją adnotacje. Ich nazwy poprzedza się znakiem @
i umieszcza w osobnych linijkach, poprzedzających deklaracje, których dotyczą. Ponieważ tutaj język jest ściśle związany z platformą (maszyną wirtualną Javy), adnotacje czasami pełnią funkcje “brakujących” słów kluczowych lub dyrektyw dla kompilatora. Typowy przykład to adnotacja Override
:
którą możemy oznaczać przesłonięte wersje metod wirtualnych. Jest to praktyczne, gdyż w przypadku popełnienia błędu (np. literówki w nazwie) kompilator ostrzeże nas, że tak naprawdę zdefiniowaliśmy zupełnie nową metodę (bez tego błąd objawiłby się dopiero nieprawidłowym działaniem programu).
Naturalnie, możliwe jest też tworzenie własnych adnotacji oraz pobieranie informacji o nich przy pomocy refleksji. Aż korci, żeby sprawdzić, kto od kogo ściągał tutaj pomysły ;-)
W C++ deklaracje standardowo nie mają żadnych “ozdobników”, ale pod tym względem w różnych kompilatorach bywa różnie. Na przykład w Visual C++ mamy słówko __declspec
, które służy do całego mnóstwo różnych celów. Wśród nich są chociażby takie oto warianty:
__declspec(align(n))
służy do określania, jak dane (np. pola struktur) mają być wyrównane w pamięci. Dzięki temu będą one umieszczone tak, by zajmowały zawsze wielokrotność podanych n bajtów, co przy odpowiedniej wartości (np. 32) może zwiększyć wydajność lub (dla 1) zmniejszyć zajętość pamięci.__declspec(deprecated)
pozwala oznaczyć dany element kodu jako przestarzały. Jego użycie będzie skutkowało wtedy ostrzeżeniem kompilatora.__declspec(dllexport)
i __declspec(dllimport)
służą do tworzenia symboli eksportowanych w bibliotece DLL i do importowania tych symboli w innym programie.__declspec(property)
wprowadza konstrukcję właściwości do klasy, bardzo podobną do tych obecnych w Delphi. Po podaniu jednej lub dwóch metod dostępowych (do odczytu i ew. zapisu), otrzymujemy właściwość o danej nazwie i typie. Jaka szkoda, że to nieprzenośne :)Zasadniczo Visual C++ posiada też atrybuty podobne do .NETowych, które są konieczne do tworzenia interfejsów COM. Na szczęście nimi, jak i samym COM-em, nie trzeba już sobie zaprzątać głowy :)
Unicode to ciekawy wynalazek. Zamiast stosować wymyślne sposoby na “przełączanie” sposobu interpretowania zwykłych 8-bitowych znaków, ktoś mądry wymyślił po prostu, że obecnie nie ma większych przeciwwskazań, aby zwykły tekst zajmował dwa razy więcej miejsca niż dotychczas. Powstał więc standard UTF-16, w którym stron kodowych nie ma, a każdy znak jest zapisany za pomocą jednego z 65536 kodów.
Ale jak to zwykle bywa z rozwiązaniami, które mają rozwiązywać istniejące problemy, Unicode natychmiast stworzył swoje własne :) Oprócz tego, że założone 16 bitów szybko okazało się za małe (stąd istnienie także UTF-32), powstał też szereg kłopotów praktycznych. Jednym z nich jest choćby to, że żyjemy w okresie przejściowym (który zresztą trwa już wybitnie długo) i że w użyciu jest zarówno unikod, jak i zwykłe ANSI. A na pierwszy rzut oka (ludzkiego i programowego) tekst jest po prostu ciągiem bajtów i bez zewnętrznych wskazówek nie jest możliwe określenie w stu procentach, czy został on zapisany w ANSI, UTF-8, UTF-16 czy UTF-32. Co więcej, znaki Unicode są oczywiście liczbami, a ponieważ zasadniczo zajmują one więcej niż jeden bajt, pojawia się problem z ustaleniem właściwej kolejności tych bajtów (little-endian lub big-endian).
Oba te problemy ma rozwiązywać tzw. znacznik porządku bajtów (Byte Order Mark), ale oczywiście jak większość dobrych praktyk, także i jego umieszczanie na początku dokumentów nie jest zbyt popularne :)
Z programistycznego punktu widzenia sprawa nie wygląda aczkolwiek aż tak źle i w większości przypadków jesteśmy w jednej z dwóch sytuacji. Pierwsza z nich to “nieświadome” korzystanie z unikodu (czy może raczej “szerokich”, dwubajtowych znaków), bo został o niego oparty używany przez nas język oraz platforma; najlepszymi przykładami są tu .NET i C# oraz Java.
Druga sytuacja to możliwość wyboru, z jakiego systemu będziemy korzystali. Bardzo dobre jest to, że prawie zawsze możemy zdecydować się na… oba, czyli potencjalne kompilowanie dwóch wersji: ANSI i Unicode. Pod Windows na przykład w programach pisanych w C++ wystarczy przestrzegać kilku prostych i dobrze znanych zasad:
char
(lub wchar_t
) należy – wszędzie tam, gdzie chodzi nam o znaki – używać typu TCHAR
, który w zależności od wersji zostanie zamieniony na jeden z tych dwóch.TEXT
(czyli TEXT("Coś")
zamiast po prostu "Coś"
), dzięki czemu zostaną one skompilowane jako łańcuchy ANSI lub Unicode.Niestety, słowo ‘niektóre’ sytuuje się dość daleko od ‘wszystkie’ i dlatego ostatecznie nie jest tak różowo. Czołowe miejsce na liście “niewspółpracujących” zajmuje standardowa biblioteka C++. Z nią trzeba sobie poradzić we własnym zakresie.
Nie jest to aczkolwiek bardzo trudne, jako że każdy kompilator definiuje makro dla rozróżnienia wersji ANSI i Unicode. Visual C++ na przykład włącza w tym przypadku symbol _UNICODE
, którego możemy użyć do stworzenia odpowiednich aliasów:
Możemy je umieścić we własnym pliku nagłówkowym i dołączać we własnych projektach. Ponieważ jednak typów zależnych od znaków jest w STL całkiem sporo, można z powodzeniem użyć chociażby rozwiązania zamieszczonego na CodeProject, w razie potrzeby rozszerzając je o kolejne aliasy.
Niezastąpionym rodzajem składników klas są pola i metody, ale większość języków umożliwia też dodawanie również innych elementów. Ich głównym przeznaczeniem jest ułatwianie życia programiście i czynienie kodu bardziej czytelnym – pod takim rzecz jasna warunkiem, że są one odpowiednio użyte.
Do tej szuflady wpadają na przykład właściwości. Są to takie składniki klas, które wyglądają jak zmienne, lecz w rzeczywistości za pobieraniem i ustawianiem ich wartości może kryć się bardziej skomplikowany kod niż tylko proste odwołanie do zmiennej.
Spośród mainstreamowych języków prawdopodobnie najwygodniejszy mechanizm właściwości występuje w C#:
Ważną cechą właściwości jest też to, aby możliwe było tworzenie takowych w wersji tylko-do-odczytu. Tutaj wystarczy pominąć frazę set
.
Dla kontrastu w Javie właściwości nie ma… w ogóle :) Mimo to narzędzia do wizualnego tworzenia aplikacji radzą sobie całkiem nieźle w odczytywaniu funkcjonalności komponentów, posługując się tzw. JavaBeans. W skrócie jest to pewien mechanizm oparty na refleksjach (odczytywaniu informacji o klasie w kodzie programu) i specyficznej konwencji nazywania metod dostępowych. Oczywiście nawet najlepsze nazwy nie zrekompensują dziwnej składni takich “właściwości”, ale sam pomysł trzeba uznać za dosyć udany.
W Delphi w zasadzie też trzeba tworzyć metody dostępowe, lecz można je podpiąć pod faktyczne właściwości:
[delphi]type TFoo = class
private
FNumber : Integer;
procedure SetNumber(const ANumber : Integer);
public
// właściwość
property Number : Integer read FNumber write SetNumber;
end;[/delphi]
Możliwe jest też, jak widać, bezpośrednie przełożenie właściwości na odpowiednią zmienną, która przechowuje jej wartość. Całkiem niezłe, chociaż wymaga to sporo pisania – co aczkolwiek jest charakterystyczną cechą Delphi :)
A cóż z naszym ulubionym językiem, czyli C++? Właściwości w nim oczywiście nie ma, chociaż w Visual C++ na przykład istnieje deklaracja __declspec(property)
, mająca podobne możliwości do słówka property
z Delphi. Jeśli zaś chodzi o przenośne rozwiązanie, to można sobie wyobrazić obiekt “opakowujący” wskaźnik na pole lub metodę, który działałby jako symulacja właściwości – na przykład:
Przy użyciu kilku sztuczek z szablonami, wspomnianymi wskaźnikami i być może jakimś rozwiązaniem dla delegatów, taka implementacja jest zapewne możliwa. Istnieje aczkolwiek dość poważna niedogodność: taki obiekt opakowujący należałoby zainicjować w konstruktorze:
zatem “deklaracja” naszej “właściwości” rozbita by została na dwa miejsca. A właściwie – na co najmniej dwa miejsca, bo w przypadku większej ilości konstruktorów rzecz wygląda nawet gorzej.
Technika ta będzie przydatna wtedy, gdy pola będziemy mogli inicjalizować w momencie ich deklaracji, co jest proponowane w C++0x. Wygląda więc na to, że znów dochodzimy do konkluzji, że język C++ pozostaje daleko w tyle w stosunku do swych młodszych braci. Ale czegóż można oczekiwać po staruszku, który w tym roku będzie obchodził swoje ćwierćwiecze? :) Zanim więc otrzyma on jakże konieczny lifting, musimy żyć z niezbyt wygodnymi, ale koniecznymi, metodami dostępowymi.
Hmm… A przecież chciałem dzisiaj ponarzekać raczej na Javę… No cóż, nie wyszło ;-)