Entwickler-Ecke

Sonstiges (Delphi) - Element eines Array of Record löschen


JayEff - Di 09.08.05 22:34
Titel: Element eines Array of Record löschen
Hi Leute. ich hab einen Record wie folgt:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
Type
    TServer = Record
        IP: String;
        Name: String;
        Info: String;
        user: String;
        pw: String;
    End;

Und einen dyn. Array:

Delphi-Quelltext
1:
2:
3:
Var
    Form1: TForm1;
    ServerList: Array Of TServer;

Und in der folgenden Prozedur versuche ich, einen Eintrag zu löschen (bisher nur, alle Einträge nach oben zu schieben :roll: )

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
Procedure TForm1.Button5Click(Sender: TObject);
Var
    schritte, removed, i: Integer;
    temp:TServer;
Begin
    If ListBox1.ItemIndex >= 0 Then
    Begin
        schritte := length(ServerList) - ListBox1.ItemIndex;
        removed := ListBox1.ItemIndex;
        For i := 1 To schritte Do
        Begin
          temp.name:=serverList[removed + i].name;
          temp.info:=serverList[removed + i].info;
          temp.ip:=serverList[removed + i].ip;   <<--Accesviolation?! warum grade hier?? oO
          temp.user:=serverList[removed + i].user;
          temp.pw:=serverList[removed + i].pw;

          ServerList[removed+i-1].Name:=temp.name;
          ServerList[removed+i-1].info:=temp.info;
          ServerList[removed+i-1].ip:=temp.ip;
          ServerList[removed+i-1].user:=temp.user;
          ServerList[removed+i-1].pw:=temp.pw;
        End;
    End
    Else
        Messagedlg('Kein Server ausgewählt!', mtError, [mbOK], 0);
End;

Bölderweise erhalte ich immer beim letzten Schleifendurchlauf eine Zugriffsverletzung.
Gefüllt ist der Array so:

Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
ini-file:
[ServerList]
Count=4
IPs=456.456.456.456,5,5,5
Names=456,5,5,5
Infos='546789456789456789,5,5,5'
users=456,5,5,5
pws=879,5,5,5

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
Var
    IPs, Names, Infos, pws, users: TStringList;
    i: Integer;
Begin
    IPs := TStringList.Create;
    Names := TStringList.Create;
    Infos := TStringList.Create;
    pws := TStringList.Create;
    users := TStringList.Create;
    SetLength(ServerList, ini.ReadInteger('ServerList''count'0));
    Names.CommaText := ini.ReadString('ServerList''names''');
    IPs.CommaText := ini.ReadString('ServerList''IPs''');
    Infos.CommaText := ini.ReadString('ServerList''Infos''');
    pws.CommaText := ini.ReadString('ServerList''pws''');
    users.CommaText := ini.ReadString('ServerList''users''');
    ListBox1.Items.Assign(Names);
    For i := 0 To length(ServerList) - 1 Do
    Begin
        ServerList[i].Name := Names[i];
        ServerList[i].IP := IPs[i];
        ServerList[i].Info := Infos[i];
        ServerList[i].user := users[i];
        ServerList[i].pw := pws[i];
    End;
    IPs.Free;
    Names.Free;
    Infos.Free;
    pws.Free;
    users.Free;

Wieso? Ich versteh es nicht!
:?!?:
Bitte Helft mir!


SMO - Di 09.08.05 23:01

Button5Click:

Nehmen wir mal an, ListBox1.ItemIndex = 0. Damit wäre schritte = length(ServerList) und removed = 0. Die Schleifenvariable i bewegt sich also im Bereich von 1 bis length(ServerList) ---> serverList[removed + i] enspricht im letzten Schleifendurchlauf serverList[length(ServerList)] und überschreitet somit ganz klar die obere Arraygrenze. serverList[length(ServerList) - 1] ist das letzte Arrayelement.

Wenn du die Listeneinträge vom ausgewählten an abwärts einfach nur "hochschieben" willst, brauchst du doch auch gar kein "temp". Ich würde das ungefähr so machen:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
        for i := ListBox1.ItemIndex to High(ServerList) - 1 do
        begin
          ServerList[i].Name := ServerList[i + 1].Name;
          ServerList[i].info := ServerList[i + 1].info;
          ServerList[i].ip := ServerList[i + 1].ip
          ServerList[i].user := ServerList[i + 1].user
          ServerList[i].pw := ServerList[i + 1].pw;
        end;

Danach sollte die Länge der Liste natürlich noch um 1 reduziert werden, sonst gibt's den letzten Eintrag doppelt.


BenBE - Di 09.08.05 23:20

