3. Struktury danych

Uwaga! Informacje na tej stronie mają ponad 5 lat. Nadal je udostępniam, ale prawdopodobnie nie odzwierciedlają one mojej aktualnej wiedzy ani przekonań.

3. Struktury danych

Tytuł tej części brzmi groźnie (jak ja lubię straszyć :), ale uczyć będziemy się w nim po prostu o zmiennych, i przechowywaniu w nim wartości różnego rodzaju.

3.1. Zmienne

Zmienna to takie pudełko. Można do niej schować, przechowywać w niej i odczytywać z niej jakąś wartość, np. tekst albo liczbę. Do zmiennej odwołujemy się przez jej nazwę.

Pamiętajmy, że TCL jest językiem bez kontroli typów. Przechowywanie w nim łańcucha, liczby czy wartości innego typu to kwestia umowna. Ma to swoje wady, ale ma też zalety. Każdą wartość możesz potraktować jako tekst. Możesz też odpowiedni tekst potraktować jako liczbę wykonując obliczenia.

Zmiennych nie trzeba deklarować przed użyciem. Zmienna jest tworzona w chwili przypisywania jej wartości. Można potem w ten sam sposób zmienić jej wartość.

Przypisanie wartości do zmiennej:

set Zmienna "Tralala"

Zmienną można też usunąć:

Usunięcie zmiennej:

unset Zmienna

Wartość zmiennej można odczytywać poprzedzając jej nazwę znakiem dolara.

Przykład zmiennej:

set sKomunikat "Ble"
Debug "Wartosc zmienej to: $sKomunikat"

Zmienna musi istnieć w chwili jej odczytywania. Inaczej próba odczytu zakończy się błędem.

3.1.1. Zmienne lokalne

Zmienna lokalna to zmienna utworzona wewnątrz procedury. Istnieje tylko w jej wnętrzu i wraz z przechowywaną wartością zostaje zniszczona w momencie zakończenia danego wywołania procedury. To jest najprostszy rodzaj zmiennej i nie ma tutaj co więcej o niej pisać. Po prostu tworzy się ją i używa się jej w procedurze tak jak na przykładzie podanym wyżej.

3.1.2. Zmienne globalne

Zmienna globalna powstaje wtedy, kiedy instrukcja set wykonuje się bezpośrednio w kodzie skryptu, a nie wewnątrz procedury. Istnieje od tego momentu przez cały czas działania skryptu chyba, że zostanie usunięta. Nie usunie jej także polecenie .rehash. Z tym, że ponowne wykonanie instrukcji set spowoduje ponowne przypisanie jej początkowej wartości.

W kodzie pisanym globalnie zmiennej globalnej można używać tak, jak używa się zmiennej lokalnej wewnątrz procedury. Inaczej niż w większości języków programowania, aby użyć zmiennej globalnej wewnątrz procedury, trzeba ją specjalnie zadeklarować. Służy do tego polecenie global. Nic się nie stanie, jeśli w momencie takiej deklaracji zmienna nie istnieje. Po prostu zostaje ona utworzona.

Przykład zmiennej globalnej:

proc UstawZmiennaGlobalna {a_Wartosc} {
  global ZmiennaGlobalna
  set ZmiennaGlobalna $a_Wartosc
}

W bezpośrednim kodzie skryptu mogłem równie dobrze wstawić instrukcję set, by naszej zmiennej przypisać jakąś wartość domyślną. Nie zrobiłem tego jednak i w ten sposób zmienna globalna zostaje stworzona dopiero w pierwszym wywołaniu procedury.

3.1.3. Zmienne w strafie nazw

Zmienna może też być składową strefy nazw. Importuje się ją do procedury w tej samej strefie instrukcją variable. Do zmiennej takiej można się też odwołać za pomocą operatora zakresu. Działa on wtedy jednakowo w każdym miejscu skryptu.

Przykład zmiennych w strefie nazw:

namespace eval Strefa {
  # Tym razem stworzymy zmienne od razu
  set m_sText "pechowa"
  set m_iLiczba 13

  proc Procedura { } {
    variable m_sText
    variable m_iLiczba

    Debug "Twoja $m_sText liczba to: $m_iLiczba"
  }
}

proc ZmienZmienne { } {
  set Strefa::m_sText "szczesliwa"
  set Strefa::m_iLiczba 7
}

Istnieje niepisana zasada mówiąca, że instrukcje importujące zmienne global i variable pisze się na początku kodu procedury.

3.1.4. Parametry

