Teilen oder für mich behalten?

Es ist, wie es in meinen Kindertagen hieß, zum Mäusemelken: Da erhalte ich dankbar Anregungen zu einigen neuen Themen, und dann fehlt mir nach wie vor die Zeit, darauf einzugehen. Ich will aber doch wenigstens mal versuchen, ein wenig nachzulegen, auch wenn ich bis Ende März vermutlich keine neuen Romane verfassen kann.

Besagte Anfrage jedenfalls (vielen Dank nochmals dafür!) enthielt den Wunsch, ein wenig mehr über die verschiedenen Arten von Eigenschaften zu erfahren. Das ist, so meine ich, eine ganz großartige Anregung, und endlich auch mal wieder ein Thema, das die versprochenen Einsteigerhilfen ein Stückchen auszubauen hilft.

Orientierte Objekte

Was heißt es eigentlich genau, wenn wir in objektorientierten Programmiersprachen Objekte duch die Speichergegend schubsen? Es drängt sich ja unweigerlich die Idee auf, es würde immer ein ganzes Stückchen Speicher bewegt – so ein Objekt besitzt häufig jede Menge Eigenschaften, die wiederum Speicherplatz benötigen. Da ist doch immer viel Umschaufelei im Gange, wenn ich z.B. einem ImageWell ein Bild zuweise, oder?

Mitnichten, und zum Glück. Bekanntermaßen gehört Arbeitsspeicher zu den weniger schnellen Dingen in einem Computer, und das Hin- und Herbewegen von großen Speicherblöcken verbrät nach wie vor vergleichsweise viel Arbeitszeit, die der Nutzer viel besser nutzen könnte …

Nein, so ein Objekt, das ist eigentlich erst mal nur ein Stückchen reservierter Arbeitsspeicher. Und wenn wir das Objekt verwenden, benötigen wir nur einen Pointer – also unter 32 Bit 4 Byte, unter 64 Bit 8 Byte, um es genau zu identifizieren. Dieser Pointer zeigt auf den Beginn des reservierten Speicherblocks, und diese Information ist alles, was beim Objekthandling bewegt wird.

Da sich ein Objekt zumeist nicht selbst genügt, sondern noch so einige Eigenschaften und Methoden mit sich schleppt, muss es noch eine Möglichkeit geben, auch diese – die für sich selbst Teilabschnitte des reservierten Speicherbereichs sind – anzusprechen. Es gibt mehrere Arten, dies zu tun, im Endeffekt läuft es aber immer darauf hinaus, dass das Objekt eine Bibliothek mit sich führt, die Auskunft darüber gibt, in welchem Abstand zum Objekt-Pointer sich der gewünschte Speicherbereich befindet. Und je nachdem, ob es sich dabei um eine Eigenschaft oder Methode handelt, wird dieser Speicherbereich dann ausgelesen, überschrieben oder ausgeführt.

Objekt 1

Handle habe ich hier gesondert hervorgehoben, obwohl es ja auch nur eine Eigenschaft darstellt – aber eben gleichbedeutend mit dem Beginn des Speicherblocks, den das Objekt einnimmt.

Wenn Handle jetzt aber ganz normal überschreibbar wäre: Was würde passieren, wenn wir dort einen neuen Wert hineinschreiben? Richtig, das Objekt selbst wäre jetzt ein anderes – und wenn wir nicht genau aufpassen würden und sicherstellen, dass dieser Bereich auch wirklich ein Objekt derselben Klasse erhält, wäre die Verarbeitung dieses neuen Objekts eine sehr unsichere, um nicht zu sagen absturzgefährdete Angelegenheit. Und überhaupt: Was wäre jetzt mit dem reservierten Speicher des alten Objekts?

Handle darf also nicht überschrieben werden. Solange das Objekt existiert, muss sein Wert gleichbleiben, damit unser Programm das Objekt auch wiederfindet. Das bringt uns jetzt also zum Thema

Zutritt nur für Befugte

Ich habe hier mal ein kleines Desktop-Programm vorbereitet, damit Sie die folgenden Schritte einfacher nachvollziehen können. Laden Sie es und starten Sie es.

Hier passiert, wie Sie sehen, noch nicht allzu viel. Ich habe da eine sehr reduzierte Klasse angelegt – die Musterklasse. Die besitzt eine Eigenschaft Handle vom Typ Integer, der im Constructor, also wenn eine solche Instanz mit New erzeugt wird, der Wert 123456 zugewiesen wird. (Das würde natürlich so nicht laufen – Handle muss ja ein eindeutiger Wert sein. Den bekommt man entweder aus Methoden des Betriebssystems, wenn man eine neue Klasse via Declare erzeugt, oder die Klasse hat einen eigenen Zähler, der automatisch erhöht wird, wenn eine neue Instanz erzeugt wird. Tun wir aber mal so, als hätte das seine Richtigkeit und 123456 wäre aus irgendeiner Berechnung heraus entstanden, ok?)

Das Fenster Window1 öffnet sich, und in seinem Open-Event wird eine neue Instanz meinMuster der Klasse Musterklasse erzeugt. Diese können Sie direkt ansehen, denn der Break-Befehl, der nun folgt, führt Sie in den Debugger.

Musterklasse1

Klicken Sie mal auf den Link „Musterklasse“ hinter meinMuster rechts unten. Jetzt sehen Sie auch den Wert von Handle, oder?

Jetzt beenden Sie bitte die Demo-App, gehen im Navigator in den Open-Event-Handler und fügen Sie eine Zeile hinzu, sodass sich alles wie folgt liest:

Dim meinMuster As New Musterklasse
meinMuster.Handle = 345761
break

Und führen Sie es wieder aus. Wir können den Wert einfach überschreiben. Soll ja nicht so sein. Was tun?

Klicken Sie im Navigator auf die Property Handle der Musterklasse und setzen Sie im Inspector den Scope, die Schreibberechtigung sozusagen, auf Private. Das sollte in etwa so aussehen:

Musterklasse2 Scope

Wenn Sie das Programm jetzt neu kompilieren, gibt es eine Fehlermeldung. Warum?

Der Scope Private hat festgelegt, dass Handle nur noch von innerhalb der Klasse selbst verändert werden kann.

Ein Schreibzugriff von außerhalb ist unzulässig, und darüber beschwert sich der Compiler nun.

Löschen Sie die Zeile meinMuster.Handle = 345761 aus dem Open-Event des Fensters nun bitte wieder und starten Sie das Programm erneut. Das sollte jetzt wieder gehen, und Sie können im Debugger den Wert für Handle auch wieder einsehen.

Also alles fein, oder? Wozu gibt es dann einen Protected-Scope?

Das feine am objektorientierten Programmieren – unter anderem – ist die Vererbungsfähigkeit von Klassen. Sie können eine angelegte Klasse vielfach verzweigen und spezialisieren. Nehmen wir mal an, Sie programmieren irgendeine Form von Adressverwaltung. In der Regel beginnt man mit der Person (als Klasse), und dort mit dem Namen und, da dieser auch mal für mehrere Personen identisch sein kann, einer eindeutigen Identifikationsnummer. Ja genau – ein Handle!

Personen können je nach Anwendungsfall auch mal Kunden sein. Dann ist es sinnvoll, eine Unterklasse von Person anzulegen, aus der heraus Sie auf unterschiedliche Rechnungs- oder Lieferadressen zugreifen oder die Bestellhistorie einsehen können. Und ebenso benötigen Sie vielleicht eine Unterklasse für Mitarbeiter, bei denen wiederum ganz andere Eigenschaften zur Grundklasse Person hinzugefügt werden müssen – Posten, Gehalt, Einstellungsdatum … Aber beide greifen auf die Eigenschaften der Klasse Person zurück.

Um das jetzt mit unserer Musterklasse zu simulieren: Rechtsklicken Sie auf diese im Navigator und wählen Sie „New Subclass„. Xojo erzeugt eine neue Klasse, benennt diese „CustomMusterklasse“ und setzt ihren Super auf Musterklasse.

