Sichtbarkeit Klassenvariablen


schiese

Erfahrenes Mitglied
#1
Hallo,

mir ist gerade folgendes Verhalten aufgefallen, das ich nicht verstehe.

Als Beispiel habe ich habe eine Klasse Flugzeug
PHP:
class Flugzeug {

    private $plaetze;

    function __construct() {
        $this->plaetze = 5;
    }

}
und eine Klasse Kleinflugzeug
PHP:
class Kleinflugzeug extends Flugzeug {

    function __construct() {
        $this->plaetze = 5;
    }

}
Ich erstelle mir nun ein Objekt vom Typ Kleinflugzeug und rufe die private Klassenvariable $plaetze der Klasse Flugzeug auf,
PHP:
$kleinflugzeug = new Kleinflugzeug();
echo "Anzahl Plätze im Kleinflugzeug: " . $kleinflugzeug->plaetze;
erhalte ich folgende Ausgabe:
Code:
Anzahl Plätze im Kleinflugzeug: 5
Ich verstehe hier nicht, weshalb ich von der Kindklasse aus, der privaten Klassenvariable der Elternklasse einen Wert zuweisen kann und anschließend über ein Objekt der Kindklasse auf diese zugreifen kann.

Ändere ich den Konstruktor der Kindklasse wie folgt
PHP:
function __construct() {

    }
erhalte ich die Ausgabe, die ich auch vorher erwartet habe
Code:
Notice:  Undefined property: Kleinflugzeug::$plaetze in C:\xampp\htdocs\xampp\CleanCode\testflugzeug.php on line 41
Anzahl Plätze im Kleinflugzeug:
Ich verstehe dieses Verhalten überhaupt nicht. Wenn ich im Konstruktor der Kindklasse einer privaten Klassenvariable der Elternklasse einen Wert zuweise, kann ich anschließend über ein Objekt der Kindklasse auf diese private Klassenvariable zuweisen, weise ich der privaten Klassenvariable der Elternklasse keinen Wert zu, ist sie dem Objekt der Kindklasse unbekannt. Aber wieso kann ich im Konstruktor der Kindklasse einer privaten Klassenvariablen der Elternklasse überhaupt einen Wert zuweisen?

Schöne Grüße

schiese
 

cwriter

Erfahrenes Mitglied
#2
Ein Beispiel:
PHP:
class Base {
  private $value;
  
  function __construct() {
     $this->value = 42; 
    }
  
  function baseValue() {
     return $this->value;
    }
}


class test extends Base {
  
  function __construct() {
      Base::__construct();
      $this->value = 123;
  }
  
  function testValue() {
     return $this->value; 
  }
}


$x = new test();
echo "value: " . $x->value . "<br>";
echo "baseValue: " . $x->baseValue() . "<br>";
echo "testValue: " . $x->testValue() . "<br>";

/* Ausgabe */
value: 123
baseValue: 42
testValue: 123
Ich erstelle mir nun ein Objekt vom Typ Kleinflugzeug und rufe die private Klassenvariable $plaetze der Klasse Flugzeug auf,
Der Denkfehler ist hier: Du kannst die private Klassenvariable nicht aufrufen, sie wird ja nicht in die Unterklasse exportiert. Du rufst stattdessen die im Konstruktor erstellte Member auf.

Ich verstehe hier nicht, weshalb ich von der Kindklasse aus, der privaten Klassenvariable der Elternklasse einen Wert zuweisen kann und anschließend über ein Objekt der Kindklasse auf diese zugreifen kann
Als einfacheres Beispiel: Wenn du in der Elternklasse eine Variable "x" als "public" kennzeichnest und in der Unterklasse ebenfalls eine Variable "x" als "public" markierst, wie ist das x dann ausgelegt? Hier "versteckt" die neue Definition die alte der Elternklasse, aber per Scoping solltest du immer noch darauf zugreifen können ("Base::$x"). Unqualifiziert ist jeder Zugriff implizit "Sub::$x".
In deinem Fall wird nicht wirklich versteckt, aber das Ergebnis ist dasselbe.