Wenn Du nicht grad .NET-kompatiblen Source voraussetzt, kannst Du das recht schnell mit Move machen:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
I := LB.ItemIndex;

If I = -1 Then 
    Exit;

Finalize(ServerList[I]);
Move(ServerList[I], ServerList[I + 1], SizeOf(TServerEintrag) * (High(ServerList) - I));


ACHTUNG: Bin mir bei Move wegen Src und Dest nicht ganz sicher, müsste aber eigentlich so stimmen, ansonsten 1.<-->2. Param. Das Finalize gibt den Speicher der bisherigen Strings frei. Konnt's aber leider grad nicht testen :(


JayEff - Di 09.08.05 23:37

Finalize? Move? HÄ? Also

Delphi-Quelltext
1:
2:
3:
4:
5:
        i := ListBox1.ItemIndex;
        Finalize(ServerList[i]);
        Move(ServerList[i], ServerList[i + 1], SizeOf(TServer) * (High(ServerList) - i));
        SetLength(ServerList, length(ServerList) - 1);
        ListBox1.DeleteSelected;

Klappt schonmal nicht, ich tausche mal die param1 und 2...

edit1:

Delphi-Quelltext
1:
2:
3:
4:
5:
        i := ListBox1.ItemIndex;
        Finalize(ServerList[i]);
        Move(ServerList[i + 1], ServerList[i], SizeOf(TServer) * (High(ServerList) - i));
        //SetLength(ServerList, length(ServerList) - 1);
        ListBox1.DeleteSelected;

brachte keine besserung, nun, das löschen klappt, auch ohne die auskommentierte zeile, versuche ich dann aber aufs letzte element zuzugreifen, t gibts eine zugriffsverletzung.
Ich kommentierte die Zeile aus => Alles klappt, bis ich einen neuen eintrag hinzufüge und dann versuche, auf diesen zuzugreifen.

hinzufügen:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
Var
    Name, IP, user, pw, Infos: String;