Das Fenster soll jetzt eine ebensolche erzeugen. Gehen Sie also bitte wieder in seinen Open-Event und ändern Sie die Klasse von meinMuster auf CustomMusterklasse. Desgleichen auch mit der Property meinMuster des Fensters.

Und jetzt nehmen wir mal Folgendes an: Die neue Unterklasse hat eine eigene Berechnungsmethode für das Handle. Fügen Sie CustomMusterklasse eine neue Methode hinzu, die Sie „Constructor“ nennen (wie gesagt, das ist die Methode, die bei einem „New“ aufgerufen wird), und überschreiben Sie das, was jetzt automatisch darin steht, mit einem

Handle = 987654

Klicken Sie jetzt auf Run, und was passiert? Der Compiler sagt Ihnen, dass eine Eigenschaft „Handle“ gar nicht existiert. Aber sie ist doch in der Elternklasse vorhanden?! Und Sie können sie sogar im Debugger anschauen.

Ändern Sie den Scope von Handle in Musterklasse nun bitte auf Protected. Klicken Sie jetzt auf „Run“ und schauen Sie mal im Debugger nach dem neuen Wert von Handle. Jetzt geht es also! Warum?

Der Scope Protected hat festgelegt, dass Handle nur innerhalb der Klasse und der Subklasse(n) verändert werden kann.

Ein kleiner, aber gewichtiger Unterschied! Sie können in Subklassen zwar jederzeit Eigenschaften und Methoden der Elternklasse überschreiben – das haben wir so mit dem Constructor gemacht und könnten theoretisch so auch ein eigenes Handle für die CustomKlasse erzeugen. Stellt sich dann aber die Frage, warum wir überhaupt eine Subklasse verwenden, wenn wir etwas so existenzielles wie das Handle nicht „bis auf den Boden“ durchschleifen.

Erklärt aber immer noch nicht die Frage nach den Computed Properties, oder?

Berechnete Eigenschaften – keine Quadratur des Kreises, aber nahe dran

In der Praxis bekommen Sie es häufiger mal mit Problemen der folgenden Natur zu tun:

  • Eine Methode liefert ein Array zurück. Sie benötigen aber regelmäßig ein definiertes Element davon.
  • Sie kommunizieren mit dem Betriebssystem und möchten einen von dort erhaltenen Wert als Property erhalten, ihn eventuell sogar zur schnellen Verfügbarkeit speichern und nicht jedesmal neu abrufen.
  • Bei der Veränderung einer Eigenschaft soll automatisch irgendeine Aktion durchgeführt werden.

Vermutlich sind dies nur wenige Beispiele aus einer Vielzahl von Anwendungsmöglichkeiten. Im Kern trifft auf sie alle zu:

Eine Computed Property ist eine Eigenschaft, die in eine Methode verpackt ist.

Falls Sie diese Aussage als zu abgehoben empfinden: Sie wird recht schnell klar, wenn Sie mit Rechts auf das Handle im Navigator klicken und „Convert to Computed Property“ anwählen.

Wie Sie sehen, ist aus Handle jetzt eine Private Property mHandle geworden. Und als Handle steht jetzt ein zweigeteiltes Konstrukt herum: Ebenfalls vom Typ Integer und mit zwei Methoden, nämlich Get und Set. Wenn Sie erstere unter die Lupe nehmen, finden Sie dort den Befehl

return mHandle

Aktuell macht diese Methode nichts weiter, als Ihnen den Wert der im Kern privaten, also nach außen unzugänglichen Eigenschaft mHandle zu geben. Und die Set-Methode macht ebenjenes umgekehrt: Sie übernimmt einen Integer-Wert von außen und speichert ihn intern.

Wir könnten statt des obigen schlauen Satzes also auch sagen:

Eine Computed Property besteht aus zwei Methoden, die bei Zugriff auf eine Eigenschaft aufgerufen werden.

Denn genau das passiert. Von außen bemerken Sie nicht, dass zwei Methoden am Arbeiten sind. Sie arbeiten einfach wie gewohnt mit Befehlen wie

meinMuster.meineProperty = irgendeinWert

oder