Im Prinzip hast du folgende Struktur:
Code:
Flugzeug:
    .plaetze

KleinFlugzeug:
    .Flugzeug:
        .plaetze // Private, nicht symbolisch verfügbar
    .plaetze // Lokal erstellt im Konstruktor, darauf greifst du zu

// Wenn du deinen Konstruktor leer lässt:
KleinFlugzeug:
    .Flugzeug:
        .plaetze // Private, nicht symbolisch verfügbar
Entsprechend ist natürlich die Property undefined, wenn du den Konstruktor leer lässt (da private, und keine Property in der Unterklasse definiert wurde).


Ich hoffe, das macht es klarer :)

Gruss
cwriter
 

Yaslaw

n/a
Moderator
#3
Aus der PHP-Doku:
Sichtbarkeit
Die Sichtbarkeit einer Eigenschaft oder Methode kann definiert werden, indem man der Deklaration eines der Schlüsselwörter public, protected oder private voranstellt. Auf public deklarierte Elemente kann von überall her zugegriffen werden. Protected beschränkt den Zugang auf Vaterklassen und abgeleitete Klassen (sowie die Klasse die das Element definiert). Private grenzt die Sichtbarkeit einzig auf die Klasse ein, die das Element definiert.
Du kanst also $plaetze auf protected umstellen. Dann kannst du es auch vom Konstruktor der Kindesklasse setzen. Private sollte nur für Properties verwendet werden, die wirklich nur in der Vaterklasse Gültigkeit haben sollten. Ansonsten nimm protected. Das gilt auch für die Methoden. Private Methoden sind nur in der Klasse aufrufbar, in der sie definiert wurden.
 

schiese

Erfahrenes Mitglied
#4
Hallo,
danke für eure Antworten. Mich hat nur verunsichert, dass ich eine Klassenvariable im Konstruktor deklarieren kann. Ich habe die Variable $plaetze ja nicht explizit in der Klasse Kleinflugzeug deklariert. Trotzdem legt der Konstruktor die Property (da kein Zugriffsmodifizierer angebeben hat sie die Sichtbarkeit public?) an und setzt den Wert. Das finde ich komisch. Ich hätte einen Fehler oder eine Warnung erwartet.

@Yaslaw ich weiß. Ich habe nur etwas rumgespielt und dann ist mir das aufgefallen. Ich konnte mir halt nicht vorstellen, dass eine Klassenvariable im Konstruktor angelegt werden kann.

Schöne Grüße

schiese
 

cwriter

Erfahrenes Mitglied
#5
Das finde ich komisch. Ich hätte einen Fehler oder eine Warnung erwartet.
Für Leute, die aus der C++/Java-Ecke kommen, ist das in der Tat sehr seltsam. Unter anderem, weil diese Sprachen jedem Objekt eine fixe Grösse zuweisen und die Identifier nur ein Alias für ein Offset von der Objekt-Basis sind.

In Python/JS (ES6) ist das aber durchaus üblich so.

Python:
class test:
    def __init__(self):
        self.asd = 123

class test2:
    asd = 123

t1 = test()
t2 = test2()

# t1.asd == t2.asd
In JS ist es noch etwas intuitiver, da jede Klasseninstanz ja nur ein Objekt ist, und jedes Objekt ja eigentlich nur ein Key-Value-Set (vereinfacht gesagt):
Javascript:
class test {
    constructor() {
        this.asd = 42;
    }
}
let t = new test();
/*
t: {
    asd: 42
}
, Zugriff per t.asd möglich
*/
let x = {};
x['asd'] = 123;
/*
x: {
    asd: 123
}
*/
Jedenfalls merke ich mir das so - vielleicht hilft das dir auch?

Gruss
cwriter
 

schiese

Erfahrenes Mitglied
#6
Hallo,

ich habe noch eine weitere Frage, die sich aus dem Kontext ergeben hat.

Ich habe folgenden Code
PHP:
class Flugzeug {

    public $plaetze = 200;

    function getPlaetze() {
        return "Elternklasse";
    }

}

class Kleinflugzeug extends Flugzeug {

    public $plaetze = 5;

    function getPlaetze() {
        return "Kindklasse";
    }

}
Ich habe die Property $plaetze in der Klasse Kleinflugzeug überschrieben. Ist es möglich auf die Variable der Elternklasse in der Klasse Kleinflugzeug zuzugreifen? Also, dass er mir 200 statt 5 ausgibt. Wären die Propertys static könnte ich ja mit
PHP:
parent::$plaetze
drauf zugreifen.

Viele Grüße

schiese
 

ComFreek

Mod | @comfreek
Moderator
#7
Das ist in keiner mir bekannten Sprache möglich. Das, was du machen möchtest, geht jedoch, wenn du es mit Methoden statt Properties modellierst:

PHP:
class Flugzeug {
    function getPlaetze() {
        return 200;
    }
}

class Kleinflugzeug extends Flugzeug {

    function getElternPlaetze() {
      return parent::getPlaetze();
    }

    function getPlaetze() {
        return 5;
    }
}
Das solltest du sowieso tun, weil die Anzahl der Plätze eine Konstante ist. Also entweder eine statische Konstante deklarieren (ich weiß nicht, ob das in PHP möglich ist) oder eine Getter Methode.
 

Yaslaw

n/a
Moderator
#8
Nö, wenn du etwas überschreibst, ist es überschrieben. Es ist ja auch nicht so, dass die Parentklasse noch irgendwo separat gehalten wird. Beide Klassen verschmelzuen sozusagen in ein Objekt.
 

cwriter

Erfahrenes Mitglied
#9
Das ist in keiner mir bekannten Sprache möglich.
C++? o_O

C++:
#include <iostream>

class base {
public:
    base() {
        x = 123;
    }
    int x;

};
class derived : public base {
public:
    derived() {
        x = 42;
    }

    int parentX() {
        std::cout << "Parent x: " << base::x << std::endl;
    }
    int x;
};


int main(int argc, char* argv[])
{
    auto d = new derived();
    d->parentX();
    std::cout << "Directly: " << static_cast<base*>(d)->x << std::endl;
    delete d;
}

/* Ausgabe:
Parent x: 123
Directly: 123
*/
Nö, wenn du etwas überschreibst, ist es überschrieben. Es ist ja auch nicht so, dass die Parentklasse noch irgendwo separat gehalten wird. Beide Klassen verschmelzen sozusagen in ein Objekt.
Hm, aber private Variablen werden ja nicht überschrieben. Wie sieht dann da die interne Struktur aus? Gibt es für die privaten Member ein eigenes Sub-Objekt?

Gruss
cwriter
 

Yaslaw

n/a
Moderator
#10
Hm, aber private Variablen werden ja nicht überschrieben. Wie sieht dann da die interne Struktur aus? Gibt es für die privaten Member ein eigenes Sub-Objekt?

Gruss
cwriter
Kurzer Test:
PHP:
<?php 
class Flugzeug {
    
    protected $plaetze = 200;
    
    function getPlaetze() {
        return $this->plaetze;
    }
    
    
}

class Kleinflugzeug extends Flugzeug {
    
    public $plaetze = 5;
    
    function getPlaetze() {
        return "Kindklasse";
    }
    
    function getParentPlaetze(){
        return parent::getPlaetze();
    }
    
}

$kl = new Kleinflugzeug();
echo $kl->getParentPlaetze();

?>
Ergibt 5
 

cwriter

Erfahrenes Mitglied
#11
Ok, das ist aber auch protected und nicht private :)

PHP:
class Flugzeug {
    private $plaetze = 200;
    function getPlaetze() {
        return $this->plaetze;
    }
}

class Kleinflugzeug extends Flugzeug {
   
    public $plaetze = 5;
   
    function getPlaetze() {
        return "Kindklasse";
    }
    function getParentPlaetze(){
        return parent::getPlaetze();
    }  
}

$kl = new Kleinflugzeug();
echo $kl->getParentPlaetze();
Ergibt 200 ;)

(Das ist nicht weiter verwunderlich: PHP: Objekt-Vererbung - Manual )
Wenn man zum Beispiel von einer Klasse erbt, so erbt die Unterklasse alle Methoden der Sichtbarkeiten public und protected von der Vaterklasse. Wenn eine Klasse diese Methoden nicht überschreibt, wird die ursprüngliche Funktionalität beibehalten.
Nur ganz verschmolzen wird ja in diesem Fall nicht - zumindest nicht intern auf den privaten Variablen.

Gruss
cwriter
 

ComFreek

Mod | @comfreek
Moderator
#12
Hm, aber private Variablen werden ja nicht überschrieben.
Aha! Ich ging davon aus, dass die (öffentliche?) Instanzvariable im Codeschnipsel meines Beitrags überschrieben wird.

Vielleicht als Erklärung für @schiese: Es gibt zwei Konzepte: Überschreiben ("overloading") und Überdecken ("name shadowing"). Gleich vorweg: Shadowing ist fast immer schlechter Stil und sollte vermieden werden.
Überschreiben:
PHP:
class Test {
  function foo() { return 5; }
}
class TestChild extends Test {
  function foo() { return 10; }
}

$obj = new TestChild
echo $obj->foo(); // 10

function bar(Test $obj2) {
  echo $obj2->foo();
}
bar($obj); // immer noch 10! "Bindung zur Laufzeit" ist das Stichwort
Überdecken: Siehe Code von @cwriter ein Beitrag über mir: Die Variablen "$plätze" wird überdeckt. Das heißt einfach nur, dass es zur Laufzeit pro Kleinflugzeug zwei Instanzvariablen gibt! Einmal das $plätze von Kleinflugzeug und einmal das $plätze von Flugzeug. Es ist nur so, dass du innerhalb des Codes von Kleinflugzeug das $plätze von Flugzeug nicht mehr referenzieren kannst. Du hast nämlich das $plätze der Elternklasse überdeckt ("shadowed") und kannst du auf das "innerste" $plätze im Scope zugreifen und das ist nun mal das $plätze von Kleinflugzeug.
Hingegen referenziert das $plätze im Code von Flugzeug immer das $plätze von Flugzeug! Deswegen wird auch 200 ausgegeben.
 

schiese

Erfahrenes Mitglied
#13
Danke für eure Antworten. Ich komme leider jetzt erst zum Antworten.
Wie @Yaslaw in Beitrag 10 geschrieben hat, wird ja die Klassenvariable aus der Elternklasse auch überschrieben/überdeckt, wenn sie nicht den Zugriffsmodifizierer private besitzt. Dann greift auch selbst die Methode getPlaetze() aus der Elternklasse auf die überdeckende Variable der Kindklasse zu. Das finde ich verwirrend. Weiter darf der Zugriffsmodifizierer der überdeckenden Variable der Kindklasse nur schwächer oder gleich dem Zugriffsmodifizierer der Elternklasse sein.

Ein weiteres Beispiel:

PHP:
class Flugzeug {

    protected $plaetze = 200;

    public function getPlaetze() {
        return $this->plaetze;
    }

    public function getPlaetzeVater() {
        return $this->plaetze;
    }

}

class Kleinflugzeug extends Flugzeug {

    protected $plaetze = 5;

    public function getPlaetze() {
        return parent::getPlaetze();
    }

}

$kleinflugzeug = new Kleinflugzeug();

echo $kleinflugzeug->getPlaetzeFlugzeug();
Hätte ich es in Java geschrieben, würde er 200 ausgeben, gibt allerdings 5 aus.

