Avatar von sentinel0815
  • sentinel0815

14 Beiträge seit 23.09.2015

Re: [Kann ich ...] - OOP, Sicherheit, Go, Rust, ...

octogen schrieb am 14.11.2017 13:56:

sentinel0815 schrieb am 13.11.2017 20:54:

Ja, es gibt in Go nicht die "klassischen" Klassen. Dafür definierst du einfach deinen Struct-Type und darauf operierende Funktionen. Ist bei der Benutzung dann quasi äquivalent zu Klassen.

OOP & Vererbung
Bei OOP geht es vor allem auch um die Reduktion von Code duplication.

Das Problem ist aber oft, dass Vererbung dafür verwendet wird. Das sieht man üblicherweise daran, dass im Code Klassen wie XXXBase auftauchen. Da ist die Basisklasse dann nur Mittel zum Zweck, um den Code zu sharen, eine sinnvolle "is-a"-Beziehung gibt's da dann oft nicht.

Wir haben in einem Projekt beispielsweise Name-Classes, die prinzipiell von einem String initialisiert werden, und dann case-insensitive matchen, aber sich dabei auch die originale Schreibweise merken.
Das ist aber nur die abstrakte Basis-Klasse, die einzelnen tatsaechlichen Names haben diverse Constraints wie Laenge oder erlaubte Zeichen. Manche haben etwas komplexere Constraints als andere.
Realisiert ist das ohne Code-Duplication durch parameterisierte Aufrufe des Constructors der jeweiligen Superclass. Das kann man in Go so elegant und effizient nicht nachbauen.

Ohne das Problem genauer zu kennen, wüsste ich nicht, warum das mit embedded Structs in Go nicht genauso elegant funktionieren würde.

Implizite Interfaces
Implizite Interfaces lassen sich zwar jederzeit irgendwo erfinden, aber keiner weiss, wo sie ueberall implementiert sind. Probier das einmal in einem grossen Projekt: "Show implementing classes".
Ganz schlimm wird's, wenn du aus irgendeinem Grund ein Interface erweitern musst. In Java, C++, etc. siehst du sofort, welche Objekte nicht mehr auf das Interface passen, wenn sich das Interface geaendert hat. In Go kannst du suchen gehen.

Versteh ich nicht, wenn du ein Interface änderst oder erweiterst meckert der Go-Compiler genauso wie der Java/C++/C#-Compiler. Go ist keine dynamische Sprache. Das schöne an impliziten Interfaces ist mir mich, dass du auch fremdem Code dein Interface verpassen kannst. Keine Wrapper-Klassen etc. pp; wenn der Interface-Contract erfüllt ist, reicht das aus.

Inkonsistentes Verhalten von Interfaces in Verbindung mit nil
Originell ist auch das Verhalten von Interfaces in Go in Verbindung mit "nil". Ein Vergleich mit "nil" kann false liefern, wenn du das Interface dann aber tatsaechlich verwendest, crasht dein Code, weil das Objekt, welches das Interface repraesentieren sollte, tatsaechlich doch "nil" ist.
(Siehe dazu "Why is my nil error value not equal to nil?" -> https://golang.org/doc/faq#nil_error)
So grobe logische Schnitzer erlaubt sich kaum eine andere Programmiersprache.

Ja, das geb ich zu, dass ist nicht gerade intuitiv.

Goroutines erfordern pass-by-value
Aehnliche versteckte Probleme hat man mit Interfaces und Goroutines.
Da hatte ich z.B. eine Situation:

type obj interface ... if condition { obj = impl_a } else { obj = impl_b } go obj.perform(something) ... much later ... elapsed_time := obj.get_timer();

Ich hatte in impl_a und impl_b einen timer eingebaut, den ich nach Beendigung der Goroutine einmal vom main thread aus abfragen wollte.

Dabei hatte ich natuerlich uebersehen, dass "obj" als Kopie an die Goroutine uebergeben wurde, mein "obj" im main thread hat natuerlich immer result 0 geliefert.

Als C++/Java-Programmier dachte ich mir, kein Problem, startet man die goroutine halt auf einem Pointer auf "obj". Geht nicht.

Ich vermute du verwechselst Value- und Pointer-Receiver einer Methode. Wenn du statt dessen "obj = &impl_A{}" verwendest, wird da nichts kopiert. Die Methode muss dann halt so aussehen:

func (impl *impl_A) perform(...) { ... }

Es scheint generell auch nicht zu gehen, den Methoden, die mit einem Type assoziiert sind, den jeweilige Type (das waere "this" in OOP) als Pointer zu uebergeben, und gleichzeitig den Type durch ein Interface hindurch zu verwenden.
Das ist anscheinend einfach nicht richtig durchdacht worden.

In dem obigen Beispiel ist "impl" der "this"-Pointer.

In Java oder C++ waere das relativ einfach gewesen, z.B.

std::thread my_thr(&ObjClass::perform(something), obj); my_thr.join(); uint64_t elapsed_time = obj->get_timer();

Thread und Go-Routines sind nur oberflächlich vergleichbar. Go-Routinen sind viel leichtgewichtiger und du hast oft tausende gleichzeitig am Laufen (mach das mal mit Threads). Und die Kommunikation mit einer Go-Routine solltest du besser über Channels machen, dafür sind sie gedacht. Shared-State zwischen Go-Routinen ist normalerweise verpönt, auch wenn es Mutexe etc. auch in Go gibt.

Das Beispiel von oben lässt sich trotzdem mit Pointer-Receivern ohne Probleme implementieren, auch wenn Channels dafür besser geeignet wären.

Zusammengefasst

Ich halte Go fuer keinen grossen Wurf. Der harte Kern der Sprache unterscheidet sich kaum von Sprachen, die in den 80ern schon verfuegbar waren. Dazu hat man dann halt die goroutines oben drangeschraubt, das ganze ein bisschen mehr wie eine Scriptsprache ausschauen lassen und das ganze Anfaengerfreundlich verpackt.

In meinen Augen sind das halt Vorurteile, die nach etwas Beschäftigung mit der Sprache normalerweise verschwinden.

Bewerten
- +