Begin
    Name := InputBox('Name''Name oder Kennzeichen des Servers''');
    IP := InputBox('IP''IP des Servers (Wird nicht auf Gültigkeit geprüft!)''');
    Infos := InputBox('Infos''Infos über den Server, z.B. Rates (optional)''');
    user := InputBox('Login ID''Ihre Login ID auf diesem Server''');
    pw := InputBox('Passwort''Ihr Passwort auf diesem Server''');
    If (length(Name) > 0And
        (length(IP) > 0And
        (length(user) > 0And
        (length(pw) > 0Then
    Begin
        showmessage(IntToStr(length(ServerList)));
        SetLength(ServerList, length(ServerList) + 1);
        showmessage(IntToStr(length(ServerList)));
        ServerList[length(ServerList) - 1].Name := Name;
        ServerList[length(ServerList) - 1].IP := IP;
        ServerList[length(ServerList) - 1].Info := Infos;
        ServerList[length(ServerList) - 1].user := user;
        ServerList[length(ServerList) - 1].pw := pw;
        ListBox1.Items.add(Name);
    End;
End;


SMO - Di 09.08.05 23:41

user profile iconBenBE hat folgendes geschrieben:
Wenn Du nicht grad .NET-kompatiblen Source voraussetzt, kannst Du das recht schnell mit Move machen:

Ja, schnell und gefährlich, denn das verursacht Speicherlecks! Wenn die ganzen String-Felder des ersten Records per Move überschrieben werden, bekommt das der Delphicompiler nicht mit und wird so auch keinen Code generieren, der den Referenzzähler der Strings dekrementiert und ggf. den durch die Strings belegten Speicher freigibt!

Edit: Ups, habe das Finalize übersehen. :oops:

Moment, bleibt trotzdem gefährlich. Mit Finalize kann man die Strings des ersten Records (derjenige, der gelöscht/überschrieben wird) freigeben. Was ist aber mit denjenigen des letzten Records? Ich hoffe folgendes Beispiel verdeutlicht, was ich meine:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
var
  s: array of string;

procedure Init;
var
  i: Integer;
begin
  SetLength(s, 10);
  for i := 0 to High(s) do
    s[i] := Format('String%d', [i]);
end;

procedure TestProc;
begin
  // string in erstem Arrayelement freigeben, weil es gleich überschrieben wird
  Finalize(s[0]);
  Move(s[1], s[0], High(s) * SizeOf(s[0]));
  // s[8] und s[9] zeigen jetzt auf denselben string, 'String9'.
  // Dieser sollte also einen Refcount von 2 haben, hat aber nur 1!

  // array um 1 verkürzen:
  SetLength(s, Length(s) - 1);
  // s[9] fliegt damit aus dem array, und da sein Refcount = 1 war, wird der
  // Speicher von 'String9' freigegeben ==> s[8] zeigt auf diesen freigegeben Speicher,
  // was früher oder später zu Fehlern führen wird
end;


BenBE - Mi 10.08.05 00:10

Hab nochmal nachgetestet (bei mir funzen folgende beiden Varianten (D7 Ent):


Variante 1:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
procedure TForm1.Button1Click(Sender: TObject);
var
    I: Integer;
begin
    I := 0;

    If I = -1 Then
        Exit;

    Finalize(ServerList[I]);
    Move(ServerList[I + 1], ServerList[I], SizeOf(TStringRecord) * (High(ServerList) - I));
    SetLength(ServerList, High(ServerList));
end;



Variante 2:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
procedure TForm1.Button1Click(Sender: TObject);
var
    I: Integer;
    TmpEntry: TStringRecord;
begin
    I := 0;

    If I = -1 Then
        Exit;

    TmpEntry := ServerList[I];
    Move(ServerList[I + 1], ServerList[I], SizeOf(TStringRecord) * (High(ServerList) - I));
    ServerList[High(ServerList)] := TmpEntry;
    SetLength(ServerList, High(ServerList));
end;


Mein TestSource sieht so aus:


Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
type 
    TStringRecord = record
        S1, S2, S3, S4, S5: String;
    end;

var
    ServerList: Array of TStringRecord;



Delphi-Quelltext
1:
2:
3:
4:
5:
6:
7:
8:
9:
procedure TForm1.FormCreate(Sender: TObject);
begin
    SetLength(ServerList, 100);
    ServerList[0].S3 := 'Test';    
    ServerList[99].S2 := Caption;    
    Button1Click(nil);
    Caption := ServerList[98].S2;
    SetLength(ServerList, 0);
end;


Das Programm ließ sich bei mir fehlerfrei beenden und brachte keinerlei Exceptions.


SMO - Mi 10.08.05 00:12

user profile iconJayEff hat folgendes geschrieben:
brachte keine besserung, nun, das löschen klappt, auch ohne die auskommentierte zeile, versuche ich dann aber aufs letzte element zuzugreifen, t gibts eine zugriffsverletzung.
Ich kommentierte die Zeile aus => Alles klappt, bis ich einen neuen eintrag hinzufüge und dann versuche, auf diesen zuzugreifen.


Liegt wahrscheinlich genau an dem Effekt, den ich oben beschrieben habe.
Hast du's mal auf meine Weise probiert, also "ServerList[i].Name := ServerList[i + 1].Name;" usw.?


BenBE - Mi 10.08.05 00:23

@SMD: Man kann auch ServerList[i] := ServerList[i+1]; schreiben. Delphi kopiert dann automatisch gleich alle Einträge des Records korrekt.


SMO - Mi 10.08.05 00:33

*handgegenkopfschlag* Na klar, du hast recht, ist viel einfacher so.

Deine zwei Varianten oben sind allerdings trotzdem beide "falsch", da potenziell gefährlich. Verstehst du, was ich meine?
Falls nicht, starte mal ein neues Delphiprojekt, füge meinen Testcode von oben ein und zusätzlich einen Button mit folgendem Handler:

Delphi-Quelltext
1:
2:
3:
4:
5:
6:
procedure TForm1.Button1Click(Sender: TObject);
begin
  Init;
  TestProc;
  ShowMessage(s[High(s)]);
end;


Bei mir verursacht ShowMessage eine Exception, und das ist auch richtig so, denn es wird versucht ein string anzuzeigen, dessen Speicher bereits freigegeben wurde. Das Problem entsteht dadurch, dass nach einer solchen Move-Operation, wie wir sie hier haben, das letzte und vorletzte Element beide dieselben strings referenzieren, diese strings jedoch weiterhin nur einen Refcount von 1 haben, obwohl es jetzt 2 sein müsste.

Dieses Problem kann folgendermaßen gelöst werden:

Delphi-Quelltext
1:
2:
3:
4:
    Finalize(ServerList[I]);
    Move(ServerList[I + 1], ServerList[I], SizeOf(TStringRecord) * (High(ServerList) - I));
    FillChar(ServerList[High(ServerList)], SizeOf(TStringRecord), 0);
    SetLength(ServerList, High(ServerList));


JayEff - Mi 10.08.05 15:39

Yaay! Bisher keine Probleme! Sollten doch wieder Probleme auftreten, melde ich mich, ansonsten: DANKE! :)