Parametry już znamy i nic nowego o nich nie powiemy, ale nie wypada nie wspomnieć o nich przy okazji omawiania rodzajów zmiennych. Parametry stają się wewnątrz procedury zmiennymi lokalnymi o przekazanych wartościach.

3.2. Uwaga na dolary!

Jeśli programowałeś wcześniej w PHP, nieobce może ci być stawianie znaku dolara $ przez nazwami zmiennych. Jest jednak względem TCL pewna istotna różnica. Pisałem że to prawdopodobnie największa pułapka w tym języku i przyszedł właśnie czas na jej wyjaśnienie. Zapamiętaj więc raz na zawsze: Dolar stawiamy tylko, kiedy chodzi nam o pobranie wartości, którą przechowuje zmienna. Jeśli chodzi nam o samą zmienną, dolara nie stawiamy.

Wydaje się to proste, ale w praktyce często niełatwo jest zdecydować, który wariant jest poprawny. Trzeba się wtedy zdać na własną intuicję i doświadczenie, a w przypadku błędów lub złego działania skryptu w pierwszej kolejności właśnie pośród postawionych i niepostawionych dolarów szukać winowajcy.

  1. Jeśli dolar jest gdzie nie powinno go być, zamiast zwykłego łańcucha próbujemy odczytać wartość zmiennej o podanej nazwie. Zmienna taka prawdopodobnie nie istnieje i tak powstaje błąd.
  2. Jeśli dolara brakuje, zamiast wartości przechowywanej przez podaną zmienną wykorzystujemy w danym wyrażeniu wartość tekstową w postaci nazwy tej zmiennej.
Brrr... okropne!

No niestety :/ Pocieszę cię jednak, że w całej swej prostocie język TCL nie ma już gorszych pułapek niż ta.

Pomożemy sobie jeszcze w zrozumieniu tego zagadnienia przykładem:

Przykład z dolarami:

# Nazwy parametrów to same... nazwy, a więc dolara nie ma
proc MojaProcedura {a_sParametr} {
  # Wyciągamy z parametru wartość, a więc dolar jest
  Debug "Wartosc parametru to: $a_sParametr"

  # Przypisanie do zmiennej, czyli chodzi nam o zmienną, czyli dolara nie ma
  set sZmienna 666
  # Interesuje nas wartość, którą ta zmienna przechowuje, czyli dolar jest
  Debug "Wartosc zmiennej to: $sZmienna"
}

3.3. Uchwyty

Mimo braku kontroli typów nie sposób nie wyróżnić kilku odrębnych rodzajów wartości. Rozpoczynamy ciągnące się aż do końca tej najważniejszej części kursu omawianie poszczególnych rodzajów wraz ze sposobami operowania na takich wartościach.

Zaczniemy od najprostszego, czyli od uchwytów. Uchwyt to ogólne określenie na wartość, którą skądś otrzymujemy, gdzieś możemy przekazać i poza tym nie da się zrobić z nią niczego sensownego. Przykładami mogą być poznane już uchwyty do timerów, a także do otwartych plików i jeszcze do innych rzeczy. Ogólnie można powiedzieć, że najczęściej uchwyt do czegoś otrzymuje się po utworzeniu tego czegoś i zapamiętuje w jakiejś zmiennej celem późniejszego usunięcia tego czegoś :)

3.3.1. Cykliczne timery - Ostatnie starcie :)

Pamiętasz naszą próbę utworzenia wykonującego się cyklicznie timera i problem, który wtedy napotkaliśmy? Teraz bogatsi w wiedzę na temat zmiennych możemy sobie z nim poradzić.

Ostateczna wersja timera cyklicznego:

proc SetTimer { } {
  global g_hTimer
  set g_hTimer [timer 1 OnTimer]
}

proc KillTimer { } {
  global g_hTimer
  killtimer $g_hTimer
}

proc OnTimer { } {
  Debug "Minela kolejna minuta"
  SetTimer
}

proc OnPreRehash {type} {
  KillTimer
}

SetTimer
bind evnt - prerehash OnPreRehash

Przed rehash (a jest to jedyna możliwa sytuacja, w której kod zostanie ponownie wykonany, czyli nowy timer utworzony, a stary nie zostałby usunięty) wykonuje się specjalna procedura i powoduje ona usunięcie timera.

3.4. Wartości logiczne

Wartość logiczna To taka wartość, która może przechowywać tylko jeden z dwóch stanów. W większości języków programowania jest to false i true. W TCL wartość logiczną reprezentuje liczba 0 lub 1. I to w zasadzie cała filozofia, trudno tu napisać coś więcej. Chyba, że omówimy sobie przy okazji...

