Difference between revisions of "Przeciążanie konstruktorów"

From MorphOS Library

(Translation in progress.)
m (Link switched to Polish version.)
 
(12 intermediate revisions by the same user not shown)
Line 6: Line 6:
 
==Obiekty bez obiektów potomnych==
 
==Obiekty bez obiektów potomnych==
  
Konstruktor obiektu (metoda ''OM_NEW()'') ma tę samą strukturę parametrów ''opSet'' co metoda ''[[Overriding OM_SET()|OM_SET()]]''. Struktura ta zawiera pole ''ops_AttrList'' będące wskaźnikiem na [[Taglists|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''.
+
Konstruktor obiektu (metoda ''OM_NEW()'') ma tę samą strukturę parametrów ''opSet'' co metoda ''[[Przeciążanie metody OM_SET()|OM_SET()]]''. Struktura ta zawiera pole ''ops_AttrList'' będące wskaźnikiem na [[Taglists|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''.
  
A rule of thumb when overriding constructors is to '''never leave a half-constructed object'''. The constructor should either return a fully constructed object, or fail completely, freeing all successfully obtained resources. This is important if the object obtains more than one resource and any of the resource allocation has failed (for example allocating a big chunk of memory or opening a file). An example implementation below obtains three resources: ''A'', ''B'' and ''C'':
+
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)
 
  IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
Line 27: Line 27:
 
  }
 
  }
  
If the object destructor frees resources  ''A'', ''B'' and ''C'' (which would be logical considering the constructor allocates them), the cleanup job may be delegated to the destructor. It requires however, that the destructor must be prepared for destruction of a not fully constructed object. It can't assume all three resources have been allocated, so it should check every resource pointer against ''NULL'' before calling a freeing function. The destructor also takes care of calling a superclass destructor when resources are freed. See [[Overriding Destructors]] for some destructor example code and explanation.
+
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]]".
  
The only question remaining is what ''CoerceMethod()'' does and why it is used instead of a plain ''DoMethod()''? The ''CoerceMethod()'' call works exactly the same as ''DoMethod()'', but performs '''method coercion''' by a forced call to the dispatcher of the class specified as the first argument instead of the dispatcher of the object's true class. It makes a difference, when the class in question is later subclassed. The flowchart below explains the problem:
+
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:
  
  
[[File:Coercemethod.png|center]]
+
[[File:Coercemethod_pl.png|center]]
  
  
The class ''B'' on the diagram is a subclass of the class ''A'' and similarly, the class ''C'' is a subclass of ''B''. Let's assume an object of the class ''C'' is being constructed. As every constructor calls the superclass first, the call goes up to ''rootclass'' (the root of all BOOPSI classes) first. Then going down the class tree, every class constructor allocates its resources. Unfortunately the constructor of class ''A'' has been unable to allocate one of its resources and decided to fail. If it had just called ''DoMethod(obj, OM_DISPOSE)'' it will unnecessarily execute destructors in classes ''B'' and ''C'', while constructors in these classes have been not yet fully executed. Even if these destructors can cope with this, calling them is superfluous. With the ''CoerceMethod()'' the destructor in the class ''A'' is called directly. Then class ''A'' constructor returns NULL, which causes constructors in classes ''B'' and ''C'' to fail immediately without resource allocation attempts.
+
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==
 
==Obiekty z obiektami potomnymi==
  
While retaining the same principles, the constructor of an object with subobjects is designed a bit differently. The most commonly subclassed classes able to have child objects are ''Application'', and ''Group''. The ''Window'' class is also often subclassed in a similar way. While a ''Window'' object can have only one child, specified by ''MUIA_Window_RootObject'', this child often has multiple subobjects. The constructor should create its child objects first, then insert them into the ''ops_AttrList'' [[Taglists|taglist]] and call the superclass constructor. If it succeeds, then resources may be allocated if needed. As any of the three constructor stages may fail, proper handling of errors becomes complicated. Also inserting objects created into the taglist as values of child tags (like ''MUIA_Group_Child'') is cumbersome. Fortunately one can use the ''DoSuperNew()'' function, which merges the creation of subobjects and the calling of the superclass into one operation. It also provides automatic handling of failed child object construction. An example below is a constructor for a ''Group'' subclass putting two ''Text'' objects in the group.
+
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)
 
  IPTR MyClassNew(Class *cl, Object *obj, struct opSet *msg)
Line 46: Line 46:
 
   if (obj = DoSuperNew(cl, obj,
 
   if (obj = DoSuperNew(cl, obj,
 
     MUIA_Group_Child, MUI_NewObject(MUIC_Text,
 
     MUIA_Group_Child, MUI_NewObject(MUIC_Text,
       /* attributes for the first subobject */
+
       /* atrybuty dla pierwszego podobiektu */
 
     TAG_END),
 
     TAG_END),
 
     MUIA_Group_Child, MUI_NewObject(MUIC_Text,
 
     MUIA_Group_Child, MUI_NewObject(MUIC_Text,
       /* attributes for the second subobject */
+
       /* atrybuty dla drugiego podobiektu */
 
     TAG_END),
 
     TAG_END),
 
   TAG_MORE, msg->ops_AttrList))  
 
   TAG_MORE, msg->ops_AttrList))  
Line 59: Line 59:
 
       && (d->ResourceC = ObtainResourceC())
 
       && (d->ResourceC = ObtainResourceC())
 
     {
 
     {
       return (IPTR)obj;    /* success */
+
       return (IPTR)obj;    /* sukces */
 
     }
 
     }
 
     else CoerceMethod(cl, obj, OM_DISPOSE);
 
     else CoerceMethod(cl, obj, OM_DISPOSE);
Line 66: Line 66:
 
  }
 
  }
  
An important thing to observe is the fact, that ''DoSuperNew()'' '''merges''' the taglist passed to the constructor via the message ''ops_AttrList'' field and the one specified in the function arguments list. It is done with a special ''TAG_MORE'' tag, which directs a taglist iterator (like ''NextTagItem()'' function) to jump to another taglist pointed by the value of this tag. Taglist merging allows for modifying the object being constructed with tags passed to ''NewObject()'', for example adding a frame or background to the group in the above example.
+
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 [[Taglists|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.
  
The automatic handling of failed child objects works in the following way: when a subobject fails, its constructor returns ''NULL''. This ''NULL'' value is then inserted as the value of a "child" tag (''MUIA_Group_Child'') in the example. All MUI classes able to have child objects are designed in a way that:
+
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:
* the constructor fails if any "child" tag has a ''NULL'' value,
+
* Konstruktor zwraca ''NULL'', jeżeli wskaźnik na którykolwiek z przekazanych obiektów potomnych jest równy ''NULL''.
* the constructor disposes any successfully constructed child objects before exiting.
+
* Konstruktor usuwa wszystkie pomyślnie stworzone obiekty potomne przed wyjściem.
Finally ''DoSuperNew()'' returns ''NULL'' as well. This design ensures that in case of any fail while building the application, all objects created are disposed and there are no orphaned ones.
+
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".

Latest revision as of 08:49, 26 January 2011

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