Indexed property – problem z modyfikacją pola rekordu

0

Chcę sobie napisać rekord, który jednocześnie będzie jakby dynamiczną tablicą rekordów, obudowaną w funkcje i nie mogę sobie poradzić z uzyskaniem właściwego rezultatu.

type
  TCell = record
    Value: Integer;
  end;

  TList = record
  strict private
    ConCell: array of TCell;
    function GetCell(Index: Integer): TCell;
    property Cell[Index: Integer]: TCell read GetCell; default;
  public
    procedure SetSize(pSize: Integer);
    procedure Clear;
  end;

var
  List: TList

=====

function TList.GetCell;
begin
  Result:= ConCell[Index];
end;

Dzięki takiej konstrukcji mogę raz "zarządzać" całą listą i np. chcąc ją wyczyścić albo zmienić jej wielkość, robię tak:

List.Clear;
List.SetSize(4);

A potem chcąc uzyskać dostęp do poszczególnych komórek listy, robię tak:

JakasZmiena:= List[0].Value;

zamiast:

JakasZmienna:= List.ConCell[0].Value;

Teoretycznie niewiele, ale to eleganckie rozwiązanie :). Sęk w tym, że to działa tylko przy odczycie i nie wiem jak to zakodować tak, żeby pozwoliło mi także zapisywać poszczególne zmienne w komórkach. Właściwość PropCell jako metodę zapisu (write) łyknie tylko coś takiego (z uwagi na zgodność typów):

procedure SetCell(Index: Integer; pCell: TCell);

Ale to odpada, bo w ten sposób mogę tylko przypisywać całe komórki, np.:

List[0]:= JakasKomorka; //JakasKomorka to zmienna typu TCell

Na coś takiego już mi nie pozwoli:

List[0].Value:= 167;

Da się to jakoś obejść?

0

Niestety nie, jest to ograniczenie tej składni.

1

Jak wspomniał @Patryk27, jest to ograniczenie języka, ale da się to rozwiązać dodając właściwość do struktury TCell. Bo w takim przypadku musi być użyta właściwość, a nie pole.

Nie musi ona posiadać settera w postaci metody – wystarczy bezpośredni dostęp do pola.

Przykład deklaracji:

type
  TItem = record
  private
    FData: Integer;
  public
    property Data: Integer read FData write FData;
  end;

type
  TList = record
  private
    FList: array [0..3] of TItem;
  private
    function GetItem(AIndex: Integer): TItem;
    procedure SetItem(AIndex: Integer; AItem: TItem);
  public
    property Items[AIndex: Integer]: TItem read GetItem write SetItem; default;
  end;

  function TList.GetItem(AIndex: Integer): TItem;
  begin
    Result := FList[AIndex];
  end;

  procedure TList.SetItem(AIndex: Integer; AItem: TItem);
  begin
    FList[AIndex] := AItem;
  end;

I przykład użycia:

var
  List: TList;
begin
  List[0].Data := 10; // składnia dozwolona
end.

To powoduje, że właściwość TItem.Data umożliwia aktualizowanie danych struktury pojedycznej pozycji, a właściwość TList.Items pozwala odczytywać i modyfikować całe rekordy pozycji.

W powyższym przykładzie posłużyłem się macierzą o statycznym rozmiarze, żeby nie zaśmiecać przykładowego kodu informacjami, których problem nie dotyczy.


Pytanie dodatkowe – dlaczego bawisz się rekordami, zamiast skorzystać z gotowego kontenera, np. TList?

0

Gdy korzystam z twojego przykładu, to wywala mi błąd: E2064 Left side cannot be assigned to

A dlaczego nie użyję TList? Bo to był tylko zmyślony przykład :). Tak na prawdę mam rekord, który grupuje powierzchnie ("Faces") i ma kilka wbudowanych metod do grupowej obsługi tych powierzchni. Czasem jednak potrzebna jest ingerencja indywidualna. Grupowo używam tego tak:

Faces.ReadNormals;

A indywidualnie tak:

Faces.Face[13].ReadNormal;

No i pomyślałem sobie, że fajnie by było móc to skrócić i pisać tak:

Faces[13].ReadNormal;
0

Kod testowałem pod Lazarusem i działa prawidłowo. ;)

Crow napisał(a):

A indywidualnie tak:

Faces.Face[13].ReadNormal;

No i pomyślałem sobie, że fajnie by było móc to skrócić i pisać tak:

Faces[13].ReadNormal;

No tak, tyle że to jest samo ukrycie właściwości (?) Face, a to robi się za pomocą default. No to jak w końcu, chodzi o stworzenie właściwości domyślnej, czy dodaniu możliwości modyfikacji poszczególnych pól struktur?

0

Chodzi o możliwość pełnego korzystania z zawartości rekordu TFace (czyli zapis i odczyt pól, wywoływanie metod), znajdującego się - w formie tablicy - wewnątrz rekordu TFaces, z jednoczesnym ukryciem odwoływania się do niego, czyli np.:

TFace = record
  Value: Integer;
  Low: Double;
  Metoda1
end;

TFaces = record
strict private
  CFace: array of TFace;
  function GetFace(Index: Integer): TFace;
  procedure SetFace(Index: Integer; TFace);
  property Face[Index: Integer]: TFace read GetFace write SetFace; default;
public
  Metoda1;
  Metoda2;
end;

Użycie:

Faces[123].Value:= 76;
Zmienna:= Faces[14].Low;
Faces[16].Metoda1;

zamiast

Faces.Face[123].Value:= 76;
Zmienna:= Faces.Face[14].Low;
Faces.Face[16].Metoda1;

A co do twojego kodu, to zrobiłem kopiuj / wklej i wyskakuje wspomniany błąd :(.

1
Crow napisał(a):

Chodzi o możliwość pełnego korzystania z zawartości rekordu TFace (czyli zapis i odczyt pól, wywoływanie metod), znajdującego się - w formie tablicy - wewnątrz rekordu TFaces, z jednoczesnym ukryciem odwoływania się do niego […]

W dalszym ciągu nie wiem dlaczego używasz rekordów i dlaczego sam implementujesz listę (w dodatku w postaci rekordu), zamiast skorzystać z klasy (do opisu pojedynczej ”twarzy”) oraz z wbudowanego generycznego kontenera pokroju TList (do przechowywania listy tych ”twarzy”). Dorobiłbyś sobie settery i mógłbyś modyfikować co tylko chcesz, nawet pojedynczo, i też mógłbyś mieć domyślne indeksowane właściwości.

Robisz tak dlatego, że pracujesz nad jakimś tam projektem i nie możesz tego zmienić, czy z innego powodu?

A co do twojego kodu, to zrobiłem kopiuj / wklej i wyskakuje wspomniany błąd :(.

Widać FPC w tym przypadku różni się od Delphi, pozwalając na taką konstrukcję. Innego rozwiązania nie znam.

0

Nie zmieniam na TList, bo cały projekt zbudowałem na bazie tablic i teraz nie chce mi się wszystkiego przerabiać :). Może w następnej wersji projektu spróbuję. Tak czy inaczej dzięki! ;].

0

Jednak muszę odkopać (przepraszam), bo coś nie działa...

TFace = record
  Value: Integer;
end;

TFaces = class(TList<TFace>)
  //Jakies metody
end;

var
  F: TFace;
  Faces: TFaces;

Faces:= TFaces.Create;
F.Value:= 0;
Faces.Add(F);
Faces[0].Value:= 123;

Dostaję error:

E2064 Left side cannot be assigned to

Próbowałem też dodać do TFace właściwość. Bezpośrednie wskazanie na zmienną stanowiącą kontener (read cValue write cValue) dawało ten sam error, skompilowało się dopiero po dodaniu gettera i settera... tyle, że całość nadal nie działa i zmienna się nie nadpisuje.

procedure TFaces.SetValue(pValue: Integer);
begin
  cValue:= pValue; //cValue to zmienna kontenera, z której odczytuje i do której zapisuje właściwość Value;
end;

Faces:= TFaces.Create;
F.Value:= 0;
Faces.Add(F);
Face[0].Value:= 123; //kompiluje się ale nadal jest tam zero

Działa tylko coś takiego:

Faces:= TFaces.Create;
F.Value:= 0;
Faces.Add(F);
F.Value:= 100;
Faces[0]:= F;

No ale coś takiego mnie nie urządza, bo ja chcę indywidualnego dostępu do pól rekordu, bez każdorazowego nadpisywania całego rekordu.

1
Crow napisał(a):

Dostaję error:

E2064 Left side cannot be assigned to

To jest ten sam problem co wcześniej – jeśli właściwość zwraca rekord, to nie możesz modyfikować jego pól bezpośrednio. Analogicznie do mojego poprzedniego przykładu, spróbuj w taki sposób:

type
  TItem = record
  private
    FData: Integer;
  public
    property Data: Integer read FData write FData;
  end;

type
  TList = class(TList<TItem>);

i użycie:

var
  List: TList;
  Item: TItem;
begin
  List := TList.Create();
  
  Item := TItem.Create();
  Item.Data := 10;
  
  List.Add(Item);
  List[0].Data := 20; // sprawdź czy działa