3.4.1. Wyrażenia logiczne

Są to po prostu porównania mówiące nam, czy jakaś wartość jest równa, mniejsza itp. od drugiej. Dostępne są następujące operatory porównania:

< >
Mniejszy i większy
<= >=
Mniejszy lub równy i większy lub równy
== !=
Równy i nierówny (różny)
eq ne
Łańcuch równy i łańcuch nierówny (różny) - porównanie wartości traktowanych zawsze jako łańcuchy znaków

Wyrażenia porównujące zwracają wartość logiczną, ponieważ stwierdzają prawdę lub fałsz. Ujmując je w okrągłe nawiasy można je łączyć za pomocą operatorów:

!
NIE (ang. "NOT"), czyli negacja
&&
I (ang. "AND")
||
LUB (ang. "OR")

Takich wyrażeń nie można pisać ot tak, bezpośrednio w kodzie. Pamiętajmy, że TCL jest językiem opartym na poleceniach, a nie na wyrażeniach. Dlatego do obliczania wyrażeń służy specjalne polecenie: expr.

A teraz zagadka. Jaka będzie wartość zmiennej bOutput?

Zagadka:

set bOutput [expr ( !( 666 < 13 ) || ( "2 + 2" == "4" ) ) && ( "7" != 7 ) ]

Rozwiązanie: powyższe wyrażenie jest oczywiście fałszywe, a więc wartością będzie 0. Mam nadzieję że w pełni rozumiesz dlaczego.

3.5. Liczby całkowite

Liczba całkowita to dodatnia lub ujemna liczba bez części ułamkowej. Każda liczba może być potraktowana jako łańcuch i każdy łańcuch będący poprawną liczbą może być potraktowany jako liczba. Liczby, jak wszystko, można zapisywać w cudzysłowach. Jednak nie jest to potrzebne i dla czytelności oraz dla podkreślenia niełańcuchowego charakteru wartości lepiej tego nie robić.

3.5.1. Inkrementacja

Do zwiększania lub zmniejszania wartości zmiennej będącej liczbą całkowitą służy wygodne polecenie incr.

Przykład inkrementacji:

set iLiczba 7
# Zwiększamy liczbę o 13
incr iLiczba 13
# Zmniejszamy liczbę o 3
incr iLiczba -3
# Inkrementujemy liczbę, czyli zwiększamy ją o 1
#   W takim wypadku ostatni parametr można pominąć
incr iLiczba

Polecenie incr zwiększa wartość podanej zmiennej. Zwróć uwagę na brak dolara przed jej nazwą. Nie piszemy go, ponieważ chodzi nam o samą zmienną a nie o jej wartość. Jeśli potrzebujesz wartość zmiennej zwiększoną o 1 (lub ilekolwiek), musisz użyć polecenia expr

3.5.2. Wyrażenia całkowite

W wyrażeniach expr możesz używać operatorów arytmetycznych do operacji na liczbach całkowitych:

+ -
Dodawanie i odejmowanie
* / %
Mnożenie, dzielenie całkowite (z obcięciem reszty) i reszta z dzielenia

A także operatorów bitowych:

~
NIE (ang. "NOT")
<< >>
Przesunięcie bitowe (ang. "shift") w lewo i w prawo
&
I (ang. "AND")
|
LUB (ang. "OR")
^
XOR (exclusive OR)

3.5.3. Losowanie

Czasami zachodzi potrzeba otrzymania liczby pseudolosowej. Służy do tego wbudowane w Eggdropa polecenie rand. Następujące polecenie:

Losowanie liczby całkowitej:

set iLiczba [rand 10]

Spowoduje przypisanie liczby z zakresu między 0 a 9.

3.6. Liczby rzeczywiste

Liczby rzeczywiste mogą przechowywać część ułamkową zapisywaną po kropce. Reprezentowane są w pamięci jako liczby zmiennoprzecinkowe o podwójnej precyzji.

3.6.1. Wyrażenia rzeczywiste

Omówione operatory porównania stosowalne są w większości do wszystkich omówionych typów danych. Operatory arytmetyczne stosowane mogą być do dowolnych liczb. Wyjątkiem jest reszta z dzielenia, którą można liczyć tylko dla liczb całkowitych. Dodatkowo w wyrażeniach operujących na liczbach rzeczywistych stosować można szereg funkcji matematycznych takich, jak funkcje trygonometryczne, potęgowe, logarytmiczne i inne. Ich wykaz znajdziesz w dokumentacji języka TCL, w opisie polecenia expr.

