Dasselbe und das gleiche – oder: Storage by Value vs. Storage by Reference

Eine der schönen Eigenschaften an der deutschen Sprache, die Lernende gelegentlich zur Weißglut treibt, ist die Möglichkeit, in einzelnen Worten etwas auszudrücken, das andernfalls umständlich umschrieben werden müsste.

So macht es beispielsweise einen fundamentalen Unterschied, ob man das gleiche Fahrrad fährt wie ein:e Kolleg:in, oder ob man dasselbe Fahrrad fährt. Das gleiche bedeutet einfach, dass beide Kollegen jeweils ein Fahrrad eines bestimmten Modells fahren. Es gibt dann insgesamt zwei Fahrräder, die gleich aussehen. Dasselbe Fahrrad hingegen müsste schon ein Tandem sein, denn dann würden beide Kolleg:innen gemeinsam darauf fahren.

Als Eselsbrücke: Das gleiche Fahrrad gleicht dem anderen. Dasselbe Fahrrad ist das Fahrrad selbst.

Im allgemeinen Sprachgebrauch werden die Begriffe aber häufig vermischt. Das ist auch nicht weiter schlimm, weil man üblicherweise am Kontext erkennt, ob von ein und demselben Gegenstand, oder zwei gleichartigen Gegenständen die Rede ist. Wen das verwirrt und wer deshalb geneigt ist, das Studium der deutschen Sprache aufzugeben sei getröstet: Ihr seid nicht alleine! Vor einem ähnlichen Problem stehen nämlich alle, die erstmals eine objektorientierte Programmiersprache lernen.

Bei Programmiersprachen ist der Empfänger aber kein Mensch, der sich aus dem Kontext herleiten kann, dass es unwahrscheinlich ist, dass zwei Kolleg:innen auf einem Fahrrad sitzen, sondern ein Compiler und der macht exakt das was mach ihm sagt und versucht dabei so wenig Speicher wie möglich zu verschwenden. Wenn man den Compiler also anweist: „Hey, das hier ist dieselbe Datenmenge wie die von vorhin“, sagt der Compiler „Prima! Dann muss ich auch nur einmal Speicher allokieren“. Das nennt sich Storage by Reference und würde in Swift würde etwa so folgendermaßen aussehen:

class Bicycle {
	var model: String
	var kilometersDriven: Float = 0.0

	init(model: String) {
		self.model = model
	}
}

class Colleague {
	let name: String
	var bike: Bicycle?

	init(named name: String) {
		self.name = name
	}
	
	func buyBike(_ newBike: Bicycle) {
		self.bike = newBike
	}

	func driveBike(distance: Float) {
		guard var bike = self.bike else {
			fatalError("\(self.name) has no bike at the moment!")
		}
		bike.kilometersDriven += distance
	}
}

Wir haben eine Klasse Bicycle mit den Eigenschaften model und kilometersDriven, sowie eine Klasse Colleague mit den Properties name und (optional) bike. Ein:e Kolleg:in kann mit der Methode buyBike ein Fahrrad kaufen und es mit der Methode driveBike fahren. Dabei werden die gefahrenen Kilometer auf das Fahrrad drauf gerechnet. Soweit ist noch alles klar. Jetzt erzeuge ich zwei Kollegen Pjotr und Eleni, lasse beide ein Centurion Crossfire kaufen und schicke Pjotr auf eine 120 Kilometer lange Tour:

let pjotr = Colleague(named: "Pjotr")
let eleni = Colleague(named: "Eleni")
let centurionRacingBike = Bicycle(model: "Centurion Crossfire")
pjotr.buyBike(centurionRacingBike)
eleni.buyBike(centurionRacingBike)
pjotr.driveBike(distance: 120.0)

Eleni begutachtet am nächsten Tag ihr eigenes Fahrrad und was muss sie dabei feststellen?

if let elenisBike = eleni.bike {
    print("Kilometers driven on Eleni's bike: \(elenisBike.kilometersDriven)")
}
//Output: Kilometers driven on Eleni's bike: 120.0

Ihr neues Fahrrad hat schon 120 Kilometer auf dem Buckel, obwohl sie es noch kein einziges mal gefahren ist! Was ist hier passiert? Eleni und Pjotr haben nicht das gleiche, sondern dasselbe Fahrrad!

Das liegt daran, dass die Methode buyBike gar kein Fahrrad als Parameter bekommt, sie bekommt einen Verweis auf den Ort, an dem ein Fahrrad abgestellt ist. Man kann sich das als Dialog etwa so vorstellen:

Pjotr: "Hallo, ich möchte gerne ein Fahrrad kaufen! Model Centurion Crossfire, haben Sie das da?"
Fahrradverkäufer: "Ja, aber ich habe nur genau eins. Es steht in der Fahrradbox mit der Nummer 0x00F53D. Sie können damit machen was Sie wollen, es muss aber wieder in dieser Box geparkt werden"
Pjotr: "Prima, danke! Ich fahre später mal eine Runde"
Einige Zeit später betritt Eleni den Laden:
Eleni: "Hallo, ich möchte gerne ein Centurion Crossfire kaufen"
Fahrradverkäufer: "Eins hab ich, ja. Es steht in der Fahrradbox mit der Nummer 0x00F53D, es gehört Ihnen! Aber stellen Sie es bitte immer wieder zurück in die Box"
Eleni: "Prima, danke! Ich fahre aber erst morgen damit"
Dann fährt Pjotr seine Tour und als Eleni am nächsten Tag kommt wurde ihr Fahrrad benutzt.

Der Vergleich hinkt ein wenig, weil ein Fahrrad natürlich nicht von mehreren Parteien gleichzeitig genutzt werden könnte, ohne dass diese das mitbekommen, aber bei Objekten funktioniert das durchaus und das kann zu dem eben beschriebenen unerwarteten Verhalten führen.

Es gibt zwei Möglichkeiten damit umzugehen. Erstens, wir bleiben bei Storage by Reference, aber Pjotr und Eleni bekommen Verweise auf unterschiedliche Fahrräder:

let centurionRacingBikePjotr = Bicycle(model: "Centurion Crossfire")
pjotr.buyBike(centurionRacingBikePjotr)
let centurionRacingBikeEleni = Bicycle(model: "Centurion Crossfire")
eleni.buyBike(centurionRacingBikeEleni)
pjotr.driveBike(distance: 120.0)
if let elenisBike = eleni.bike {
    print("Kilometers driven on Eleni's bike: \(elenisBike.kilometersDriven)")
}
//Output: Kilometers driven on Eleni's bike: 0.0

Als Dialog:

Pjotr: Hallo, ich möchte ein Fahrrad!
Verkäufer: Ich habe extra für Sie eins angefertigt, es steht in der Box Nummer 0x00F53D.
Eleni: Ich möchte auch so ein Fahrrad!
Verkäufer: Für Sie habe ich auch eins angefertigt, es steht nebenan in der Box 0x00F53E.

Eine zweite Möglichkeit sieht im Code eleganter aus und ist auch kundenfreundlicher: Statt dass der:die Kund:in im Laden nach irgendwelchen Parkplatznummern suchen und außerdem fürchten muss, dass das Fahrrad an dieser Stelle von anderen Kunden ebenfalls genutzt wird, überreicht man ihm:ihr einfach das Fahrrad direkt beim Kauf. Das nennt sich Storage by Value und unterscheidet sich in der obigen Implementierung nur dadurch, dass Bicycle jetzt als struct statt als class gekenntzeichnet wird:

struct Bicycle {
    var model: String
    var kilometersDriven: Float = 0.0

    init(model: String) {
        self.model = model
    }
}

let pjotr = Colleague(named: "Pjotr")
let eleni = Colleague(named: "Eleni")
let centurionRacingBike = Bicycle(model: "Centurion Crossfire")
pjotr.buyBike(centurionRacingBike)
eleni.buyBike(centurionRacingBike)
pjotr.driveBike(distance: 120.0)
if let elenisBike = eleni.bike {
    print("Kilometers driven on Eleni's bike: \(elenisBike.kilometersDriven)")
}
//Output: Kilometers driven on Eleni's bike: 0.0

Wird ein struct statt einer class als Parameter an eine Methode übergeben, arbeitet die Methode daraufhin nicht mit einem Verweis auf die ursprüngliche Datenmenge, sondern mit einer wertgetreuen Kopie. Ein letztes mal gehen wir noch zurück ins Geschäft:

Pjotr: Hallo, Fahrrad?
Verkäufer: Sie schon wieder! Ja, ich habe hier eins, aber das gebe ich nicht her. Ich kann Ihnen dieses Modell aber nachbauen, dann können Sie es mitnehmen.
Eleni: Hallo, ich..
Verkäufer: Ja, Sie kriegen auch einen Nachbau. Verschwindet ihr zwei dann endlich mal? Ihr stellt den ganzen Laden auf den Kopf!

Die Quintessence dieses Artikels ist: Es gibt Gemeinsamkeiten zwischen menschlichen Sprachen und Programmiersprachen, aber man darf nie vergessen, wer zuhört: Computern muss man alles haarklein erklären, man kann nicht wie bei Menschen auf die Interpretationsfähigkeit des Gegenübers hoffen. Aber dafür muss man sie auch nicht fürchten.

Add a comment

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert