Spis treści

Przeciążanie podstawowych metod

Przeciążanie konstruktora

Obiekty bez obiektów potomnych

Konstruktor obiektu (metoda OM_NEW()) ma tę samą strukturę parametrów opSet co metoda OM_SET(). Struktura ta zawiera pole ops_AttrList będące wskaźnikiem na taglistę zawierającą początkowe wartości atrybutów obiektu. Implementacja konstruktora dla obiektu nie zawierającego obiektów potomnych jest raczej prosta. Na początku wywołuje się konstruktor klasy nadrzęnej. Jeżeli zwróci on wskaźnik na obiekt, konstruktor inicjalizuje dane obieku, alokuje potrzebne zasoby (np. bufory w pamięci) i ustawia początkowe wartości atrybutów zgodnie z tagami przekazanymi w ops_AttrList.

Najważniejszą zasadą przy przeciążaniu konstruktorów jest nie zostawianie nigdy częściowo skonstruowanego obiektu. Konstruktor powinien zwrócić albo kompletny i całkowicie zaincjalizowany obiekt, albo zakończyć się niepowodzeniem, ale przedtem zwrócić wszystkie te zasoby, które udało się mu zarezerwować. Jest to szczególnie istotne, jeżeli obiekt uzyskuje przydział więcej niż jednego zasobu i którykolwiek z przydziałów zakończy się niepowodzeniem (przykładowo alokacja dużego obszaru pamięci albo otwarcie pliku). W przykładzie poniżej obiekt usiłuje zarezerwować trzy zasoby nazwane umownie A, B i C.

IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
{  
  if (obj = DoSuperMethodA(cl, obj, (Msg)msg))
  {
    struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

    if ((d->ResourceA = ObtainResourceA()
     && (d->ResourceB = ObtainResourceB()
     && (d->ResourceC = ObtainResourceC())
    {
      return (IPTR)obj;    /* sukces */
    }
    else CoerceMethod(cl, obj, OM_DISPOSE);
  }
  return NULL;
}

Jeżeli destruktor obiektu zwalnia zasoby A, B i C (co byłoby logiczne, skoro są rezerwowane w konstruktorze), oczyszczanie po nieudanej konstrukcji można wykonać przy jego użyciu. Taki destruktor musi być jednak przygotowany na fakt, że może mieć do czynienia z częściowo skonstruowanym obiektem. Nie może zakładać, że wszystkie zasoby do zwolnienia na pewno zostały przydzielone. Najcześciej oznacza to sprawdzenie każdego wskaźnika do zasobu na okoliczność wartości zerowej (lub innej, jeżeli zero jest prawidłowym identyfikatorem zasobu). Destruktor wywołuje następnie destruktor klasy nadrzędnej. Przykłady destruktorów z objaśnieniami znajdują się w rozdziale „Przeciążanie destruktora”.

Pozostaje jeszcze otwarta kwestia funkcji CoerceMethod(). Jakie jest jej działanie i dlaczego została tu użyta zamiast zwykłego DoMethod()? Funkcja ta wykonuje metodę na obiekcie, tak samo jak DoMethod(), ale wykonuje tzw. „wymuszenie” (ang. coercion) wykonania metody ściśle określonej klasy, poprzez bezpośredni skok do jej dispatchera, zamiast do rzeczywistej klasy obiektu. Jeżeli obiekt jest klasy pochodnej względem tej, której konstruktor przeciążamy, to będą to oczywiście dwie różne klasy. Diagram poniżej ilustruje problem:


Wyjaśnienie działania funkcji CoerceMethod().

Klasa B na rysunku jest klasą pochodną klasy A, klasa C jest klasą pochodną B. Załóżmy, że konstruujemy obiekt klasy C. Ponieważ każdy konstruktor zaczyna pracę od wywołania konstruktora klasy nadrzędnej, łańcuch wywołań dociera do klasy rootclass (klasy głównej wszystkich klas BOOPSI). Następnie wywołania schodzą w dół drzewa klas, konstruktory tych klas inicjalizują swoje części obiektu i rezerwują zasoby. Niestety okazało się, że konstruktor klasy A nie dostał wszystkich oczekiwanych zasobów i zakończył się niepowodzeniem. Gdyby w tym wypadku po prostu wywołał destruktor przez DoMethod(obj, OM_DISPOSE), niepotrzebnie wywołałby destruktory klas B i C, mimo tego, że ich konstruktory nie zostały jeszcze wykonane. Nawet jeżeli konstruktory te potrafią sobie poradzić z taką sytuacją, wywoływanie ich jest całkowicie zbędne. Uruchomienie destruktora funkcją CoerceMethod() powoduje, że od razu wywoływany jest destruktor w klasie A. Po zwolnieniu tych zasobów, do których dało się uzyskać dostęp, konstruktor klasy A zwraca NULL, co z kolei powoduje natychmiastowe zakończenie wykonywania się konstruktorów klas B i C, również z wynikiem zerowym, bez próby inicjalizacji obiektu i alokacji zasobów.

Obiekty z obiektami potomnymi

Konstruktor klasy, która dodaje obiektowi obiekty potomne, jest napisany zgodnie z podstawowymi zasadami omówionymi powyżej, ale pewne szczegóły są inne. Z klas standardowych MUI mogących posiadać obiekty potomne najcześciej klasy pochodne tworzy się od klasy Group i Application (obiektami potomnymi aplikacji są okna). Bardzo często używa się również klas pochodnych od Window, z tym, że obiekt tej klasy posiada tylko jeden obiekt potomny, klasy Group, specyfikowany atrybutem MUIA_Window_RootObject. Oczywiście ten obiekt posiada z reguły liczne podobiekty, mianowicie całą zawartość okna. Konstruktor powinien najpierw przystąpić do tworzenia obiektów potomnych a dopiero potem wywołać konstruktor klasy nadrzędnej. Jeżeli jego wywołanie zakończy się sukcesem, konstruktor wykonuje inicjalizację danych i stanu obiektu oraz rezerwuje potrzebne zasoby. Ponieważ każda z faz konstruktora może zakończyć się niepowodzeniem, prawidłowa obsługa błędów staje się dość skomplikowana. Dodatkowo wstawianie wskaźników stworzonych podobiektów do taglisty (jako wartości atrybutów takich jak MUIA_Group_Child) jest niezbyt wygodne. Na szczęście zadania te upraszcza funkcja DoSuperNew(), która łączy tworzenie obiektów potomnych i wywołanie konstruktora klasy nadrzędnej w jedną operację. Zapewnia również automatyczną obsługę błędów przy konstrukcji obiektów potomnych. Poniższy przykład demonstruje konstruktor klasy pochodnej od Group tworzącej dwa obiekty tekstowe (klasa Text):

IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
{  
  if (obj = DoSuperNew(cl, obj,
    MUIA_Group_Child, MUI_NewObject(MUIC_Text,
      /* atrybuty dla pierwszego podobiektu */
    TAG_END),
    MUIA_Group_Child, MUI_NewObject(MUIC_Text,
      /* atrybuty dla drugiego podobiektu */
    TAG_END),
  TAG_MORE, msg->ops_AttrList)) 
  {
    struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

    if ((d->ResourceA = ObtainResourceA()
     && (d->ResourceB = ObtainResourceB()
     && (d->ResourceC = ObtainResourceC())
    {
      return (IPTR)obj;    /* sukces */
    }
    else CoerceMethod(cl, obj, OM_DISPOSE);
  }
  return NULL;
}

Warto zauważyć, że funkcja DoSuperNew() łączy taglistę przekazaną konstruktorowi w polu ops_AttrList struktury parametrów metody z taglistą zbudowaną ze swoich argumentów. Robi się to za pomoca specjalnego taga TAG_MORE, który przekierowuje iterator taglisty (taki jak na przykład funkcja NextTagItem()) do następnej części znajdującej się pod adresem określonym przez wartość tego taga. Łączenie taglist pozwala na modyfikowanie obiektu tagami podanymi w wywołaniu konstruktora, na przykład dodanie ramki lub tła do grupy z powyższego przykładu.

Automatyczna obsługa błędów przy tworzeniu obiektów potomnych działa następująco: jeżeli konstruktor któregokolwiek z podobiektów zwróci NULL, wskaźnik ten umieszczany jest w budowanej na stosie tagliście jako wartość taga (np. MUIA_Group_Child). Wszystkie standardowe klasy MUI, które mogą posiadać obiekty potomne zaprojektowane są w sposób taki, że:

Ponieważ wynik funkcji DoSuperNew() jest wynikiem działania konstruktora klasy nadrzędnej, funkcja ta również zwróci zerowy wskaźnik. W ten sposób w przypadku jakiegokolwiek błędu przy budowaniu drzewa obiektów aplikacji wszystkie stworzone obiekty zostaną prawidłowo zniszczone, bez pozostawiania obiektów "osieroconych".

Przeciążanie destruktora

Jedynym zadaniem destruktora jest zwolnienie zasobów zarezerwowanych przez konstruktor i inne metody (niektóre zasoby mogą być alokowane na żądanie). W każdym przypadku destruktor musi zostawić obiekt w takim stanie jak zaraz po wywołaniu DoSuperMethod()/DoSuperNew() w konstruktorze. Następnie destruktor wywołuje destruktor w klasie nadrzędnej. Struktura parametrów destruktora jest pusta (zawiera tylko identyfikator metody).

IPTR MyClassDispose(Class *cl, Object *obj, Msg msg)
{
  struct MyClassData *d = (struct MyClassData*)INST_DATA(cl, obj);

  if (d->ResourceA) FreeResourceA();
  if (d->ResourceB) FreeResourceB();
  if (d->ResourceC) FreeResourceC();
  return DoSuperMethodA(cl, obj, msg);
}

Przykładowy destruktor powyżej jest kontynuacją przykładu konstruktora w artykule „Przeciążanie konstruktora”. Zwalnia trzy zasoby przydzielane w konstruktorze. Destruktor jest przygotowany na sytuację, gdy obiekt jest jedynie częściowo skonstruowany, wskaźnik na każdy zasób jest sprawdzany na okoliczność wartości zerowej. Jeżeli dla jakiegoś typu zasobów zero jest poprawnym identyfikatorem, najprościej dodać do danych obiektu flagę, która będzie pamiętała alokację danego zasobu.

Przeciążanie metody OM_SET()

Metoda OM_SET() jako strukturę parametrów otrzymuje strukturę opSet. Jest ona zdefiniowana w pliku nagłówkowym <intuition/classusr.h>.

struct opSet
{
  ULONG              MethodID;            /* zawsze OM_SET (0x103) */
  struct TagItem    *ops_AttrList;
  struct GadgetInfo *ops_GInfo;
};

Najważniejszym polem struktury jest ops_AttrList. Mieści ono w sobie wskaźnik do taglisty zawierającej atrybuty i ich wartości do ustawienia w obiekcie. Pole ops_GInfo jest historyczną pozostałością i nie jest używane przez nowoczesne elementy systemu takie jak MUI, czy Reggae. Implementacja metody OM_SET() powinna przejrzeć po kolei całą taglistę i ustawić wszystkie rozpoznane atrybuty. Operacja ustawienia atrybutu może się sprowadzać do ustawienia jakiejś wartości w danych obiektu, może też uruchamiać jakieś akcje (na przykład odrysowanie obiektu). Zaleca się jednak, aby bardziej złożone akcje implementować raczej jako metody niż zmiany atrybutu. Wzorcowa metoda OM_SET() może wyglądać następująco:

IPTR MyClassSet(Class *cl, Object *obj, struct opSet *msg)
{
  struct TagItem *tag, *tagptr;
  IPTR tagcount = 0;

  tagptr = msg->ops_AttrList;

  while ((tag = NextTagItem(&tagptr)) != NULL)
  {
    switch (tag->ti_Tag)
    {
      case SOME_TAG:
        /* kod zmieniający atrybut SOME_TAG */
        tagcount++;
      break;

      /* inne atrybuty */
    }
  }

  tagcount += DoSuperMethodA(cl, obj, (Msg)msg);
  return tagcount;
}

Taglista jest iterowana za pomocą funkcji NextTagItem() z utility.library. Przy każdym wywołaniu funkcja zwraca wskaźnik na kolejny element taglisty. Aktualna pozycja pamiętana jest w zmiennej tagptr. Zaletą użycia tej funkcji jest automatyczna obsługa tagów kontrolnych (TAG_MORE, TAG_IGNORE, TAG_SKIP), nie są one zwracane jako kolejne elementy, za to wykonywane są odpowiadające im operacje.

Metoda OM_SET() powinna jako wynik zwracać łączną ilość rozpoznanych i ustawionych atrybutów. Ich zliczanie odbywa się w zmiennej tagcounter. Zmienna jest zwiększana o 1 przy każdym rozpoznanym tagu, a następnie powiększana o ilość atrybutów rozpoznanych w klasach nadrzędnych.

Typowe błędy w implementacji OM_SET() to:

W rzadkich przypadkach klasa pochodna może chcieć całkowicie przejąć jakiś atrybut, tak, aby nie był wysłany do klas nadrzędnych. Można to zrobić zastępując identyfikator atrybutu w tagliście przez TAG_IGNORE. Jest tu jednak pewien haczyk. Najczęściej w C i C++ taglista tworzona jest dynamicznie na stosie procesu z argumentów funkcji (np. SetAttrs()). Jest jednak możliwe, że taglista będzie obiektem statycznym (na przykład globalnym, albo stworzonym w zaalokowanym bloku pamięci). W tym przypadku zmiana taga na TAG_IGNORE będzie operacją trwałą, co może skutkować nieoczekiwanymi efektami. Uwaga ta dotyczy również zmiany wartości atrybutu, przed przekazaniem taglisty klasie nadrzędnej. Bezpiecznym rozwiązaniem jest sklonowanie taglisty funkcją CloneTagItems() z utility.library. Następnie dokonuje się zmian w otrzymanej kopii i kopię tę przekazuje klasie nadrzędnej. Po powrocie z DoSuperMethodA() kopię zwalnia się wywołując funkcję FreeTagItems(). Wadą tego rozwiązania jest możliwość wystąpienia błędu przy klonowaniu taglisty z powodu braku pamięci. Możliwość tę trzeba jakoś obsłużyć w kodzie.

Przeciążanie metody OM_GET()

Metoda OM_GET() służy do odczytywania wartości atrybutu obiektu. Otrzymuje strukturę opGet jako strukturę parametrów. Struktura opGet jest zdefiniowana w pliku nagłówkowym <intuition/classusr.h>:

struct opGet
{
  ULONG  MethodID;           /* zawsze OM_GET (0x104) */
  ULONG  opg_AttrID;
  ULONG *opg_Storage;
};

W przeciwieństwie do OM_SET(), ta metoda w jednym wywołaniu odczytuje zawsze jeden atrybut. Jego identyfikator umieszcza się w polu opg_AttrID. Pole opg_Storage jest wskaźnikiem na miejsce w pamięci, gdzie zostanie umieszczona odczytana wartość atrybutu. Pole to jest zdefiniowane jako wskaźnik na ULONG, ale może wskazywać na cokolwiek (na przykład jakąś większą strukturę). Dzięki temu można odczytywać wartości nie mieszczące się w 32 bitach. Ponieważ metoda OM_GET() nie ma pętli przeglądania taglisty, jej implementacja jest prosta:

IPTR MyClassGet(Class *cl, Object *obj, struct opGet *msg)
{
  switch (msg->opg_AttrID)
  {
    case Tag_Liczbowy:
      *msg->opg_Storage = /* wartość atrybutu */;
    return TRUE;

    case Tag_Tekstowy:
      *(char**)msg->opg_Storage = "stały tekst";
    return TRUE;
  }

  return DoSuperMethodA(cl, obj, (Msg)msg);
}

Funkcja metody OM_GET() składa się z wyrażenia switch z przypadkami dla wszystkich rozpoznawanych atrybutów. Jeżeli atrybut jest rozpoznany, wynikiem funkcji powinna być wartość logiczna TRUE. To bardzo ważne, ponieważ notyfikacje nie będą działały na danym atrybucie jeżeli OM_GET() zwróci po prostu zero. Nierozpoznane atrybuty są przekazywane do klasy nadrzędnej. Wywołanie funkcji DoSuperMethodA() może być alternatywnie umieszczone w przypadku default wyrażenia switch. Trzeba zwrócić uwagę na fakt, że przy zapisywaniu wartości atrybutu odwołujemy się do wartości wskazywanej przez msg->opg_Storage. Jeżeli typem wartości nie jest liczba całkowita, niezbędne jest rzutowanie typu. Dla wartości typu T, odwołanie się do niej wraz z rzutowaniem jest zapisywane w C jako *(T*).