Jedną z nich jest bezparametrowa funkcja rand(), która zwraca losową liczbę rzeczywistą z przedziału od 0.0 do 1.0. Nie należy jej mylić z przedstawionym wyżej poleceniem rand!

3.6.2. Uwaga na rodzaje liczb

Na rozróżnienie liczb całkowitych od rzeczywistych należy zwracać baczną uwagę. Nie wszystkie operatory i funkcje stosują się do obydwu tych rodzajów. Liczba całkowita może zostać potraktowana jako rzeczywista. Jeśli przynajmniej jeden z operandów wyrażenia składowego (np. operacji arytmetycznej) jest liczbą rzeczywistą, wynik także będzie liczbą rzeczywistą.

Ciekawym przypadkiem jest dzielenie. Jeśli dzielisz dwie liczby całkowite, wykonane zostaje dzielenie całkowite z obcięciem reszty.

Przykład dzielenia:

set iLiczba1 [expr 5 / 2]
set iLiczba2 [expr 5.0 / 2]

Wartością pierwszej zmiennej będzie 2, ponieważ wykonane zostało dzielenie całkowite. Wartością drugiej zmiennej będzie liczba rzeczywista 2.5, ponieważ jeden z operandów także był liczbą rzeczywistą.

3.7. Łańcuchy

Łańcuch (ang. "string", inaczej tekst) to ciąg dowolnych znaków. Nawet jeśli nie zawiera spacji, radzę ujmować go w cudzysłowy by podkreślić jego tekstowy charakter.

Do dołączania nowego tekstu na końcu łańcucha służy polecenie append.

Przykład z append:

append a $b
set a $a$b

Te dwie instrukcje są logiczne równoważne, ale pierwsza jest lepsza. Użycie append zamiast przypisywania całej wartości może mieć duże znaczenie dla wydajności kodu, szczególnie w przypadku długich łańcuchów.

Inne możliwości manipulacji na łańcuchach zapewnia polecenie string.

string first <substr> <str> [index]
Zwraca indeks pierwszego znaku pierwszego znalezionego wystąpienia wyszukiwanego podłańcucha w przeszukiwanym łańcuchu. index, jeśli podany, oznacza indeks znaku, od którego rozpoczyna się przeszukiwanie. Jeśli nie podany, przeszukiwany jest cały łańcuch str.
string index <str> <index>
Zwraca łańcuch zawierający pojedynczy znak pobrany z podanego łańcucha spod podanego indeksu.
string is <class> <str>
Zwraca wartość logiczną zależnie od tego, czy wszystkie znaki łańcucha należą do podanej klasy, np. cyfr, liter, dużych liter itd. Szczegóły znajdziesz w dokumentacji.
string length <str>
Zwraca liczbę całkowitą odzwierciedlającą ilość znaków podanego łańcucha.
string range <str> <first> <last>
Zwraca podłańcuch złożony ze znaków podanego łańcucha począwszy od tego o indeksie podanym jako first, a skończywszy na tym o indeksie podanym jako last. Jako last można podać end. Zwrócony zostanie wtedy łańcuch skopiowany od pozycji podanej jako first aż do końca.
string replace <str> <first> <last> [newstr]
Zastępuje część łańcucha nowym łańcuchem. Właściwie to usuwa z podanego łańcucha znaki począwszy od indeksu first aż do indeksu last i w to miejsce wstawia nowy łańcuch, jeśli został podany.

Znaki łańcucha indeksowane są od 0. Dokładny opis tych i wielu innych możliwości polecenia string znajdziesz w dokumentacji języka TCL.

Wartość każdego rodzaju, w tym liczbę, listę, a nawet uchwyt, możesz zawsze potraktować jako łańcuch.

3.7.1. Porównywanie łańcuchów

Pracując z łańcuchami natknąłem się na pewien dziwny błąd. Jeśli dwa identyczne łańcuchy zawierały niektóre polskie litery w standardzie Windows-1250 (którego używa m.in. mIRC), jakiekolwiek ich porównania zawsze zwracały 0 (czyli fałsz) wskazując niepoprawnie, że nie są one jednakowe. Zarówno operator ==, eq, jak i polecenie string użyte do porównywania.

Ku mojemu zaskoczeniu okazało się, że błąd nie występuje, jeśli porównuje się łańcuchy znak po znaku. Mimo, że przecież taki pojedynczy wyciągnięty znak także jest łańcuchem. Napisałem dzięki temu własną funkcję do porównywania łańcuchów:

Funkcja porównująca łańcuchy:

