Klasa jako tablica

0

Być może ten temat nadaje się do działu Newbie, jeżeli tak, to proszę przenieść.

Chciałbym uzyskać odpowiedź, czy w Delphi można stworzyć klasę, która zachowuje się jak tablica?

Zamiast:

TKlasa = class
Dane: array of Integer;
end;

var
Klasa: TKlasa

begin
Klasa:= TKlasa.Create;
Klasa.Dane[0]:= 120;
end;

chciałbym móc zrobić tak:

var
Klasa: TKlasa

begin
Klasa:= TKlasa.Create;
Klasa[0]:= 120;
end;

Tak to działa np. w TList z biblioteki Generics, ale nie wiem jak osiągnąć taki efekt.

2

@Crow: najpierw musisz zaalokować pamięć dla wewnętrznej macierzy – za pomocą SetLength – aby móc z niej korzystać. Wywołanie konstruktora klasy jedynie alokuje pamięć dla obiektu.

0

Dzięki za odpowiedzi.

A czy jest możliwe stworzenie indeksowanej właściwości wielowymiarowej, do której mógłbym się odwoływać przez

Klasa[0, 0]:= 123;

?

Ogólnie cel mam taki, że chcę sobie stworzyć typ stanowiący tablicę dwuwymiarową (a konkretniej macierz), która sama się wypełnia danymi, w zależności od podanych parametrów. Np. mam jakiś wektor [X, Y, Z, W] i chcę go przemnożyć przez jedną z wcześniej zaprojektowanych macierzy, powiedźmy macierz obrotu. Nie chcę tego robić tak:

var
MacierzObrotu: TMacierz; //jest to array of array of Double;

begin
WypełnijMacierzDanymi(MacierzObrotu, JakiesParametry);
NowaMacierz:= PomnozMacierze(MacierzObrotu, Wektor);
end;

Tylko tak:

NowaMacierz:= PomnozMacierze(TMacierz.Create(JakieśParametry), Wektor));

Czyli zamiast tworzyć zmienną wskazującą na TMacierz i wypełniać ją określonymi danymi (za pomocą odrębnej procedury), chciałbym móc stworzyć sobie taką macierz w locie, podając ją jako parametr funkcji (sama macierz byłaby wypełniana odpowiednimi danymi w konstruktorze Create).

Może w ogóle zabieram się za to od złej strony i można to rozwiązać inaczej?

0

Przynajmniej dwa wymiary chyba jest możliwe teraz nie mam czasu się w to zagłębiać jak ale o ile mnie pamięć nie myli to choćby TStringGrid ma ten sposób zaimplementowaną właściwość Cells gdzie podaje się index X i Y komórki.

4

Jeśli istnieją ograniczenia związane z liczbą indeksów w właściwości indeksowanej, to są to ograniczenia związane z liczbą parametrów zwykłej metody. Pewnie jakieś istnieją, ale jest to liczba dość duża, której w zwykłym kodzie się raczej nie osiągnie. No bo kto by potrzebował metody przyjmującej np. 50 parametrów? ;)

Przykład:

type
  TSome = class(TObject)
  private
    function GetFoo(A, B, C, D, E, F, G: Integer): Integer;
    procedure SetFoo(A, B, C, D, E, F, G: Integer; AValue: Integer);
  public
    property Foo[A, B, C, D, E, F, G: Integer]: Integer read GetFoo write SetFoo;
  end;

Użycie takiej właściwości:

var
  Some: TSome;
begin
  Some := TSome.Create();
  Some.Foo[0, 1, 2, 3, 4, 5, 6] := 100;

Jak widać, właściwość indeksowana siedmioma wartościami może istnieć (sens jej istnienia to inna bajka). Tutaj warto zaznaczyć, że indeksy właściwości indeksowanej nie mają nic wspólnego z wymiarami. Można powiedzieć, że właściwość Foo jest jedynie krótkim wrapperem na metody GetFoo i SetFoo, a nawet cukrem składniowym, a jej indeksy to po prostu parametry tych metod. Tak więc aby móc korzystać z takiej właściwości, nie musisz mieć wielowymiarowej macierzy, z której czyta getter i którą modyfikuje setter.