Jeśli działa to popraw swój kod, a jak nie to wymień rekordy na zwykłe, poste klasy. A jeśli potrzebujesz typu zarządzanego, alokowanego na stosie, to spróbuj w podobny sposób wykorzystać stare obiekty.

0

Dokładnie tak samo próbowałem to rozwiązać, ale nie działa. Na szczęście udało się z klasami prostymi, z którymi, z jakiegoś powodu nie ma problemu. Wszystko się kompiluje, a wartości zostają nadpisane poprawnie (nawet nie muszę używać właściwości, zwykłe pola wystarczą). Dzięki, zobaczę w praniu, czy wszystko zadziała jak należy :).


Edit: A jednak nie ;/.

type
  TFace = class(TObject)
    class var Value: Integer;
  end;

  TFaces = class(TList<TFace>)
    //Jakies metody
  end;

Faces:= TFaces.Create;
  Face.Value:= 0;
  Faces.Add(Face);
  Faces.Add(Face);
  Faces[0].Value:= 123;
  Faces[1].Value:= 345;
  ShowMessage(IntToStr(Faces[0].Value));
  ShowMessage(IntToStr(Faces[1].Value));

Zwraca dwa razy 345, czyli przypisanie do obojętnie którego elementu listy, nadpisuje całą :(.

0

@Crow: wiesz w ogóle czym są zmienne klasowe? Raczej nie…

Miałeś skorzystać ze zwykłej klasy, w której byłoby prywatne pole i publiczna właściwość:

type
  TFace = class(TObject)
  private
    FValue: Integer;
  public
    property Value: Integer read FValue write FValue;
  end;

albo od biedy publiczne pole bez właściwości, jeśli jakakolwiek walidacja itp. nie jest potrzebna:

type
  TFace = class(TObject)
  public
    Value: Integer;
  end;

Takie pola są unikalne dla każdej instancji (obiektu), więc modyfikacja danych jednego obiektu nie spowoduje zmiany danych w innych obiektach tej samej klasy. Druga istotna rzecz to to, że nie ma dostępu do tych pól przez utworzeniem instancji klasy, a po jej utworzeniu można te dane modyfikować tylko za pomocą referencji. Czyli tak:

var
  Face: TFace;
begin
  Face := TFace.Create();
  Face.Value := 100; // zmiana wartości

Zmienne klasowe to zupełnie inna bajka. Te przynależą do klasy i są wspólne dla klasy oraz wszystkich jej instancji. Istotny jest właśnie ten fakt – zmienna należy do klasy, a jej wartość można modyfikować za pomocą referencji, czyli tak:

type
  TFace = class(TObject)
    class var Value: Integer;
  end;

var
  Face: TFace;
begin
  Face := TFace.Create();
  Face.Value := 100; // zmiana wartości

ale też za pomocą samego identyfikatora klasy (bez konieczności tworzenia instancji):

type
  TFace = class(TObject)
    class var Value: Integer;
  end;

begin
  TFace.Value := 100; // zmiana wartości

Twój kod działa prawidłowo, dlatego że zmienna klasowa jest jedna, bez względu na liczbę utworzonych referencji. Modyfikując jej wartość sprawiasz, że nowa wartość dostępna jest z poziomu dowolnego elementu listy i jest taka sama.


Podsumowując, nie używaj rzeczy, których przeznaczenia nie rozumiesz i nie kombinuj, jeśli nie potrzebujesz. A po drugie, zanim użyjesz nowego elementu lub konstrukcji, najpierw kuknij do dokumentacji i zapoznaj się z jego/jej specyfiką, bo programowanie randomowe to z reguły bardzo zły pomysł. ;)

0

Dzięki za odpowiedź. A co do kombinowania, to właśnie w taki sposób uczę się nowych rzeczy :) (jak kiedyś pisałem, nie jestem programistą tylko humanistą ;p). Testuję -> nie działa -> próbuję rozwiązać problem -> profit :). No ale to chyba raczej nie zadziała, bo nijak nie jestem w stanie uzyskać dostępu do zmiennej Value wewnątrz TFace poprzez Faces[Index].Value. Przy próbie korzystania z właściwości, ciągle wywala mi ten sam błąd E2064 Left side cannot be assigned to, niezależnie czy używam domyślnego TList, własnego TList, czy tablic. W Lazarusie to może działa, w Delphi niestety nie ;/.

2

Użyj generycznego TObjectList:

type
  TFaces = TObjectList<TFace>;
0

Tak, w przypadku klas kompilator się już nie pluje i można je normalnie nadpisywać, nawet bez użycia właściwości. Czyli to było problemem, wystarczyło zamienić rekord na klasę. Dzięki! :).

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