Dim meinWert As Integer = meinMuster.meineProperty.

Klassenintern wird aber eine Methode aufgerufen, die Sie ganz nach Herzenslust mit Funktionen füllen können. Statt einen internen Eigenschaftswert wie jetzt mHandle zu liefern, kann die Get-Methode auch das Betriebssystem nach einem Wert befragen. In diesem Fall ist ein Puffer wie mHandle gar nicht nötig, es sei denn, Sie wollen ihn aus Geschwindigkeitsgründen nur einmal berechnen und ansonsten auf diesen Wert zurückgreifen. Lazy Initializing nennt sich das und ist sehr beliebt, weil keine unnötigen Initialisierungen durchgeführt werden, deren Ergebnisse womöglich gar nicht benötigt werden.

Praktisch sähe das so aus (im Getter von meineProperty):

If m_meineProperty = 0 Then //oder auf NIL testen, je nachdem, welchen Datentyp Sie hier haben
BerechneMeineProperty // Die entsprechende Funktion müssten Sie natürlich entwerfen.
// Diese legt den berechneten Wert in m_meineProperty ab. Sobald er einmal da ist, muss er nicht mehr neu berechnet werden.
End If
Return m_meineProperty

Um die obigen Beispiele für Anwendungsfälle noch aufzugreifen: Alternativ könnte hier z.B. meinArray(0) zurückgegeben werden, wenn eine Funktion als Ergebnis ein Array liefert und sie einen bestimmten Wert daraus extrahieren wollen.

Das ist außerdem fürs Debugging sehr praktisch, da Sie die Ergebnisse von Computed Properties im Debugger einsehen können, nicht aber die Resultate von Methoden.

Und im Prinzip muss noch gar nicht einmal eine wirkliche Speicher-Eigenschaft hinter einer Computed Property stecken. Sie können Sie auch verwenden, um z.B. die Summe aus anderen Properties zu berechnen und weiterzugeben. Deshalb sollten wir unseren schlauen Satz noch einmal revidieren:

Eine Computed Property ist zwei Methoden (Get und Set), die so tun (und so angesprochen werden), als wären sie eine Eigenschaft.

Und wenn Sie die Set-Methode einer Computed Property ergänzen, können Sie bei Veränderung eines Werts weitere Methoden aufrufen oder Programmschritte hinzufügen, haben dann also eine Eigenschaft mit Funktion hintendran.

Sie können auch Getter oder Setter löschen und damit nach außen hin Eigenschaften erzeugen, die nur schreib- oder nur lesbar sind.

Also eine riesige Vielfalt an Möglichkeiten, die sich mit diesen Features erschließen.

Und wann teile ich nun?

Oh richtig, dann gibt es noch die Shared Properties. Als Shared Property oder Shared Method sollten Sie alles anlegen, was der ganzen Klasse einheitlich zur Verfügung stehen soll. In unserem Beispiel heißt das:

mHandle bzw. Handle soll ja ein Identifikator für die jeweilige Instanz sein. Aber wenn ich herausfinden will, welche Klasse ich damit vor mir habe? Dann füge ich eine Shared Property vom Typ Text oder String ein, nenne sie KlassenName und setze unter Default im Inspector den Vorgabewert auf „Musterklasse“. (Alles andere ist natürlich auch denkbar – ein Integer wie bei Handle, ein Ptr …)

Klassenname

Der Vorteil dabei? Ist zweierlei!

  • Sollte sich dieser Wert später einmal ändern müssen, erfolgt dies an dieser einzigen Stelle und gilt für alle Instanzen von Musterklasse und CustomMusterklasse.
  • Ich spare enorm Speicher und Verwaltungsaufwand, da die Eigenschaft KlassenName nur ein einziges Mal als Klasseneigenschaft existiert. Alle Instanzen meiner Klasse verweisen auf diesen einzigen Bereich im Speicher. Es wird in der Instanz selbst kein weiteres Byte dafür verwendet.