Druga sprawa to typy danych parametrów indeksowanej właściwości. Nie muszą to być indeksy tego samego typu, bo tak jak napisałem wyżej, właściwość indeksowana to tylko wrapper na konkretne metody. Skoro metoda może posiadać parametry różnego typu, to i właściwość może przyjmować indeksy różnego typu.

Drugi przykład:

type
  TSome = class(TObject)
  private
    function GetFoo(A: Integer; B: Boolean; C: String; D: TClass): Double;
    procedure SetFoo(A: Integer; B: Boolean; C: String; D: TClass; AValue: Double);
  public
    property Foo[A: Integer; B: Boolean; C: String; D: TClass]: Double read GetFoo write SetFoo;
  end;

Indeksy właściwości są cztery, każdy innego typu – nie ma problemu. I przykładowe wywołanie:

var
  Some: TSome;
begin
  Some := TSome.Create();
  Some.Foo[8, False, 'furious', TStringList] := 3.14;

Parametr indeksowanej właściwości może być też rekordem, a nawet macierzą (a co!):

type
  TCaptions = array of String;

type
  TSome = class(TObject)
  private
    function GetFoo(A: String; const B: TCaptions): Double;
    procedure SetFoo(A: String; const B: TCaptions; AValue: Double);
  public
    property Foo[A: String; const B: TCaptions]: Double read GetFoo write SetFoo;
  end;

Przykład użycia:

var
  Some: TSome;
begin
  Some := TSome.Create();
  Some.Foo['furious', TCaptions.Create('foo', 'bar', 'baz')] := 3.14;

Do tej pory trzeba było pisać nazwę właściwości – tu: Foo – aby móc z niej skorzystać. Żeby tego nie robić, wystarczy dodać słówko default na końcu deklaracji właściwości, np.:

property Foo[A, B, C, D, E, F, G: Integer]: Integer read GetFoo write SetFoo; default;

Dzięki temu nawiasy do podawania indeksów mogą teraz być pisane tuż po identyfikatorze zmiennej:

var
  Some: TSome;
begin
  Some := TSome.Create();
  Some[0, 1, 2, 3, 4, 5, 6] := 100;

Należy pamiętać, że tylko jedna właściwość w klasie może być domyślną, a także o tym, że default ma więcej znaczeń, co zależy od kontekstu. Dzięki niemu możliwe jest też definiowanie domyślnej wartości parametru metody czy właściwości upublicznionej (co ma związek z zasobami formularzy, ale nie ma związku z problemem poruszanym w tym wątku, więc o tym kiedy indziej).


Można powiedzieć, że właściwości w obiektowym Pascalu są jednymi z najbardziej (jeśli nie najbardziej) rozbudowanymi i funkcjonalnymi konstrukcjami spośród wszystkich języków programowania. Indeksowanie właściwości to tylko jedna z wielu fajnych i przydatnych rzeczy – jest jeszcze wiele innych ciekawych konstrukcji.

Trzeba jednak pamiętać, że rozbudowane właściwości są możliwością, a nie koniecznością. Nikt nie lubi skoplikowanego kodu, więc należy unikać przesadyzmu.

0

Ponownie dzięki za pomoc.

A jak to wygląda z odśmiecaniem w Delphi? @babubabu słusznie zwrócił uwagę na wyciek, bo takie każdorazowe wywoływanie klasy chyba faktycznie niepotrzebnie będzie zajmować pamięć... czy nie? Delphi ma jakiś odśmiecacz automatycznie czyszczący takie nieużytki, czy trzeba to robić ręcznie?

Dla pewności przerobiłem kod, dzięki czemu zamiast klasy, w parametrze funkcji podaję po prostu inną funkcję (tworzącą potrzebną mi w danym momencie macierz). Z tego co wiem, po zakończeniu działania funkcji result powinien automatycznie zostać wywalony z pamięci, czyli nie zostanie syf. Trochę inaczej, ale mam co chciałem :).

