c# im dritten Anlauf



  • µ schrieb:

    pascal2009 schrieb:

    Aber wenn ich ein geschachteltes Array mit gemischten Typen deklariere, muß ich für die geschachtelten Array mit new wieder separat Speicher anfordern? Also Speicher innerhalb des Speichers?

    Ja.

    Statt rohe Arrays solltest Du die schon mehrfach empfohlenen Container verwenden. List<T> ist dabei Allzweckwaffe.

    List<CL_Land> Land = new List<CL_Land>();
    
    CL_Land land = new CL_Land();
    land.name = "Belgien";
    Land.Add(land);
    

    Falls der Vorteil nicht sofort ersichtlich ist: Diese Container wachsen dynamisch. Überbleibsel im C-Stil wie Arrays fester Größe und irgendwelchen MAX_LÄNDER Konstanten sind nicht nötig.

    Das werde ich mir mal zu Gemüte führen. Danke für den Hinweis.

    Feste Arrays sind dann Ballast, wenn man die Datenmenge nicht kenn. Wenn es sich aber wie bei dem Brettspiel Risiko um 42 Länder handelt und um 4 Spieler, kann man drüber wegsehen.

    Als learner by doer oder try and error hab ich den Code jetzt doch zum Laufen gebracht. Das ist allerdings wirklich nicht elegant, mit new innerhalb der Klasse für jedes Array nochmal Speicher anzufordern. Wie gesagt, ich werde mir das mit List<T> mal zu Gemüte führen.

    So läuft es jedenfalls fehlerfrei (man bemüht sich, sich in den Compiler hineinzudenken 🤡 ):

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    
    namespace Domination_WinForms
    {
        public class CL_World
        {
            public const int MAX_LÄNDER = 3;
            public const int MAX_NACHBARN = 4;
    
            public class CL_Land
            {
                public int nr;
                public string name;
    //            public int gehört_zu_spieler;
    //            public int farbe;
    //            public int armeen;
                public int[] nachbar;
            }
    
            public CL_Land[] Land = new CL_Land[MAX_LÄNDER]; 
    
            public bool init_Länder()
            {
                for (int i = 0; i < MAX_LÄNDER; i++)
                {
                    Land[i] = new CL_Land();
                    Land[i].nr = i;
                    Land[i].name = ""; // ausnullen
                    Land[i].nachbar=new int[MAX_NACHBARN];
                    for (int k = 0; k < MAX_NACHBARN; k++)
                    {
                        Land[i].nachbar[k] = new int();
                        Land[i].nachbar[k] = -1;
                    }
                } 
                            Land[0].name = "England";
                            Land[0].nachbar[0] = 1;
                            Land[0].nachbar[1] = 2;
    
                            Land[1].name = "Schottland";
                            Land[1].nachbar[0] = 0;
    
                            Land[2].name = "Belgien";
                            Land[2].nachbar[0] = 0;
    
                            return true;
            } // init Länder
        } // class CL_World
    } // namespace
    

    Und zeigt dann auf dem Hauptbildschirm der Form1 sehr schön die Länder mit den Daten an.

    Ich sehe es aus der AnsiC Perspektive so, daß mit dem

    public CL_Land[] Land = new CL_Land[MAX_LÄNDER]; sowie
    Land[i].nachbar=new int[MAX_NACHBARN]; für das eingeschlossene Array

    jeweils Zeiger auf den Startpunkt bzw. die Elemente der Array-Liste bereitgestellt werden, aber kein Speicherplatz für die Elemente bereitsteht, die man dann für jedes Element wieder neu anfordern muß mit:

    Land[i] = new CL_Land(); sowie für die eingeschlossenen Array-Elemente mit:
    Land[i].nachbar[k] = new int();

    Das ist verglichen mit AnsiC wirklich etwas heftig, muß man überhaupt erstmal drauf kommen. Man kommt dahin, wenn man die (kritischen) Programmelemente stufenweise ausklammert. Dann bekommt man einen Hinweis, wo der Hase im Gebüsch sitzen könnte.

    Pascal



  • new int() ist nicht notwendig. Für Wertetypen muss man den Speicher nicht mit new besorgen.

    Das ist allerdings wirklich nicht elegant, mit new innerhalb der Klasse für jedes Array nochmal Speicher anzufordern.

    Stimmt. Momentan ist Deine Klasse aber nicht mehr als ein Struct im C-Stil. Die Eleganz kommt noch, lies einfach mal weiter.



  • µ schrieb:

    new int() ist nicht notwendig. Für Wertetypen muss man den Speicher nicht mit new besorgen.

    Das ist allerdings wirklich nicht elegant, mit new innerhalb der Klasse für jedes Array nochmal Speicher anzufordern.

    Stimmt. Momentan ist Deine Klasse aber nicht mehr als ein Struct im C-Stil. Die Eleganz kommt noch, lies einfach mal weiter.

    Du hast recht, wenn man das rausnimmt, läuft es auch. Irgendwie klar, daß man für Wertetypen keinen Speicher auf dem Heap anfordert. Ogottogott 🤡 Sind eben Übergangsprobleme.

    Ich hoffe, deine Prognose bzgl. der Eleganz wird eintreffen. 😉 Ich arbeite dran.

    Pascal



  • pascal2009 schrieb:

    Du hast recht, wenn man das rausnimmt, läuft es auch. Irgendwie klar, daß man für Wertetypen keinen Speicher auf dem Heap anfordert. Ogottogott 🤡 Sind eben Übergangsprobleme.

    Achtung!
    new ist in C# erstmal nur Syntax.
    new heisst nicht unbedingt dass Speicher vom managed Heap besorgt wird!
    Es heisst nur: hier wird ein neues Objekt erzeugt/initialisiert.

    Wenn der verwendete Typ ein Value-Type (short, int, long, ..., DateTime, TimeSpan, eigene structs, ...) ist, dann wird nix vom Heap genommen.

    Für Value-Types wird nur was vom Heap genommen, wenn sie "boxed" werden. Was man allerdings nicht dadurch erreicht dass man "new" schreibt, sondern indem man sie als "object" behandelt/übergibt.

    Wenn der verwendete Typ dagegen ein Reference-Type ist ("class"), dann schon.



  • Um deinen Code mal ein bisschen zu verschönern:

    public class CL_World
        {
            public class CL_Land
            {
                //public int gehört_zu_spieler; 
                //public int farbe; 
                //public int armeen
                public string name;
                public int[] nachbar;
    
                public CL_Land(string name, int[] nachbar)
                {
                    this.name = name;
                    this.nachbar = nachbar;
                }
            }
    
            public CL_Land[] Land;
    
            public CL_World()
            {
                Land = new CL_Land[3];
                Land[0] = new CL_Land("England", new int[] { 1, 2 });
                Land[1] = new CL_Land("Schottland", new int[] { 0 });
                Land[2] = new CL_Land("Belgien", new int[] { 0 });
    
            } // init Länder 
        } // class CL_World
    

    Für Arrays fixer Größer nimmst du natürlich auch in C# Arrays, alles andere ist Unsinn.
    Falls du CL_Land statt Class zu einer Struct machen würdest, müsstest du nicht für jedes Element extra Speicher anfordern (hat aber dann andere Nachteile). Wenn du jetzt aber die Länder direkt über den Konstruktor füllst (wie es gedacht ist) dann macht das "extra Speicher reservieren müssen" auch nix mehr. 😉



  • hustbaer schrieb:

    pascal2009 schrieb:

    Du hast recht, wenn man das rausnimmt, läuft es auch. Irgendwie klar, daß man für Wertetypen keinen Speicher auf dem Heap anfordert. Ogottogott 🤡 Sind eben Übergangsprobleme.

    Achtung!
    new ist in C# erstmal nur Syntax.
    new heisst nicht unbedingt dass Speicher vom managed Heap besorgt wird!
    Es heisst nur: hier wird ein neues Objekt erzeugt/initialisiert.

    Wenn der verwendete Typ ein Value-Type (short, int, long, ..., DateTime, TimeSpan, eigene structs, ...) ist, dann wird nix vom Heap genommen.

    Ich denke, ein Wert-Typ wird einfach auf den Stack gepackt.

    Mit class x // x cx = new x(); wird ein Zeiger erzeugt, der auf die Startadresse des Objekts(der Klasse) verweist. Ob der Zeiger auf dem Stack abgelegt ist oder im Heap, weiß ich nicht. Stack würde ja Sinn machen, um ihn danach wieder runterzunehmen. Der ganze Speicher für die Elemente der Liste muß vom Heap kommen, weil er ja wahlfrei ist und das dynamisch organisiert werden muß.

    Ich hab zu List<T>mal einen kleinen Test probiert. Besteht aus Performance Test im Vergleich Array-List. Zugleich hab ich noch den Speicher ausgelesen, dieser Teil ist aber hier deaktiviert.

    using System.Linq;
    using System.Text;
    using System.Collections;
    using System.Diagnostics;
    
    namespace Test
    {
        using System;
    using System.Collections.Generic;
    
        class Program
            {
    /* alles unwichtig, die Speicherauslesung deaktiviert
            class RAM
            {
                public void zeige_RAM()
                {
                    long lr;
                    Process currentProcess = Process.GetCurrentProcess();
                    //To get the current process and use:
                    lr = currentProcess.PrivateMemorySize64/1024;
                    Console.WriteLine(lr.ToString());
                    Console.ReadKey();
                }
            }
    
            class diag
            {
                // Define variables to track the peak 
                // memory usage of the process. 
                long peakPagedMem = 0,
                    peakWorkingSet = 0,
                    peakVirtualMem = 0;
    
                public void zeige_Diag()
                {
                    Process myProcess = Process.GetCurrentProcess();
                    // Refresh the current process property values.
                    myProcess.Refresh();
    
                    Console.WriteLine();
    
                    // Display current process statistics.
    
                    Console.WriteLine("{0} -", myProcess.ToString());
                    Console.WriteLine("-------------------------------------");
    
                    Console.WriteLine("  physical memory usage: {0}",
                        myProcess.WorkingSet64/1024);
                    Console.WriteLine("  base priority: {0}",
                        myProcess.BasePriority);
                    Console.WriteLine("  priority class: {0}",
                        myProcess.PriorityClass);
                    Console.WriteLine("  user processor time: {0}",
                        myProcess.UserProcessorTime);
                    Console.WriteLine("  privileged processor time: {0}",
                        myProcess.PrivilegedProcessorTime);
                    Console.WriteLine("  total processor time: {0}",
                        myProcess.TotalProcessorTime);
                    Console.WriteLine("  PagedSystemMemorySize64: {0}",
                        myProcess.PagedSystemMemorySize64 / 1024);
                    Console.WriteLine("  PagedMemorySize64: {0}",
                       myProcess.PagedMemorySize64 / 1024);
    
                    Console.ReadKey();
    
                } // RAM
            } // Class DIAG
    
     */ 
    
                static void Main(string[] args)
                {
                    int t1, t2;
                    int dummy;
                    int zeit = 0;
                    string str = "Das ist das Haus vom Nikolaus";
                    string buf = "";
                    ArrayList a1 = new ArrayList();
                    List<int> list1 = new List<int>();
                    t1 = Environment.TickCount;
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            list1.Add(i);
    
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            dummy = list1[i];
    
                    t2 = Environment.TickCount;
                    zeit = t2 - t1;
                    Console.WriteLine("Dauer für List: " + zeit.ToString());
    
                    t1 = Environment.TickCount;
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            a1.Add(i);
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            dummy = (int)a1[i];
    
                    t2 = Environment.TickCount;
                    zeit = t2 - t1;
                    Console.WriteLine("Dauer für ArrayList: " + zeit.ToString());
    
                    ArrayList a2 = new ArrayList();
                    List<string> list2 = new List<string>();
                    t1 = Environment.TickCount;
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            list2.Add(str);
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            buf = list2[i];
    
                    t2 = Environment.TickCount;
                    zeit = t2 - t1;
                    Console.WriteLine("Dauer für String List: " + zeit.ToString());
    
                    t1 = Environment.TickCount;
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            a2.Add(str);
                    for (int i = 0; i < 1000000; i++)
                        for (int k = 0; k < 5; k++)
                            buf = (string)a2[i];
    
                    t2 = Environment.TickCount;
                    zeit = t2 - t1;
                    Console.WriteLine("Dauer für ArrayList mit String: " + zeit.ToString());
                    Console.ReadKey();
                } // main
            } // class program
    } // namespace
    

    Ergebnis:

    Die Klasse list ist bei Integer um den Faktor 5-6 schneller als ArrayList. Das kommt sicher daher, daß beim Zurücklesen eine Typumwandlung erfolgen muß (Cast).

    Bei Strings ist allerdings ArrayList mindestens genausoschnell. Warum?

    Ich weiß es nicht.

    Würde ich aber gern wissen. Falls von den Strings nur die Speicheradresse gespeichert wird, wäre das ja auch ein INT32, und die Rückumwandlung müßte dieselbe Zeit beanspruchen wie für den Wertetyp INT. Tut sie aber nicht.

    Mein schlaues Buch sagt, ArrayList ist nichtgenerisch. List dagegen ist generisch (Typgebunden), was eine Reihe von Vorteilen hat bzgl. der Sicherheit der Codierung mit sich bringen soll. Nun, von Ansi-C aus gesehen, ist der Vorteil so nicht ganz nachzuvollziehen, wenn wir eine Datei binär speichern, egal von welchem Typ, kann man die ohne den passenden struct/Typ sowieso nicht auslesen, und von welchem Typ die Binärdatei stammt, sieht man ja in der Speicherfunktion (allerdings, ja, das ist ein Fortschritt, nicht im intellisense, den gibt es in Ansi C nicht, und der ist wirklich nicht zu verachten. Daher ist in C# wohl List der Arraylist vorzuziehen.

    Insgesamt gesehen, hab ich noch was Delegates, Klassenvererbung und viele andere Sachen betrifft

    ehrlich gesagt

    sehr große Verständnisprobleme. Ich frag mich dabei: gut, das gibt es. Aber wer braucht das und wozu soll das nütze sein? Wenn ich keinen Nutzen sehe, kann ich mich schlecht reindenken.

    Aber was solls, da muß man durch.

    Pascal



  • DarkShadow44 schrieb:

    Um deinen Code mal ein bisschen zu verschönern:

    public class CL_World
        {
            public class CL_Land
            {
                //public int gehört_zu_spieler; 
                //public int farbe; 
                //public int armeen
                public string name;
                public int[] nachbar;
    
                public CL_Land(string name, int[] nachbar)
                {
                    this.name = name;
                    this.nachbar = nachbar;
                }
            }
    
            public CL_Land[] Land;
    
            public CL_World()
            {
                Land = new CL_Land[3];
                Land[0] = new CL_Land("England", new int[] { 1, 2 });
                Land[1] = new CL_Land("Schottland", new int[] { 0 });
                Land[2] = new CL_Land("Belgien", new int[] { 0 });
    
            } // init Länder 
        } // class CL_World
    

    Für Arrays fixer Größer nimmst du natürlich auch in C# Arrays, alles andere ist Unsinn.
    Falls du CL_Land statt Class zu einer Struct machen würdest, müsstest du nicht für jedes Element extra Speicher anfordern (hat aber dann andere Nachteile). Wenn du jetzt aber die Länder direkt über den Konstruktor füllst (wie es gedacht ist) dann macht das "extra Speicher reservieren müssen" auch nix mehr. 😉

    So allmählich krieg ich die Vorstellung, wie C# Code aussehen sollte. 😉

    Sieht wirklich gut aus.

    Aber wie lese ich aus, ob es nun einen Nachbarn gibt oder viele?

    Die Elemente sind mit {} ja nur teilweise und unterschiedlich befüllt.

    Wie ist die Abbruchbedingung bei der Abfrage, wieviele Nachbarn es gibt? (ich hatte die ja daher in meinem Code mit -1 befüllt, weil 0 ja auch ein gültiger Wert wäre für ein Nachbarland).

    Wäre es bei max 4 Nachbarn vielleicht besser so: {1,2,-1,-1} ?

    Pascal



  • Die Klasse list ist bei Integer um den Faktor 5-6 schneller als ArrayList. Das kommt sicher daher, daß beim Zurücklesen eine Typumwandlung erfolgen muß (Cast).

    Bei Strings ist allerdings ArrayList mindestens genausoschnell. Warum?

    Ich weiß es nicht.

    Würde ich aber gern wissen. Falls von den Strings nur die Speicheradresse gespeichert wird, wäre das ja auch ein INT32, und die Rückumwandlung müßte dieselbe Zeit beanspruchen wie für den Wertetyp INT. Tut sie aber nicht.

    Ist ansich ganz einfach. Bei der generischen List können Werttypen direkt in der List gespeichert werden. Bei der nicht generischen ist Boxing nötig. D.h. es wird für jeden kleinen int ein neues Objekt auf dem Heap angelegt. Ergo: langsam.
    Bei strings ist das nicht der Fall, die sind so oder so Referenztypen. Ergo auch nicht viel langsamer. Nur die Typchecks sind noch nötig, weil von object nach string gecastet wird.

    Btw., solche Mikrobenchmarks erfordern die Beachtung einiger Regeln. Z.B. solltest du den GC beachten, und den Releasebuild benchmarken (ohne unterdrückte Optimierungen wenn unter Kontrolle des Debuggers).

    Aber wie lese ich aus, ob es nun einen Nachbarn gibt oder viele?

    Die Elemente sind mit {} ja nur teilweise und unterschiedlich befüllt.

    Das mit den Klammern ist nur schöne Syntax. Das ist die Erstellung eines Arrays.
    Arrays wissen in C# ihre Größe, du kannst also einfach "nachbar.Length" schreiben und hast die Anzahl der Nachbarn. Irgendwelche Spezialwerte um zu signalisieren dass das der letzte Nachbar war sind also nicht nötig. 😉



  • DarkShadow44 schrieb:

    Ist ansich ganz einfach ...

    Btw., solche Mikrobenchmarks erfordern die Beachtung einiger Regeln. Z.B. solltest du den GC beachten, und den Releasebuild benchmarken (ohne unterdrückte Optimierungen wenn unter Kontrolle des Debuggers).

    Hast du wirklich gut erklärt.

    Wie ich allerdings einen "Releasebuild benchmarken" soll, nun ja.

    Hmm ... 🤡

    Pascal



  • Du stellst von "Debug" auf "Release" um und lässt es ohne Debugger laufen ? 😃



  • pascal2009 schrieb:

    Die Klasse list ist bei Integer um den Faktor 5-6 schneller als ArrayList. Das kommt sicher daher, daß beim Zurücklesen eine Typumwandlung erfolgen muß (Cast).

    Bei Strings ist allerdings ArrayList mindestens genausoschnell. Warum?

    Ich weiß es nicht.

    Das liegt daran dass ArrayList immer "object" speichert. Und Value-Typen müssen geboxt werden wenn man sie als "object" rumreicht.
    D.h. es wird für jeden int ein Reference-Type (die "Schachtel" in die der int zum Zwecke der ver-obejktung eingepackt wird) auf dem Managed-Heap erzeugt wenn man den int in die ArrayList tut.
    Deswegen nimmt man auch List<int> - da ist das nämlich nicht so.

    string dagegen ist ein Value-TypeReference-Type, und der kann direkt in der ArrayList gespeichert werden.



  • Da mir hier schon ganz nett geholfen wurde, kommt noch ein kleines Rätsel hinterher. Ist es so, daß man mit List keine struct bzw. Klassenlemenente speichern kann? Wenn nein, was mach ich falsch?

    class CL_Konten
        {
            public class CL_Konto
            {
               public int kontonr;
               public string kontobez;
               public double eroeffnung;
               public double abschluss;
               public char art;
            }
    
            public bool init_Kontenrahmen_version1()
            { 
                 // Todo: Konten aus Datei auslesen und in Liste ablegen
                List <string> kontenliste = new List<string>();
                CL_Konto kbuf = new CL_Konto();
                kbuf.kontonr = 4900;
                kbuf.kontobez = "Sonstige Kosten";
                kontenliste.Add(kbuf.kontobez);
                MessageBox.Show(kontenliste.Count.ToString());
                kbuf.kontobez = "geändert";
                MessageBox.Show(kontenliste[0]); // Ausgabe hier: "Sonstige Kosten" 
                return true;      
            }
            public bool init_Kontenrahmen_version2()
            {
                // Todo: Konten aus Datei auslesen und in Liste ablegen
                List<CL_Konto> kontenliste = new List<CL_Konto>();
                CL_Konto kbuf = new CL_Konto();
                kbuf.kontonr = 4900;
                kbuf.kontobez = "Sonstige Kosten";
                kontenliste.Add(kbuf);
                MessageBox.Show(kontenliste.Count.ToString());
                kbuf.kontobez = "geändert";
                MessageBox.Show(kontenliste[0].kontobez); // Ausgabe:"geändert", also falsch
                // so geht es auch nicht:
                kbuf.kontobez = kontenliste[0].kontobez;
                MessageBox.Show(kbuf.kontobez); // Ausgabe auch hier "geändert", wieder falsch
                return true;
            }
    
        }
    

    Version 1 speichert nur das Textfeld des Datensatzes, ist natürlich Unsinn aber es funktioniert.

    Bei Version 2 (das ist das, was gewünscht ist) geschieht seltsames:
    Der Datensatz wird der Liste übergeben, und diese zeigt auch den richtigen Wert.

    Dann wird der Datensatz geändert, die Änderungen werden aber nicht der Liste übergeben. Trotzdem zeigt die Liste jetzt den geänderten Text.

    Und wenn man das zurückschreiben will, dasselbe. Nimmt man mehrere Datensätze, wird für alle der Wert des letzten zugewiesenen Elements angezeigt, also völlig falsch.

    Die einzig logische Erklärung dafür wäre, daß die Liste die Elemente gar nicht speichert, sondern lediglich einen Adressoperator auf den Speicherplatz von kbuf enthält.

    Sie funktioniert also nicht als Liste, sondern nur als Zeiger. Was mach ich falsch?

    Pascal



  • pascal2009 schrieb:

    Ich denke, ein Wert-Typ wird einfach auf den Stack gepackt.

    Wenn ein Wertetyp ein Feld einer Klasse ist, wird er nicht auf dem Stack gespeichert.
    Konkret: Du hast ein Integer in einer Klasse und legst nun ein Objekt dieser Klasse an. Dann befindet sich der Integer nicht auf dem Stack. Wie sollte das auch funktionieren? Der Stack ist für methodenlokale Wertetypen und Referenzen (nicht Objekte hinter einer Referenz!) und Übergabe- sowie Rückgabeparameter.
    Aber wie schon gesagt spielt es kaum eine Rolle wo was gespeichert wird. Mit new konstruierst Du ein neues Objekt und für Wertetypen ist das nicht zwingend notwendig.
    string ist eine Ausnahme. Zwar Klasse, aber der Compiler zaubert ein bisschen, damit es sich wie ein Wertetyp anfühlt. So kannst Du einer string-Referenz die null ist ein Literal zuweisen, ohne das Objekt vorher mit new zu konstruieren.



  • pascal2009 schrieb:

    Bei Version 2 (das ist das, was gewünscht ist) geschieht seltsames:
    Der Datensatz wird der Liste übergeben, und diese zeigt auch den richtigen Wert.

    Dann wird der Datensatz geändert, die Änderungen werden aber nicht der Liste übergeben. Trotzdem zeigt die Liste jetzt den geänderten Text.

    Kurz: In der List werden nur die Referenzen gespeichert. Die sind vergleichbar mit Zeigern aus der C und C++ Welt.
    CL_Konto ist eine Klasse und damit Referenztyp. Wenn Du testweise mal CL_Konto zu einer struct (Wertetyp!) umwandelst, wird Deine Änderung an kontobez sich nicht mehr auf das Objekt in der List auswirken.

    Ich sage damit nicht, dass Du generell alles als structs modellieren sollst. Der von Dir beobachtete Effekt ist oft ein Segen und genau das was man möchte. Erfordert aber etwas umdenken, wenn man von C/C++ kommt.

    Nachtrag: Wie im vorherigen Post schon geschrieben ist string eine Ausnahme. Obwohl eine Klasse, beobachtest Du im ersten Test ein anderes Verhalten als im zweiten. string ist ein "immutable reference type" dem zusätzlich ein wenig Compilermagie beim Zuweisungsoperator spendiert wurde. Um den Unterschied zwischen Referenz- und Wertetypen zu verstehen, solltest Du string vielleicht vorerst mal außer acht lassen, denn das Verhalten dieser Klasse passt absolut nicht zum Rest der C#-Welt.



  • µ schrieb:

    pascal2009 schrieb:

    Bei Version 2 (das ist das, was gewünscht ist) geschieht seltsames:
    Der Datensatz wird der Liste übergeben, und diese zeigt auch den richtigen Wert.

    Dann wird der Datensatz geändert, die Änderungen werden aber nicht der Liste übergeben. Trotzdem zeigt die Liste jetzt den geänderten Text.

    Kurz: In der List werden nur die Referenzen gespeichert. Die sind vergleichbar mit Zeigern aus der C und C++ Welt.
    CL_Konto ist eine Klasse und damit Referenztyp. Wenn Du testweise mal CL_Konto zu einer struct (Wertetyp!) umwandelst, wird Deine Änderung an kontobez sich nicht mehr auf das Objekt in der List auswirken.

    Ich sage damit nicht, dass Du generell alles als structs modellieren sollst. Der von Dir beobachtete Effekt ist oft ein Segen und genau das was man möchte. Erfordert aber etwas umdenken, wenn man von C/C++ kommt.

    Nachtrag: Wie im vorherigen Post schon geschrieben ist string eine Ausnahme. Obwohl eine Klasse, beobachtest Du im ersten Test ein anderes Verhalten als im zweiten. string ist ein "immutable reference type" dem zusätzlich ein wenig Compilermagie beim Zuweisungsoperator spendiert wurde. Um den Unterschied zwischen Referenz- und Wertetypen zu verstehen, solltest Du string vielleicht vorerst mal außer acht lassen, denn das Verhalten dieser Klasse passt absolut nicht zum Rest der C#-Welt.

    Vielen Dank für die schnelle Antwort. Mittlerweile war ich durch Rumprobieren schon auf der Spur, daß die Liste keine Kopien der übergebenen Objekte anlegt (mit einem Adreßverweis auf den neuen Speicher), sondern einfach einen Verweis auf den vorhandenen Speicherbereich des Elements setzt. Da kbuf als Buffer dient, in dem Sinne, das er ständig überschrieben wird zum Lesen und Schreiben, daher also ständig verändert wird und in der Methode auch für Zuweisungen an andere Datentypen (z. B. bei mehreren Listen) verwendet werden könnte, wäre rückwirkend das letzte Element der jeweils letzten Liste beschädigt, und das ist unerwünscht.

    Abhilfe: Also muß man 1. dem Buffer vor jeder neuen Wertzuweisung mit new neuen Speicher zuweisen, und wenn das erledigt ist, nochmal mit new einen neuen Speicherbereich, so daß er sozusagen ins Leere zeigt, dann kann er in der Liste keinen Unsinn anstellen. So ginge es:

    public bool init_Kontenrahmen_version2()
            {
                // Todo: Konten aus Datei auslesen und in Liste ablegen
                List<CL_Konto> kontenliste = new List<CL_Konto>();
    
                CL_Konto kbuf = new CL_Konto();
                kbuf.kontonr = 4900;
                kbuf.kontobez = "Sonstige Kosten";
                kontenliste.Add(kbuf);
    
                kbuf = new CL_Konto(); // ohne das würde nur der erste Buffer wieder überschrieben
                kbuf.kontonr = 4920;
                kbuf.kontobez = "Telefon";
                kontenliste.Add(kbuf);
    
                MessageBox.Show(kontenliste[0].kontobez); //Sonstige Kosten
                MessageBox.Show(kontenliste[1].kontobez); // Telefon
    
                kbuf = new CL_Konto(); // Zeiger vom letzten Element herunternehmen
                kbuf.kontobez = "geändert"; // so daß sich das nicht  auf die Liste auswirkt
    
                MessageBox.Show(kontenliste[0].kontobez); //Sonstige Kosten
                MessageBox.Show(kontenliste[1].kontobez); // Telefon
                return true;
            }
    

    Das erscheint mir aber nicht sehr elegant und außerdem fehleranfällig, wenn man es mal vergessen sollte. So ein Fehler, daß nur ab und zu jeweils das eine oder andere Element beschädigt ist, wäre gar nicht einfach aufzuspüren.

    Vielleicht wär in dem Fall wirklich eine Struct besser.

    Pascal



  • pascal2009 schrieb:

    Abhilfe: Also muß man 1. dem Buffer für jeden neuen Wert mit new neuen Speicher zuweisen, und wenn das erledigt ist, nochmal mit new einen neuen Speicherbereich, dann kann er in der Liste keinen Unsinn anstellen.

    Ja und das ist völlig in Ordnung. Mehrere Objekte erstellst Du mit mehrfachem new.

    pascal2009 schrieb:

    Das erscheint mir aber nicht sehr elegant und außerdem fehleranfällig, wenn man es mal vergißt.

    Vielleicht wär in dem Fall wirklich eine Struct besser.

    Nein! Verfalle jetzt nicht wieder in die C-Denke.

    Du kannst auf kbuf übrigens ganz verzichten.
    Mit entsprechendem Konstruktor:
    kontenliste.Add(new CL_Konto(4920, "Telefon"));
    Oder mit öffentlichen Properties:
    kontenliste.Add(new CL_Konto{ kontonr = 4920, kontobez = "Telefon"});



  • µ schrieb:

    Nein! Verfalle jetzt nicht wieder in die C-Denke.

    Du kannst auf kbuf übrigens ganz verzichten.
    Mit entsprechendem Konstruktor:
    kontenliste.Add(new CL_Konto(4920, "Telefon"));
    Oder mit öffentlichen Properties:
    kontenliste.Add(new CL_Konto{ kontonr = 4920, kontobez = "Telefon"});

    Ja, der Buffer ist C-Denke. Da er auch nicht so funktioniert wie ursprünglich gedacht, lasse ich ihn demnächst weg.

    Dieser Code sieht auch viel eleganter aus. und mehr nach C#;

    Und er ist sicherer, weil man da nichts vergessen kann.

    Pascal

    PS wäre nur die Frage: in C speichere ich mittels des Buffers solche Datensätze binär und brauche ihn natürlich, um die Datei wieder auszulesen (mit sizeof(buffer)). In C# könnte man den Datensatz in String umformen und als Textdatei speichern (und nach dem Auslesen wieder zerlegen), oder man macht das binär vielleicht mit serialize (was wahrscheinlich deutlich einfacher wäre, hab die Funktion aber noch nicht ausgetestet).



  • Serialisierung ist schon das richtige Stichwort. Das geht sowohl Binär als auch in Textform (xml).



  • hustbaer schrieb:

    string dagegen ist ein Value-Type, und der kann direkt in der ArrayList gespeichert werden.

    Bloß ein Schreibfehler, aber nicht dass es noch jemand glaubt: string ist natürlich ein Referenz-Typ.



  • Der Weg zu C# ist schon steinig. Man kann auch nicht immer alles irgendwo googlen. Im Moment geht es trotz googeln an diesem Punkt nicht weiter:

    Es wird eine Liste erstellt List<ClassPerson> mit string Name und int nr und aufgefüllt mit einigen Datensätzen. Diese Liste soll nun von der aufrufenden Methode weiterverarbeitet bzw. benutzt werden, und was ich nicht hinkriege, ist die Zuweisung.

    Wenn List<ClassPerson> als Rückgabewert mit return zurückkommt, und in der aufrufenden Methode eine List<ClassPerson> mit new definiert ist, kann ich mit List1=handle.fuelleListe() = Rückgabewert der Funktion nicht zuweisen, obwohl innerhalb derselben Methode List1=List2 durchaus funktioniert (da aber keinen Sinn macht).

    Übergabe mit ref oder out?

    Vom Praktischen her wäre das Problem in WinForms schon gelöst, da ich z. B. listBox1 mit den Daten aus der Datei füttern und via SelectedItem in der ganzen WinForms-Umgebung problemlos benutzen kann (wenn auch nur im string-Format, also ständig hin- und herverwandeln). Eine Liste mit strukturierten Daten wäre das Gewünschte.

    In Ansi-C wäre es überhaupt kein Problem ... lassen wir das. Das führt hier nicht weiter.

    Mir fehlt im Moment wirklich die zündende Idee. 😮

    Pascal

    PS: Die Ansi-C-Denke (soll bzw. muß ja ausgemerzt werden, aber kleines Beispiel, was ich eigentlich will). Der Code hier ist aus einem laufenden Programm, was ich für die FiBu benutze. Das ist eine dynamische Liste zu Fuß programmiert, die eine Binärdatei einliest, Datenelement ist eine struct, übergeben wird die Startadresse des ersten Elements der Liste (da wäre natürlich List in C# einiges einfacher, wenn man es denn mal verstanden hätte):

    struct s_element *kopiere_datei_in_liste(char *dname)
    {
            struct s_element *listenkopf=NULL;       
            struct s_element *laufzeiger=NULL;
            struct s_bsatz buffer;     
            FILE *bdp;
            bdp=fopen(dname,"rb");
            if (bdp==NULL){meldung("Kann Datei",dname,"nicht oeffnen");return;}
            while (fread(&buffer,sizeof(struct s_bsatz),1,bdp)==1)
            {
               if (listenkopf==NULL){listenkopf=element_anhaengen(listenkopf);laufzeiger=listenkopf;}
               else laufzeiger=element_anhaengen(laufzeiger);
               laufzeiger->data=buffer;
            }                         
          fclose(bdp);  
          return listenkopf;
    }
    

    Element anhängen ist so definiert (im Grunde ist der Code C# und AnsiC irgendwo von der Logik sehr ähnlich, fast schadet es aber von AnsiC nach C#, weil die Ähnlichkeit eben nur oberflächlich ist, wahrscheinlich besser, man würde bei 0 anfangen, jedenfalls Z. B. malloc und new da sticht die Ähnlichkeit ja wirklich ins Auge:

    struct s_element *element_anhaengen(struct s_element *vorgaenger)
    {
        struct s_element *neues;
        neues=malloc(sizeof(struct s_element));
        neues->next=NULL;
        if (vorgaenger==NULL) neues->prev=NULL;
        else {neues->prev=vorgaenger;vorgaenger->next=neues;}
        return neues;
    }
    

Anmelden zum Antworten