Das kann man sich also etwa so vorstellen: Nach außen hin sieht es aus, als ob die Instanzen meiner Klassen die geteilte Eigenschaft selbst besäßen. In Wirklichkeit gibt es da nur einen Verweis auf einen einzigen Speicherbereich, der im reservierten Speicher für die gemeinsamen Klasseneigenschaften existiert.

Objekt 2

 

Mit anderen Worten:

Eine Shared Property ist eine Klasseneigenschaft. Sie gehört nicht zu einer Instanz, sondern gilt für alle Instanzen der Klasse zugleich.

Und mit den Methoden verhält es sich ganz genauso. Sie können ihre Verfügbarkeit über den Scope eingrenzen: Völlig frei, für alle Subklasseninstanzen oder für die Instanz selbst – je nachdem, ob die Funktionen nach außen zur Verfügung stehen sollen, also von anderen Objekten aufgerufen werden sollen, ob sie innerhalb der Klassen und Subklassen benutzt werden sollen oder nur innerhalb der Klasse selbst.

Und wenn Sie eine Methode als Shared definieren, benötigen Sie keine Instanz, um sie aufzurufen. Das finden Sie etwa in Xojo-Methoden wie

Double.FromText (aText as Text) As Double

Das erzeugt eine neuen Double-Wert, wo vorher noch keiner war. Es wird in diesem Fall die Klassenmethode FromText der Klasse Double aufgerufen. Diese nimmt einen Textwert und liefert einen neuen Double zurück. Wenn Sie nur Instanzenmethoden verwenden, müssten Sie zunächst eine neue Instanz initialisieren und dann die Methode aufrufen. Das sähe dann also etwa so aus:

Dim D As Double
D = D.FromText (aText)

Nicht nur umständlicher, sondern auch leicht vorstellbar, dass auch hier wieder mehr Speicher verbraten würde. Und CPU-Zeit darüberhinaus. Eine Klasseninstanz muss sich stets auf sich selbst beziehen und den Offset der Methode in Relation zum eigenen Handle berechnen – sie erinnern sich an die bunte Grafik oben? Eine Klassenmethode dagegen – eine Shared Method also – steht während der Laufzeit konstant an gleicher Stelle zur Verfügung. Einmal Sprungpunkt berechnen reicht. Das summiert sich:

Speedtest

(Wenn Sie das selbst ausprobieren wollen: Das erweiterte „Projekt“ mit Geschwindigkeitstest finden Sie hier.)

Und wenn Sie sich nun fragen, wie Sie am besten mit der ganzen Thematik umgehen sollten:

Eine Grundidee des objektorientierten Programmierens ist die Verkapselung von Objekten – andere Objekte sollten nicht in deren Innereien rümwühlen und Dinge durcheinanderbringen können, sondern stets nur über definierte Zugangswege miteinander reden. Es ist deshalb überhaupt nicht verkehrt, um es vorsichtig auszudrücken, im Zweifel Eigenschaften privat zu deklarieren. Und wenn Sie merken, dass der Compiler dann Laut gibt, überlegen Sie sich gründlich: Können Sie die Eigenschaft gefahrlos öffentlich deklarieren? Oder ist es besser, sie über eine Computed Property zugänglich zu machen, um evtl. die Gültigkeit übergebener Werte zu prüfen, statt sie ungeprüft in Ihre Berechnungen einfließen zu lassen?

Und wenn es Ihnen um Performance und Optimierung geht: Die Vorzüge der Klassenmethoden sprechen ganz klar für sich, oder?

2 Gedanken zu “Teilen oder für mich behalten?

  1. Leider funktioniert der Download der Beispiele nicht. Würde gerne damit etwas experimentieren, denn bei den Shared Methoden steige ich einfach nicht durch.

    Gefällt mir

    1. Danke für den Hinweis! Ich habe früher in meinen Dropbox-Public Folder verlinkt, aber der funktioniert ja nicht mehr. Sollte jetzt klappen. Wo ähnliche Probleme existieren: Bitte einfach Bescheid geben!

      Gefällt mir

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Abmelden / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Abmelden / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Abmelden / Ändern )

Google+ Foto

Du kommentierst mit Deinem Google+-Konto. Abmelden / Ändern )

Verbinde mit %s