Tak swoją drogą, przydałoby się dodać do kompendium coś o właściwościach indeksowanych, bo nie ma tam ani słowa.

0
Crow napisał(a):

A jak to wygląda z odśmiecaniem w Delphi? […] Delphi ma jakiś odśmiecacz automatycznie czyszczący takie nieużytki, czy trzeba to robić ręcznie?

Zasada jest prosta – jeśli sam zaalokowałeś pamięć, to musisz ją też sam zwolnić. Wyjątkiem są typy proste, macierze, struktury, interfejsy oraz komponenty posiadające rodzica. Natomiast tworzone instancje klas muszą być zwalniane za pomocą Free (lub Destroy, ale tego się nie stosuje).

Dla pewności przerobiłem kod […]

Unikaj tworzenia instancji klas wewnątrz metod i ich zwracania na zewnątrz – łatwo się w tym pogubić. Staraj się zawsze tworzyć obiekty i przekazywać referencje do metod. W ten sposób unikniesz wycieków pamięci i problemów z czytelnością kodu.

Tak swoją drogą, przydałoby się dodać do kompendium coś o właściwościach indeksowanych, bo nie ma tam ani słowa.

Używaj dokumentacji dostarczonej wraz ze środowiskiem, bo ta zawiera wszystkie potrzebne informacje i są one aktualne dla używanej wersji języka i bibliotek. Kompendium niestety ale jest stare i zapomniane.

0

Trochę niezwiązane z tematem, ale nawiązujące do naszej rozmowy. Jak wygląda kwestia zarządzania pamięcią przy użyciu zmiennych prostych (class var) w rekordach? Mam coś takiego (to tylko przykład):

type
TData = record
class var Value: Integer;
class function Create(const zValue: Integer): TData; static;
end;

class function TData.Create;
begin
Value:= zValue;
end;

var
Data: TData;

begin
Data:= TData.Create(150);
end;

Gdy już taki rekord przestanie mi być potrzebny, to jak mam się go pozbyć z pamięci? Albo jak pozbywać się z pamięci nieużywanych już zmiennych class var? Powinienem napisać jakaś procedurę czyszczącą, która wszystko wyzeruje ("wyniluje")? A może lepiej korzystać z klas? Nie ukrywam, że w tej konstrukcji wzorowałem się na System.Math.Vectors, w którym zastosowano takie właśnie rozwiązanie (to znaczy rekordy z metodami i zmiennymi prostymi), ale może to nie jest rozwiązanie optymalne?

Bez class var się nie obejdzie, bo metody proste w rekordach muszą być statyczne, a static nie ma dostępu do elementów swojej klasy. Metoda z kolei musi być prosta, bo rekordy nie pozwalają na tworzenie zwykłych metod z referencją do samych siebie...

0
Crow napisał(a):

Jak wygląda kwestia zarządzania pamięcią przy użyciu zmiennych prostych (class var) w rekordach?

Tak samo jak w przypadku innych typów zarządzanych – samo się alokuje i dealokuje. Standardowe rekodry są zarządzane, typy proste jak Integer też.

Gdy już taki rekord przestanie mi być potrzebny, to jak mam się go pozbyć z pamięci?

Nie możesz się go pozbyć – on sobie będzie istnieć w pamięci dotąd, aż wykonanie kodu wyjdzie poza zakres widoczności takiej zmiennej (lokalnej). Jeśli owa zmienna jest globalna, to istnieje w pamięci przez cały czas życia procesu. W końcu po to jest zarządzana, aby się nią nie przejmować.

Albo jak pozbywać się z pamięci nieużywanych już zmiennych class var?

Też nijak. Typ danych – a raczej informacje o nim – istnieje przez cały czas życia procesu. Jeśli taki typ zawiera zmienną klasową, to ona też żyje sobie razem z nim – w końcu takie są założenia. Po co chcesz je usuwać?

Powinienem napisać jakaś procedurę czyszczącą, która wszystko wyzeruje ("wyniluje")?

Jeśli używasz referencji lub wskaźników przechowujących dane alokowane ręcznie, to tak – w przeciwnym razie spowodujesz wycieki pamięci. To co chcesz użyć (te dziwne struktury) nie wymagają czyszczenia czy dealokacji.

A może lepiej korzystać z klas?

Do tej pory nie pytałem o to w jakim celu upychasz te dane w rekordach, ale jeśli nie musisz ich używać, to skorzystaj z normalnych klas. A jeśli z jakiegoś powodu nie chcesz samemu tworzyć i zwalniać instancji, to skorzystaj z klasycznych obiektów.

Te obiekty też są zarządane, więc odpada konieczność ich tworzenia i zwalniania. Tak jak struktury standardowo alokowane są na stosie, a dodatkowo wspierają dziedziczenie, hermetyzację i polimorfizm. Stare obiekty są protoplastą współczesnych klas i posiadają większą funkcjonalność niż rekordy (nawet te zaawansowane).

Szczerze pisząc koncepcja zaawansowanego rekordu to jakiś wypierd (po mojemu) będący rozpaczliwą próbą przeniesienia funkcjonalności starych obiektów do rekordów i odseparowania terminu ”obiekt” od jego pierwotnej postaci. Ten smutny żart ze strony twórców komplikuje relatywnie proste rzeczy i niepotrzebnie wprowadzaja dodatkowe zależności.

Metoda z kolei musi być prosta, bo rekordy nie pozwalają na tworzenie zwykłych metod z referencją do samych siebie...

Nie za bardzo rozumiem o czym piszesz, więc przykład jakiś by się przydał.

0
furious programming napisał(a):

Nie za bardzo rozumiem o czym piszesz, więc przykład jakiś by się przydał.

Chodziło mi o to, że coś takiego nie zadziała:

type
TTest = record
function Create (X: Integer): TTest;
end;

var
Test: TTest;

begin
Test:= TTest.Create(X);
end;

Nie zadziała, bo kompilator krzyczy, że taka składnia (w której funkcja zwraca jako result swój typ macierzysty) jest zarezerwowana dla konstruktorów i metod prostych. Konstruktorów w rekordach nie ma, więc zostaje metoda prosta, a ta z kolei - jak już wspomniałem - musi być statyczna, przez co nie ma dostępu do elementów swojego typu macierzystego.

0

Taka składnia nie jest prawidłowa, bo metoda Create nie jest statyczna, więc nie możesz jej wywołać z typu danych – potrzebujesz zmiennej:

var
  Test: TTest;
begin
  Test := Test.Create(10);

Wiem że to wygląda dziwnie, ale składniowo jest poprawne. Mimo wszystko jest dziwne, więc czegoś takiego nie używaj, bo to strasznie mąci w kodzie. Jeśli potrzebujesz metody inicjalizującej zawartość pól rekordu, to stwórz sobie metodę proceduralną i wywołaj ją z poziomu zmiennej:

type
  TTest = record
    procedure Create(X: Integer);
  end;

var
  Test: TTest;
begin
  Test.Create(10);
end.

A jeśli już koniecznie interesuje Cię składniowa imitacja konstrukcji tworzenia instancji klas, to dodaj do typu tego rekordu statyczną metodę funkcyjną zwracającą dane typu, w którym się znajduje (czyli TTest):

type
  TTest = record
    class function Create(X: Integer): TTest; static;
  end;

var
  Test: TTest;
begin
  Test := TTest.Create(10);
end.

Składnia użycia tej imitacji ”konstruktora” wygląda identycznie jak wywołanie konstruktora klasy. Różnica polega na tym, że nie musisz wołać swojego Create, aby móc użyć zmiennej Test. Kod odpowiedzialny za przydzielenie pamięci dla tej zmiennej generowany jest przez kompilator, bo rekordy są zarządzane.

W przypadku klas tak nie jest, przez co najpierw trzeba wywołać kontruktor, następnie poużywać sobie instancji, a na koniec koniecznie wywołać destruktor (pośrednio metodą Free). Jeśli tego nie zrobisz, to próba użycia instancji rzuci wyjątkiem typu Access Violation (lub SIGSEGV).

0

No wiem, ale to jednak nie jest funkcja, a więc nie może być podana jako parametr do innej funkcji czy procedury (co tu jest kluczowe). Nie zrobię sobie np. czegoś takiego:

V:= RotateVector(TVector.Create(120, 87, 435));

Twórcy biblioteki System.Math.Vectors postawili jak widać na takie samo rozwiązanie jak ja na początku, tylko klasę zastąpili rekordem.

A co do drugiej części twojego posta, no to właśnie o tym pisałem, że takie rozwiązanie zastosowałem u siebie, tj. rekord ze statyczną metodą prostą. Tyle że tam jest właśnie wymóg stosowania zmiennych prostych (class var), bo do innych static nie ma dostępu.

0

Jeśli dobrze zrozumiałem tę linijkę, którą podałeś w przykładzie post wyżej, to możesz tak zrobić:

type
  TVector = record
    X, Y, Z: Integer;
    class function Create(AX, AY, AZ: Integer): TVector;
  end;

  class function TVector.Create(AX, AY, AZ: Integer): TVector; static;
  begin
    Result.X := AX;
    Result.Y := AY;
    Result.Z := AZ;
  end;

  function RotateVector(AVector: TVector): TVector;
  begin
    {..}
  end;

var
  Vector: TVector;
begin
  Vector := RotateVector(TVector.Create(120, 87, 435));
Crow napisał(a):

Tyle że tam jest właśnie wymóg stosowania zmiennych prostych (class var), bo do innych static nie ma dostępu.

Możesz albo użyć class var, albo pole oznaczyć jako static. Lepiej by było, gdybyś sobie poczytał dokumentację.

0
furious programming napisał(a):

Jeśli dobrze zrozumiałem tę linijkę, którą podałeś w przykładzie post wyżej, to możesz tak zrobić:

type
  TVector = record
    X, Y, Z: Integer;
    class function Create(AX, AY, AZ: Integer): TVector;
  end;

  class function TVector.Create(AX, AY, AZ: Integer): TVector;
  begin
    Result.X := AX;
    Result.Y := AY;
    Result.Z := AZ;
  end;

  function RotateVector(AVector: TVector): TVector;
  begin
    {..}
  end;

var
  Vector: TVector;
begin
  Vector := RotateVector(TVector.Create(120, 87, 435));

No i dokładnie tak robię, to co napisałem (że tak się nie da), odnosiło się do pierwszej części twojego posta, gdzie zaproponowałeś przerobienie funkcji create na procedurę create :).

Możesz albo użyć class var, albo pole oznaczyć jako static. Lepiej by było, gdybyś sobie poczytał dokumentację.

Bez urazy, ale ty na prawdę zrozumiałeś co ja napisałem wcześniej? Przecież już w 3 poście z rzędu proponujesz mi rozwiązania, co do których napisałem dużo wcześniej, że Z NICH KORZYSTAM :D. Moje pytanie nie dotyczyło jak to zrobić (bo to rozkminiłem sam), tylko jak to się ma do zarządzania pamięcią...

0

Pytasz o różne rzeczy, przy okazji źle je nazywając, więc staram się odpowiadać na pytania i pewne rzeczy prostować. Pisałem już dużo wcześniej, że pamięci dla typów zarządzanych się nie alokuje i dealokuje, nieważne czy są to osobne zmienne, czy istniejące wewnątrz rekordu, klasy, starego obiektu itd.

W tym momencie zajmuję się też innymi rzeczami, co jakiś czas doskakując do komputera, więc mogłem coś pominąć lub nie doczytać. Choć napisane zostało już tyle, że nie powinno być żadnych wątpliwości.

0
furious programming napisał(a):

Pytasz o różne rzeczy, przy okazji źle je nazywając, więc staram się odpowiadać na pytania i pewne rzeczy prostować. Pisałem już dużo wcześniej, że pamięci dla typów zarządzanych się nie alokuje i dealokuje, nieważne czy są to osobne zmienne, czy istniejące wewnątrz rekordu, klasy, starego obiektu itd.

W tym momencie zajmuję się też innymi rzeczami, co jakiś czas doskakując do komputera, więc mogłem coś pominąć lub nie doczytać. Choć napisane zostało już tyle, że nie powinno być żadnych wątpliwości.

No i generalnie jestem wdzięczny, na prawdę! Zwłaszcza, że na tym forum trudno znaleźć osobę lepiej od ciebie obeznaną z Delphi. A to, że się nie dealokuje to ja rozumiem, tylko zastanawiam się, co się potem dzieje z pamięcią. Mam kilka modeli 3D, na których pracuję. Każdy z nich składa się z kilku tysięcy wierzchołków, które wczytuję i przerabiam na wektory, a następnie przetwarzam. W związku z tym zależy mi na odpowiednim zarządzaniu pamięcią, bo wyobrażam sobie, że taka ilość zmiennych może zrobić niezły śmietnik.

A za brak znajomości odpowiedniego słownictwa przepraszam. Nie jestem programistą (to raczej moje hobby), a większość wiedzy czerpię z anglojęzycznej dokumentacji, stąd nie zawsze wiem jak na pewne rzeczy powinno się mówić po polsku.

0
Crow napisał(a):

No i generalnie jestem wdzięczny, na prawdę! Zwłaszcza, że na tym forum trudno znaleźć osobę lepiej od ciebie obeznaną z Delphi.

Bez przesady, są tutaj znacznie bardziej obeznani i doświadczeni. Ja nawet nie używam Delphi – lata temu zmieniłem środowisko na Lazarusa i w nim programuję.

Mam kilka modeli 3D, na których pracuję. Każdy z nich składa się z kilku tysięcy wierzchołków, które wczytuję i przerabiam na wektory, a następnie przetwarzam. W związku z tym zależy mi na odpowiednim zarządzaniu pamięcią, bo wyobrażam sobie, że taka ilość zmiennych może zrobić niezły śmietnik.

Kilka tysięcy wierzchołków to jest pryszcz – to w końcu raptem kilka kilobajtów objętości. Jeśli trzymasz te wierzchołki w jakiejś liście (np. generycznej) to ona odpowiada za zarządzanie pamięcią dla swoich elementów.

0

Natomiast tworzone instancje klas muszą być zwalniane za pomocą Free (lub Destroy, ale tego się nie stosuje).

Uściślając: przeciążamy Destroy. Wywołujemy Free.
Free wywoła Destroy.

0
furious programming napisał(a):

/ciach/

Jeśli trzymasz te wierzchołki w jakiejś liście (np. generycznej) to ona odpowiada za zarządzanie pamięcią dla swoich elementów.

W Delphi? No nie jest tak, to zależy jakie to elementy (pisałeś już tu o typach prostych itd.) i jaka to lista.
Jeśli mamy obiekt typu TMyObject i listę TList<TMyObject> to taka lista nie zarządza pamięcią elementów.
Te obiekty trzeba sobie zwolnić.

Można to zrobić używając TObjectList<TMyObject>.Create(True) lub nawet TDictionaryprzekazując odpowiednie parametry, którą informują listę, czy ma zwalniać pamięć po elementach na swojej liście.
Tu więcej:
http://docwiki.embarcadero.com/Libraries/Tokyo/en/System.Generics.Collections.TObjectList.OwnsObjects

0
wloochacz napisał(a):

W Delphi? No nie jest tak, to zależy jakie to elementy (pisałeś już tu o typach prostych itd.) i jaka to lista.

Brakło precyzji – to fakt. Oczywiście mowa o typach prostych, bo takich OP używa w swoim kodzie (strukturki z kilkoma liczbami). Słuszna też uwaga co do OwnsObjects, choć OP opakowuje wszystko w rekordy.

Temat zarządzania pamięcią jest dość szeroki i trudno tutaj wymienić wszystkie przypadki (i wyjątki or reguły).

1 użytkowników online, w tym zalogowanych: 0, gości: 1