C# merkwürdige Variablen Änderung

xparet0209

Ensign
Registriert
Okt. 2007
Beiträge
180
Hi lieber CB'ler
ich habe ein Problem bei folgender Sache. Ich habe eine Klasse GameState:

PHP:
    public class GameState
    {
        public List<Tower> towers;

        public Tower getTower(int city,int slot)
        {
            Tower temp = new Tower(city,slot);
            foreach (Tower t in towers)
            {
                if (t.city == city && t.slot == slot)
                {
                    temp = t;
                    break;
                }
            }
            return temp;
        }
        // weitere methoden und variablen sind unwichtig
    }
und eine weiter Klasse Tower die ein paar int Werte beinhaltet und ein paar Methoden um diese int Werte zu verändern.
Ihr könnt euch das Spiel Manhattan vorstellen. Man hat ein Spielfeld mit verschiedenen Städten. Jede Stadt hat wiederum verschiedene Bauplätze (Slots), auf denen Türme errichtet werden können. Man kann nun in einer bestimmten Stadt auf einem Bauplatz einen Turm gewisser Höhe bauen-

Bei folgenden Aufruf verstehe ich allerdings nicht was passiert:
PHP:
            GameState state = new GameState();
            GameState temp = state;
            temp.getTower(0, 0).addPart(PlayerColor.BLUE, 3);
In der ersten Zeile wird ein neues Objekt vom Typ GameState erschaffen namens "state". Dieer GameState wird nun kopiert in ein zweites Objekt namens temp. In der dritten Zeile wird vom GameState temp der Tower in der Stadt 0 und am Slot 0 zurückgeliefert und die addPart() - Methode angwendet. Der blaue Spieler legt also auf den Platz einen Turm der Größe 3.

Das heißt, eigentlich müsste sich in temp und state nichts verändert haben, da die addPart()-Methode auf den Tower angewendet wird, der von getTower() zurückgeliefert wurde. Wenn ich aber im DebugModus nachschaue, dann wurden sowohl im temp als auch im state Objekt der Turm an der Stelle (0,0) um 3 erhöht. Was geht da vor sich?
Vorallem wieso ändert sich das state Objekt ob wohl ich doch nur auf das temp Objekt zugreife?

Ich hoffe ich konnte mein Problem verständlich darstellen :D . Es wäre sehr nett wenn mir jemand das Ereignis erklären kann.

mfg xparet0209

PS: Der Konstruktor der GameState Klasse setzt die ganzen towers, die er als Parameter überliefert bekommt...
Ich hab das in dem Beispiel mal rausgenommen da das nicht so wichtig ist (denke ich)
 
Zuletzt bearbeitet:
Weil du hier mit Referenzen arbeitest und nicht mit Kopien ;)

um eine Kopie von einer eigenen Klasse zu erstellen musst du dafür eine Copy - Methode schreiben.
public Tower Copy()
{
return new Tower() { a = this.a }; // etc. blabla
}

Das Copy geht auch anders, ist hier nur als Beispiel zur Verdeutlichung ^^
 
Zuletzt bearbeitet:
Das eine ist wie schon in #2 gesagt, du arbeitest mit Referenzen. temp und state referenzieren dasselbe Objekt, daher sieht man die Änderungen bei beiden.

Das andere Problem:
Code:
GameState temp = state;
temp.getTower(0, 0).addPart(PlayerColor.BLUE, 3);
entspricht
Code:
GameState temp = state;
temp.getTower(0, 0);
temp.addPart(PlayerColor.BLUE, 3);
Daher rufst du die addPart-Methode auf dem Objekt auf, das temp referenziert, da "getTower" als Rückgabewert temp den Tower zuweist. Es ist nicht so, dass du "addPart" auf dem Tower von "getTower" durchführst und temp davon vollkommen unbeeindruckt bleibt.
 