# Porównuje 2 łańcuchy znak po znaku unikając tym samym
# błędu podczas porównywania polskich liter
# Zwraca 1 lub 0 zależnie, czy podane łańcuchy są identyczne
proc MyStrCmp {s1 s2} {
  set l1 [string length $s1]
  set l2 [string length $s2]
  if {$l1 != $l2} {
    return 0
  }
  for {set i 0} {$i < $l1} {incr i} {
    if {[string index $s1 $i] != [string index $s2 $i]} {
      return 0
    }
  }
  return 1
}

3.7.2. Formatowania IRC

Usługa IRC oferuje oprócz przesyłania zwykłego tekstu także pewne możliwości jego formatowania. Być może znasz je używając np. skrótów klawiszowych [Ctrl]+[K] i [Ctrl]+[B] w mIRCu. Ich wykorzystywanie na codzień nie jest zalecane, ponieważ nie wszyscy używają klienta IRC, który je obsługuje. Poza tym niektórych one po prostu denerwują.

Warto jednak znać sposób ich tworzenia. Dzięki temu pisząc w TCL rozbudowaną grę pod IRC możesz zaoferować każdemu zarejestrowanemu graczowi możliwość włączenia formatowań jeśli chce. Dzięki temu przekazywane informacje będą bardziej czytelne. Formatowań dokonuje się wstawiając do przeznaczonego na wyjście IRC tekstu odpowiednie znaki specjalne o ustalonych kodach:

\037
Włącza lub wyłącza podkreślenie
\002
Włącza lub wyłącza pogrubienie
\003--
Zmienia kolor na podany w miejsce -- zapisany jako jedno- lub dwucyfrowa liczba w systemie dziesiętnym
\003
Wyłącza kolor

Kolory przypisane poszczególnym numerom są takie same jak te widoczne w mIRCu po naciśnięciu kombinacji klawiszy [Ctrl]+[K]. Oto ich pełna lista:

  1. Biały
  2. Czarny
  3. Ciemny niebieski
  4. Ciemny zielony
  5. Czerwony
  6. Bordowy
  7. Fioletowy
  8. Pomarańczowy
  9. Żółty
  10. Jasny zielony
  11. Morski
  12. Błękitny
  13. Niebieski
  14. Różowy
  15. Ciemny szary
  16. Jasny szary

Projektując kolory weź też poprawkę na to, że nie wszystkie są czytelne na białym tle. Jeśli dodam do tego, że nie wszyscy mają ustawione w swoich klientach IRC właśnie białe tło, będzie to kolejny argument przeciwko używaniu tych formatowań.

3.8. Listy

To najciekawsze struktury danych w TCL. Lista przechowuje kilka wartości zachowując ich kolejność. Mogą to być wartości dowolnego rodzaju, nawet kolejne listy.

3.8.1. Tworzenie list

Aby utworzyć listę, można ją zadeklarować używając nawiasów klamrowych:

Tworzenie listy 1:

set lLista1 {
  { "Programowanie" "ciekawe" }
  { "TCL" "prosty" }
}

W powyższym przykładzie powstała lista składająca się z dwóch elementów, a każdy z nich jest kolejną listą zawierającą po dwa łańcuchy.

Drugim sposobem na utworzenie listy jest polecenie list. Zwraca ono po prostu listę utworzoną ze wszystkich podanych elementów.

Tworzenie listy 2:

set l1 [list "Programowanie" "ciekawe"]
set l2 [list "TCL" "Prosty"]
set lLista2 [list $l1 $l2]

3.8.2. Dzielenie i łączenie

Aby podzielić łańcuch na elementy, które zwrócone zostaną w postaci listy, użyj polecenia:

Dzielenie:

split <łańcuch> [podzielnik]

Możesz też połączyć elementy listy w jeden łańcuch:

Łączenie:

join <lista> [łącznik]

Łącznik/podzielnik to łańcuch (zazwyczaj jeden znak), który oddziela lub ma oddzielać kolejne elementy w łańcuchu. Jeśli nie podany, domyślnie przyjęta zostaje spacja.

3.8.3. Operacje na listach

Do operacji na listach istnieją następujące polecenia:

concat [lista] [lista] [lista] itd...
Zwraca listę utworzoną z połączenia wszystkich list. Nie tworzy listy zawierającej podane listy, ale łączy elementy podanych list w jedną listę.
lappend <lista> [element] [element] [element] itd...
Dodaje podane elementy na końcu podanej listy.

Przykład dodawania elementów do listy:

lappend a $b
set a [concat $a [list $b]]

Te dwie instrukcje są logiczne równoważne, ale pierwsza jest lepsza. Użycie lappend zamiast przypisywania całej listy może mieć duże znaczenie dla wydajności kodu, szczególnie w przypadku dużych list.

lindex <lista> <index>
Zwraca element listy o podanym indeksie. Elementy listy indeksowane są od zera.
linsert <lista> <index> <element> [element] [element] itd...
Zwraca listę utworzoną po wstawieniu podanych elementów do podanej listy na podaną pozycję. Elementy wstawiane są przed element o podanym indeksie. Jako index można podać end. Nowe elementy zostaną wtedy dodane na końcu listy.
llength <lista>
Zwraca liczbę elementów na liście.
lrange <first> <last>
Zwraca listę będącą wycinkiem podanej listy składającą się z elementów począwszy od tego o indeksie first, a skończywszy na tym o indeksie last lub ostatnim, jeśli jako last podane zostało end.
lreplace <lista> <first> <last> [element] [element] [element] itd...
Zastępuje część elementów listy nowymi. Właściwie to usuwa z podanej listy element począwszy od indeksu first aż do indeksu last i w to miejsce wstawia nowe elementy, jeśli zostały podane.
lset <lista> <index> <wartość>
Zmienia wartość elementu podanej listy o podanym indeksie.

Istnieją też polecenia do sortowania i przeszukiwania list. Możesz nawet napisać własną procedurę, która zostanie wykorzystana przez TCL do porównywania elementów podczas takiego sortowania. Po szczegóły odsyłam do opisu poszczególnych poleceń w dokumentacji języka TCL.

3.9. Tablice

Tablica to nie kolejny, zwyczajny rodzaj danych. To pewien mechanizm TCL, który warto poznać i którego warto używać. Zmienna jest albo zwykłą pojedynczą wartością dowolnego rodzaju, albo tablicą.

Tablica w TCL ma postać niezachowującego kolejności zbioru wartości i skojarzonych z nimi kluczy. Tablice przeznaczone są głównie do tego, by uzyskiwać dostęp do poszczególnych wartości poprzez klucze. Zarówno klucz, jak i wartość może być dowolnego rodzaju. Możemy się domyślać, że tablice TCL są w jakiś sposób optymalizowane w kierunku szybkości dostępu do wartości o podanym kluczu nawet wobec dużej liczby pozycji w tablicy.

Tablice nie mogą być przekazywane jako parametry funkcji itp. Nie można pobierać ani kopiować zmiennej tablicowej jako całości. Wynika z tego, że przed nazwą zmiennej tablicowej nigdy nie stawiamy znaku dolara chyba, że chodzi nam o wartość konkretnego jej elementu.

Zmienna może być albo zwykła, przechowująca pojedynczą wartość, albo może być tablicą. Potraktowanie zwykłej zmiennej jako tablicy i odwrotnie zakończy się błędem. Zmienna nieistniejąca (niezainicjalizowana) nie jest tablicą i nie można odczytywać z niej jak z tablicy, dopóki nie zostanie ona tablicą w wyniku instrukcji przypisania:

Zapisywanie elementów tablicy:

set aTablica(Programowanie) "ciekawe"
set aTablica(TCL) "prosty"

Jak widać, do poszczególnych elementów tablicy odwołujemy się podając klucz za nazwą zmiennej tablicowej ujęty w nawiasy okrągłe.

Uwaga! Klucz, którego nazwa w nawiasie ujęta zostanie w cudzysłowy potraktowany zostanie dosłownie - jako zawierający te cudzysłowy - i będzie to inny klucz niż nie ujęty w nie.

Aby odczytać wartość elementu tablicy o podanym kluczu, użyj konstrukcji podobnej do tej:

Odczytanie z tablicy:

Debug $aTablica(Programowanie)

Polecenie unset można stosować do tablic na 2 sposoby:

Zastosowania unset:

unset aTablica(Programowanie)
unset aTablica

Pierwsze polecenia powoduje usunięcie elementu o podanym kluczu z tablicy. Drugie usuwa całą zmienną tablicową.

Tablica (ani żadna inna wartość) nie może zawierać w środku drugiej tablicy. Niemożliwe jest więc tworzenie tablic wielowymiarowych inaczej, niż przez ich reprezentację jednowymiarową lub przez użycie zagnieżdżonych list. Niemożliwa jest konstrukcja w rodzaju: $aTablica(2)(3).

3.9.1. Operacje na tablicach

Do operowania na tablicach służy polecenie array. Niektóre jego możliwości to:

array exists <tablica>
Zwraca 1 lub 0 zależnie, czy podana zmienna istnieje i jest tablicą. Należy tego używać przed próbą odczytania z tablicy szczególnie jeśli nie ma pewności, że tablica nie jest pusta (bo wtedy taka zmienna nie zostanie uznana jako tablicowa)!
array names <tablica>
Zwraca listę zawierającą wszystkie klucze tablicy.
array size <tablica>
Zwraca liczbę elementów tablicy.

Dużo więcej dostępnych parametrów polecenia array znajdziesz w jego dokumentacji.

3.9.2. Przykład

Przykład tablicy:

set aTablica(Programowanie) "ciekawe"
set aTablica(TCL) "prosty"

foreach {sKlucz} [array names aTablica] {
  Debug "$sKlucz jest $aTablica($sKlucz)"
}

Pętlę foreach poznamy już wkrótce, w następnej części. Powyższy kod spowoduje wypisanie:

Wynik:

[DEBUG] Programowanie jest ciekawe
[DEBUG] TCL jest prosty

3.9.3. Tablice kontra listy

Tablice i listy to dwa sposoby na przechowywanie wielu informacji w jednej zmiennej. Czasami można stanąć przed koniecznością wyboru, którego lepiej użyć. Aby to ułatwić, prezentuję krótkie porównanie.

Organizacja danych
Listy - kolejno następujące po sobie elementy
Tablice - pary klucz - wartość
Kolejność
Listy - zachowana
Tablice - nie zachowana
Wydajność
Możemy się domyślać, że dostęp do elementu tablicy o podanym kluczu jest szybki. O szybkości dostępu do wskazanego elementu listy trudno coś powiedzieć. W przypadku częstego przechodzenia wszystkich elementów tablicy i tak posługujemy się listą.
Elastyczność
Listy - można przekazywać jako parametry procedur, zwracać itp.
Tablice - nie można tego robić

3.10. Data i czas

Data i czas reprezentowana jest w TCL przez liczbę sekund, jakie upłynęły od jakiegoś tam umówionego momentu w przeszłości. Dzięki temu wartości tego typu można do siebie dodawać i odejmować od siebie, a także zwiększać i zmniejszać o podaną liczbę sekund.

Do operacji na wartościach czasowych służą 3 odmiany polecenia clock:

clock seconds
Zwraca aktualny czas.
clock format <czas> -format <format>
Zmienia wartość czasową na łańcuch wg podanego formatu. Format to łańcuch zawierający miejsca na poszczególne elementy daty i czasu, np. %A to nazwa dnia tygodnia, a %H to godzina.
clock scan <łańcuch>
Parsuje podany łańcuch na datę automatycznie próbując rozpoznać jego format. Wystarczy powiedzieć, że rozpoznany zostanie nie tylko tradycyjny format np. 2003-09-02 12:54:03, ale nawet słowo yesterday.

Po szczegółowy opis tego elastycznego polecenia odsyłam do dokumentacji TCL.

3.10.1. Przykład

Przykład daty i czasu:

Debug "Jutro będzie: [clock format [expr [clock seconds] + 60*60*24] -format "%Y-%m-%d %H:%M:%S"]

Powyższy kod wypisuje datę i czas, jaka będzie dokładnie za 24 godziny od chwili wykonania tej instrukcji.

3.11. Notacja węgierska

Zapewne zwróciłeś uwagę, że w dotychczasowym kodzie używałem tajemniczych przedrostków przed nazwami zmiennych. Teraz, kiedy znamy już wszystkie rodzaje danych TCL, możemy to wyjaśnić.

Notacja węgierska to pewien nieoficjalny standard nakazujący poprzedzanie nazw zmiennych i innych identyfikatorów podczas programowania przedrostkami wskazującymi na ich typ oraz zakres. Zdania na temat jego przydatności są podzielone.

Jedni twierdzą, że poprawia on czytelność kodu. Inni zaś, że to dodatkowy niepotrzebny kłopot. Faktycznie w językach programowania z kontrolą typów korzyści z używania notacji węgierskiej mogą być wątpliwe. W językach skryptowych takich jak TCL, szczególnie podczas pisania większych skryptów, warto jednak ją stosować. Ostateczny wybór należy do ciebie.

Przedstawię teraz mój pomysł na stosowanie notacji węgierskiej w skryptach TCL. Ogólny schemat konstrukcji nazwy zmiennej może wyglądać tak:

Konstrukcja nazwy zmiennej:

<zakres>_<typ><nazwa>

Zakresem może być:

g
(od ang. "global") Zmienna globalna
m
(od ang. "member") Zmienna składowa strefy nazw
a
(od ang. "argument") Parametr procedury
(brak)
Zmienna lokalna
c
(od ang. "const") Stała, czyli zmienna, której wartości nie będziemy modyfikować