Schöne Grüße

schiese
 

ComFreek

Mod | @comfreek
Moderator
#14
Wie @Yaslaw in Beitrag 10 geschrieben hat, wird ja die Klassenvariable aus der Elternklasse auch überschrieben/überdeckt, wenn sie nicht den Zugriffsmodifizierer private besitzt.
In dem Fall wird sie, wie dein Beispiel zeigt, tatsächlich überschrieben und eben nicht überdeckt. Würde $plätze überdeckt werden, so würde 200 ausgegeben werden.

Wenn in Java 200 ausgegeben wird, so kann man in Java anscheinend keine Membervariablen überschreiben. Tatsache: If you overwrite a field in a subclass of a class, the subclass has two fields with the same name(and different type)?

Am besten verwendet man niemals gleichnamige Membervariablen, egal ob es die Programmiersprache hergibt oder nicht. Um das sicherzustellen, verwendet man am besten auch ein statisches Lintingtool :)
 

cwriter

Erfahrenes Mitglied
#15
Da die Diskussion nun doch etwas länger wurde, sollten wir das ein bisschen theoretischer betrachten.

Es gibt 2 Herangehensweisen für Klassenvererbungen:
1) Überschreiben (override)
2) Überdecken (~hide)

Mir ist nur eine Sprache bekannt, die beides zulässt: C++.
Gehen wir man ein paar Schritte zurück und schauen uns an, was die Idee hinter den Klassen ist.

C++:
class Tier {
public:
    void ton() {
        std::cout << "Unbekanntes Tier!" << std::endl;
    }
}

class Hund : public Tier {
public:
    void ton() {
        std::cout << "Wuff!" << std::endl;
    }
}

class Katze : public Tier {
public:
    void ton() {
        std::cout << "Miau!" << std::endl;
    }
}
//....

auto h = new Hund();
h->ton(); // "Wuff!"
Hier macht es nicht viel Sinn, Vererbung zu benutzen - ich will ja nur, dass ein Hund laut gibt. Meist will man aber, dass mehrere Tiere in einer Sammlung etwas tun sollen. Z.B. will ich, dass alle Tiere im Haus einen Laut von sich geben. Ich habe also mehrere Tiere:
C++:
std::vector<Tier*> tiere = { new Hund(), new Katze()};
for(auto t : tiere) {
    t->ton();
}
Hier greifen wir auf den statischen Typ "Tier" zu, hinter dem Pointer ist aber der dynamische Typ "Hund" bzw. "Katze".
Aber interessanterweise wird bei dem Beispiel nicht "Wuff! Miau!" ausgegeben, sondern 2x "Unbekanntes Tier!".
Hier überdeckt im statischen Typ die Definition von ton() in Hund und Katze die statische Definition von ton() in Tier. Wenn wir also auf den statischen Typ zugreifen, wird auch die Funktion des jeweiligen statischen Typs aufgerufen.

Das ist seltsam für alle Java-Programmierer. In C++ gibt es das "Java-Verhalten" aber auch: Man nennt es "virtual inheritance":
C++:
class Tier {
public:
    virtual void ton() {
        std::cout << "Unbekanntes Tier!" << std::endl;
    }
}

class Hund : public Tier {
public:
    virtual void ton() override {
        std::cout << "Wuff!" << std::endl;
    }
}

class Katze : public Tier {
public:
    virtual void ton() override {
        std::cout << "Miau!" << std::endl;
    }
}
//....

auto h = new Hund();
h->ton(); // "Wuff!"

std::vector<Tier*> tiere = { new Hund(), new Katze()};
for(auto t : tiere) {
    t->ton(); //"Wuff! Miau!"
}
Der Grund dahinter ist relativ einfach: C++ unterstützt mehrfache Vererbung, Java nicht. Diese Mehrfachvererbung führt zu einem Overhead beim Cast, weshalb es unsinnig wäre, alles per default virtual zu halten. Java hat dieses Problem nicht.

