Przeciążanie konstruktorów

From MorphOS Library

Grzegorz Kraszewski


Ten artykuł w innych językach: angielski


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;    /* success */
    }
    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 destruktorów".

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:


Coercemethod pl.png


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:

  • Konstruktor zwraca NULL, jeżeli wskaźnik na którykolwiek z przekazanych obiektów potomnych jest równy NULL.
  • Konstruktor usuwa wszystkie pomyślnie stworzone obiekty potomne przed wyjściem.

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".