Nicht direkt zum Thema, aber kleine Stil-Anmerkung: den new Tower, den Du bei nicht-finden zurücklieferst, musst Du nur instanziieren, wenn Du auch nichts findest. Also:
PHP:
public Tower getTower(int city,int slot)
{
        foreach (Tower t in towers)
        {
                if (t.city == city && t.slot == slot)
                {
                        return t;
                }
        }
        return new Tower(city,slot);
}
oder mit Linq:
PHP:
public Tower getTower(int city,int slot)
{
        return (from t in towers 
                where t.city == city && t.slot == slot 
                select t).FirstOrDefault()
                ?? new Tower(city,slot);
}
Zumindest die 1. Variante ist absolut äquivalent. Eventuell hilft Dir das gedanklich etwas auf die Sprünge, wo der "Denkfehler" hing -- in Sprachen wie Java und C# wird ohne manuelles Zutun nie eine Speicherstruktur (außer primitive Typen/ValueTypes) kopiert, auch nicht, wenn sie als Parameter einer Methode übergeben wird. Das mag in anders sein als in anderen Sprachen (C++?).
 
Zuletzt bearbeitet:
Danke für die Infos. Jetzt verstehe ich warum Änderungen an temp auch welche an state nach sich ziehen... Ich wusste ja nicht das komplexere Objekte nur referenziert werden.

Jedoch weiß ich nicht genau wie ich das Problem behebe.

@Freeman4gu
PHP:
public Tower Copy()
{
  return new Tower() { a = this.a }; // etc. blabla
}
Das Beispiel kann ich nicht nachvoll ziehen. Wieso steht hinter dem Aufruf des Tower Konstruktors {a = this.a} ? Und wieso muss ich eine Copy()- Methode für die Klasse Tower implementieren, wenn ich doch nur ein GameState kopieren möchte.


Ich schaue mir mal das ICLoneable Interface an. Folgender Link spricht von einem shallow copy und einem deep copy. http://en.csharp-online.net/ICloneable
Soweit ich verstanden habe ist deep copy das was ich suche oder?


Die Sache mit dem zurückgegebenen Tower auf dem dann Steine raufgesetzt werden...
Es wir also nur eine Referenz übergeben statt einem Objekt Tower? und wie wäre es, wenn ich in der GameState Klasse meine Tower-Liste als privat deklariere? Könnte ich dann mit der Referenz von außen auf die private Tower-Liste zugreifen und schreiben?
 
Objekte, deren Zugriff durch "private" beschränkt wird, lassen sich nicht von anderen Objekten aus direkt verändern. Umgeht man das irgendwie, beispielsweise durch eine Methode, die das Objekt dann doch zurückgibt, wäre das dasselbe wie wenn man die Referenz direkt kopiert. (Das heißt man kann durchaus auch Objekte, die ursprünglich in einer Klasse privat erzeugt wurden, außerhalb dieser Klasse referenzieren und dann dort damit arbeiten, wenn ich mich nicht täusche)
 
Also ich habe jetzt verstanden warum ich eine Clone / Copy - Methode für die Tower Klasse erstellen muss, denn wenn ich ein GameState kopieren möchte, muss ich die Tower auch mit kopieren und zwar nicht als Referenz. Daher die Clone Methode, die mir einen neuen Tower liefert.

Das Ganze war zwar etwas kompliziert, da die Tower im GameStae als liste gespeichert waren. Aber Google hilft weiiter:
http://stackoverflow.com/questions/222598/how-do-i-clone-a-generic-list-in-c

PHP:
        public static IList<T> Clone<T>(this IList<T> listToClone) where T: ICloneable
        {
                return listToClone.Select(item => (T)item.Clone()).ToList();
        }
Hat dann meine Liste ungewandelt. Allerdings versteh ich nicht was "...=>... " macht.
 
Also ich muss ehrlich sagen ich habe überhaupt nicht verstanden was ein Lamda Operator ist. Google lieferte mir nur weitere komplizierte Wikipedia Artikel (Lambda Kalkül). Ich glaube die Sache ist zu hoch für mich. Kann man das nicht mit einfachen Worten beschreiben?

Eine Frage zum Quellcode. Kann man auch schreiben:

PHP:
public static IList<T> Clone<T>(this IList<T> listToClone) where T: ICloneable         
{                
        return listToClone.Select((T)item.Clone()).ToList();         
}
 