Dann gibt es noch ein Problem: Wir haben eine Klasse Tier, die keinen ton() von sich geben kann, es aber trotzdem tut. Insbesondere kann man ein Tier erstellen, das kein konkretes Tier ist:
C++:
auto t = new Tier();
t->ton();
Das ist schlecht. In Java gibt es dazu Interfaces, in C++ gibt es "pure virtual": Abstrakte Funktionen:
C++:
class Tier {
public:
    virtual void ton() = 0;
}

auto t = new Tier(); // Compiler Error: Abstract Class instantiated
Nun haben wir die "virtual inheritance" auch geklärt.

In Java sind die Memberfunktionen immer virtual (werden überschrieben) und die Felder (Membervariablen) immer non-virtual (werden verdeckt). Die Verdeckung findet aber natürlich nur in der Unterklasse statt, da die Basisklasse nichts von einer Überdeckung weiss. (Per super() kann auch eine Kindklasse auf die Elternvariablen zugreifen, aber soweit ich weiss geht das nicht von ausserhalb).

In PHP und meines Wissens auch JS (ES6) ist alles virtual (Ausnahme: Private Felder). Exportierte Symbole der Kindklassen teilen sich den Namespace mit allen Elternklassen.
Analog kennt PHP auch keinen Statischen Typ*: Alles ist ein Objekt, und der Typ ist (nur) dynamisch feststellbar.
Übrigens ist das bei den Skriptsprachen allgemein verbreitete Duck Typing ein Vorteil, der aus den fehlenden Statischen Typen hervorgeht: Du kannst damit selbst die Funktionen überschreiben (zumindest in JS, weiss gerade nicht, ob das auch in PHP so leicht geht).
(Ja, ich weiss, dass auch Skriptsprachen einige Andeutungen an statische Typen haben).

Am besten verwendet man niemals gleichnamige Membervariablen, egal ob es die Programmiersprache hergibt oder nicht.
Das lässt sich selten vermeiden - die gängige Art, dagegen vorzugehen, ist, getter und setter zu verwenden und die Membervariablen privat zu halten.
Aber es kann ja auch sein, dass man die Funktionalität einer Klasse mit einer eigenen Klasse erweitern will - oft werden die exportierten Variablen nicht dokumentiert, und man möchte ja auch nicht alle Klassen durchlesen.
Das Problem hier ist halt: Bei PHP gibt es das "Sorglospaket" nicht.


Um zusammenzufassen: PHP verhält sich anders als Java/C++. Ein Vergleich:
Code:
Feature                        C++        Java        PHP
Function Inheritance     static/virtual   virtual    virtual
Variable Inheritance     static           static     virtual (except private)
Oder anders: Die Objektstruktur von Java ist
Code:
class Base {
    vf_table
    base_fields
}
class Derived extends Base {
    vf_table
    class Base {
        base_fields
    }
    derived_fields
}
Die Basisklasse wird also in die Unterklasse "reinkopiert".
Die vf_table (virtual function table) beinhaltet die Function pointers, vereinfacht gesagt wird da jeweils die Adresse der überschreibenden Funktion an den Funktionsnamen gebunden.
Bei PHP ist das Klassenlayout so:
Code:
class Base {
    map_base_private
    map_members
}

class Derived extends Base {
    map_base_private
    map_derived_private
    map_members = assign map_derived_members to map_base_members
}
Als Schlusswort: Klassen verhalten sich in unterschiedlichen Sprachen leider anders. In deinem Fall kannst du Flugzeug als "abstract class" (= Interface) definieren, damit es gar nie Variablen geben kann, die sich überschreiben.
Und bei allem anderen: Private Variablen mit Gettern und Settern verwenden, das geht auch wie in Java.

Gruss
cwriter

*: Nicht ganz wahr - PHP hat Elemente von Statischen Typen, wie abstract classes, aber vieles andere (wie typed variables) fehlt halt.
 

Neue Beiträge