Typem może być:

h
(od ang. "handle") Uchwyt
b
(od ang. "boolean") Wartość logiczna
i
(od ang. "integer") Liczba całkowita
f
(od ang. "floating point") Liczba rzeczywista
s
(od ang. "string") Łańcuch znaków
c
(od ang. "char") Pojedynczy znak, czyli łańcuch zawsze o długości 1
l
(od ang. "list") Lista
a
(od ang. "array") Tablica
t
(od ang. "time") Data i czas
(brak)
Typ nieznany lub dowolny

Rozważ następujący przykład:

Przykład zastosowania notacji węgierskiej:

namespace eval ble {
  set m_iLiczba 0

  proc SetLiczba {a_iLiczba} {
    variable m_iLiczba
    set m_iLiczba $a_iLiczba
  }
}

3.12. PROJEKT - heap

TCL jest językiem skryptowym i nie posiada wskaźników, referencji ani żadnych innych możliwości tego rodzaju. Tymczasem dynamiczna alokacja i zwalnianie pamięci takie jak w językach kompilowanych przydałoby się czasem do tworzenia zaawansowanych struktur danych takich, jak drzewa czy grafy. Spróbujemy napisać w TCL własny system dynamicznej alokacji pamięci.

Heap to po angielsku sterta. Jak podaje dokumentacja Windows, sterta to dostępne dla procesu miejsce w pamięci, na którym może on alokować bloki, używać ich przechowując w nich dane dowolnego rodzaju i na koniec zwalniać je. Każdy blok pamięci identyfikowany jest przez wskaźnik, który zachowuje się, mówiąc w przybliżeniu, podobnie jak poznane przez nas uchwyty.

Rozwiązaniem może być użycie tablicy. Wskaźniki do zaalokowanych bloków pamięci będą kluczami tablicy, co zapewni do nich szybki dostęp. Alokacja będzie polegała na dodaniu nowego elementu do tablicy, a zwolnienie bloku na usunięciu elementu z tablicy.

Pozostaje jeszcze kwestia wyboru sposobu generowania tych kluczy - wskaźników. Najprościej będzie podawać kolejne liczby całkowite.

Oprócz dostępu do zaalokowanych bloków w celu zapisania i odczytania całej zawartości, dodamy także odpowiedniki poleceń append i lappend, by zapewnić jak największą wydajność kodu używającego naszego modułu.

Razem z tym kodem należałoby rozszerzyć przedstawioną wyżej notację węgierską o nowy przedrostek typu: p (od ang. "pointer") - wskaźnik.

heap.tcl:

namespace eval mem {
	# ===== PRIVATE =====

	# W tej tablicy przechowywane będą zaalokowane bloki pamięci
	# Struktura: [int] Wskaźnik > [any] Dane
	#   m_aHeap

	# Ta zmienna to licznik do generowania nowych wskaźników
	# Wartością jest numer wskaźnika, który zostanie zaalokowany
	#   w najbliższej alokacji
	set m_iCounter 0

	# ===== PUBLIC =====

	# Alokuje nowy blok pamięci i zwraca wskaźnik do niego
	proc new { } {
		variable m_aHeap
		variable m_iCounter

		set iResult $m_iCounter
		set m_aHeap($iResult) ""
		incr m_iCounter
		return $iResult
	}

	# Zwalnia zaakolowany wcześniej blok pamięci o podanym wskaźniku
	proc delete {p} {
		variable m_aHeap

		unset m_aHeap($p)
	}

	# Pozwala zapisać dane do pamięci o podanym wskaźniku
	proc write {p data} {
		variable m_aHeap

		set m_aHeap($p) $data
	}

	# Pozwala odczytać pamięć spod podanego wskaźnika
	proc read {p} {
		variable m_aHeap

		return $m_aHeap($p)
	}

	# Zwraca liczbę zaalokowanych bloków pamięci
	proc count { } {
		variable m_aHeap

		return [array size m_aHeap]
	}

	# Dodaje element do listy przechowywanej pod podanym wskaźnikiem
	proc append_list {p value} {
		variable m_aHeap

		lappend m_aHeap($p) $value
	}

	# Dołącza łańcuch do łańcucha przechowywanego pod podanym wskaźnikiem
	proc append_string {p value} {
		variable m_aHeap

		append m_aHeap($p) $value
	}
}
Adam Sawicki
[Download] [Dropbox] [pub] [Mirror] [Privacy policy]
Copyright © 2004-2021