Ein Lambda-Ausdruck verpackt grob gesagt eine Funktion in ein Objekt. Also wenn Du JavaScript kannst, ist der C#-Code
PHP:
Func<int, int, int> f = (x, y) => x * y;
int result = f.Invoke(2, 3); // result = 6
von der Idee her äquivalent zum JavaScript-Code:
PHP:
var f = function (x, y) { return x * y; };
var result = f(2, 3); // result = 6
In Deinem Fall gibst Du der Select-Methode also quasi die unbenannte Funktion
PHP:
function (T item) { return (T)item.Clone(); }
mit, die Select dann später mit irgendwelchen Parametern aufrufen kann um ein Ergebnis zu ermitteln. Dazu kommt, dass die Select-Methode eigentlich eher für Linq-Aufrufe gedacht ist. Hier wird also deklarativ programmiert. Ich würde das ganze eher so schreiben:
PHP:
return (from item in listToClone select (T) item.Clone()).ToList();

Für Anfänger ist das allerdings nicht gerade lesbar. In diesem Sinne ist diese Extension-Method zum Klonen generischer Listen etwas tricky. Wenn das für dich verständlicher ist, implementiere die Methode so:
PHP:
List<T> deeplyClonedList = new List<T>();
foreach (var i in listToClone) {
    deeplyClonedList.Add(i.Clone());
}
return deeplyClonedList;
Das macht genau das selbe. Deine Tower-Klasse muss dann natürlich auch ICloneable und damit die Clone-Methode implementieren:
PHP:
return new Tower { city = this.city, slot = this.slot };
 
Zuletzt bearbeitet:
Als Ergänzung zum obigen Code ( Copy-Paste von MSDN)

"Lambda-Ausdrücke werden in methodenbasierten LINQ-Abfragen als Argumente für Standardabfrageoperator-Methoden wie Where verwendet."

Natürlich sind Lambdas nicht nur auf LINQ beschränkt, aber dort wirst du sie am häufigsten sehen.

Die Extension, die du gepostet hast, z.B., benutzt auch LINQ (in fluenter Syntax).

Der Code ist übrigens vollkommen in Ordnung und der Lösung mit dem ForEach-Loop vorzuziehen.
Last Edit: Hab das geschrieben, bevor captmcneil seinen Post geändert hat :) War wohl etwas unglücklich ausgedrückt. Die Extension selbst kannst du natürlich auch via ForEach implementieren und auf LINQ komplett verzichten.
Das ICloneable Interface musst du so oder so implementieren. Siehe Beispiel unten:

Deine Tower-Klasse:
Code:
    public class Tower : ICloneable
    {
        private int _city;
        private int _slot;

        public Tower( int city, int slot )
        {
            // ...
        }

        #region Implementation of ICloneable

        public object Clone() { return new Tower( _city, _slot ); }

        #endregion
    }

Die List-Extension:
Code:
    public static class ListExtension
    { 
        public static IList<T> Clone<T>( this IList<T> listToClone ) where T : ICloneable
        {
            return listToClone.Select( item => (T) item.Clone() ).ToList();

            //obiges mal in Query-Syntax geschrieben
            var clonedItems = from item in listToClone
                              select (T) item.Clone();
            return clonedItems.ToList();
        }
    }

Ab hier kannst du dann auf jede List<ICloneable> die Clone() Methode aufrufen. IntelliSense ist clever genug, um es dir auch direkt anzubieten. Probier es einfach aus.

Code:
List<int> listOfInts = new List<int>();
List<Tower> listOfTowers = new List<Tower>();

Wenn du jetzt listOfInts schreibst, dann . (Punkt :)) wirst du nirgends ein Clone() finden.
Ganz im Gegenteil zu listOfTowers, hier wird dir Clone() angeboten.
 
Zuletzt bearbeitet:
Danke an die beiden letzten Post. Ich habe dadurch eine Vorstellung vom Lambda Ausdruck bekommen :). Ich habe mich letztenendes trotzdem für die Variante mit der foreach-Loop entschlossen. Was ich toll finde ist, dass IntelliSense mir die Clone-Methode direkt anbietet. :D

Nachmals ein großes Dankeschön an alle.:daumen:
 
Zurück
Oben