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.
 
#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
 
#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.