Viele der beliebtesten Programmierung languges (wie C ++, Java, Python etc.) haben das Konzept der Deck / Beschattung von Variablen oder Funktionen. Wenn ich auf das Verstecken oder Abschatten gestoßen bin, waren sie die Ursache für schwer zu findende Fehler, und ich habe noch nie einen Fall gesehen, in dem ich es für notwendig hielt, diese Funktionen der Sprachen zu verwenden.
Mir scheint es besser, das Verstecken und Beschatten zu verbieten.
Kennt jemand eine gute Verwendung dieser Konzepte?
Update:
Ich beziehe mich nicht auf die Kapselung von Klassenmitgliedern (private / geschützte Mitglieder).
Antworten:
Wenn Sie das Ausblenden und Abschatten nicht zulassen, haben Sie eine Sprache, in der alle Variablen global sind.
Das ist eindeutig schlimmer, als lokale Variablen oder Funktionen zuzulassen, die globale Variablen oder Funktionen verbergen könnten.
Wenn Sie Verstecken und Shadowing nicht zulassen, und Sie versuchen , zu „schützen“ , um bestimmte globale Variablen erstellen Sie eine Situation , in der der Compiler den Programmierer „erzählt Tut mir leid, Dave, aber Sie können diesen Namen nicht verwenden, ist es bereits im Einsatz . " Die Erfahrung mit COBOL zeigt, dass Programmierer in dieser Situation fast sofort auf Profanität zurückgreifen.
Das grundlegende Problem ist nicht das Ausblenden / Abschatten, sondern globale Variablen.
quelle
Die Verwendung genauer, beschreibender Bezeichner ist immer sinnvoll.
Ich könnte argumentieren, dass das Ausblenden von Variablen nicht viele Fehler verursacht, da zwei sehr ähnlich benannte Variablen desselben / ähnlichen Typs (was Sie tun würden, wenn das Ausblenden von Variablen nicht zulässig wäre) wahrscheinlich genau so viele Fehler und / oder genau so viele Fehler verursachen schwere Fehler. Ich weiß nicht, ob dieses Argument richtig ist , aber es ist zumindest plausibel umstritten.
Die Verwendung einer Art ungarischen Notation zur Unterscheidung von Feldern und lokalen Variablen umgeht dies, hat jedoch einen eigenen Einfluss auf die Wartung (und die Vernunft des Programmierers).
Und (wahrscheinlich der Grund, warum das Konzept überhaupt bekannt ist), ist es für Sprachen weitaus einfacher, das Ausblenden / Abschatten zu implementieren, als es zu verbieten. Eine einfachere Implementierung bedeutet, dass Compiler mit geringerer Wahrscheinlichkeit Fehler aufweisen. Eine einfachere Implementierung bedeutet, dass die Compiler weniger Zeit zum Schreiben benötigen, was eine frühere und umfassendere Einführung der Plattform zur Folge hat.
quelle
Nur um sicherzustellen, dass wir uns auf derselben Seite befinden, bedeutet die Methode "Verstecken", dass eine abgeleitete Klasse ein Element mit demselben Namen wie eines in der Basisklasse definiert (das, wenn es sich um eine Methode / Eigenschaft handelt, nicht als virtuell / überschreibbar markiert ist ) und beim Aufrufen von einer Instanz der abgeleiteten Klasse im "abgeleiteten Kontext" wird das abgeleitete Element verwendet, während beim Aufrufen von derselben Instanz im Kontext ihrer Basisklasse das Basisklassenelement verwendet wird. Dies unterscheidet sich von der Elementabstraktion / -überschreibung, bei der das Basisklassenelement erwartet, dass die abgeleitete Klasse eine Ersetzung definiert, und von Bereichs- / Sichtbarkeitsmodifikatoren, die ein Element vor Verbrauchern "verbergen", die außerhalb des gewünschten Bereichs liegen.
Die kurze Antwort auf die Frage, warum dies zulässig ist, lautet, dass dies die Entwickler zwingen würde, gegen mehrere wichtige Grundsätze des objektorientierten Designs zu verstoßen.
Hier ist die längere Antwort; Betrachten Sie zunächst die folgende Klassenstruktur in einem alternativen Universum, in dem C # das Ausblenden von Mitgliedern nicht zulässt:
Wir möchten dem Mitglied in Bar das Kommentarzeichen entziehen und auf diese Weise erlauben, dass Bar einen anderen MyFooString bereitstellt. Dies können wir jedoch nicht tun, da dies gegen das Verbot der alternativen Realität verstoßen würde, Mitglieder zu verstecken. Dieses Beispiel ist voller Bugs und ein gutes Beispiel dafür, warum Sie es möglicherweise verbieten möchten. Welche Konsolenausgabe würden Sie beispielsweise erhalten, wenn Sie Folgendes tun würden?
Ich bin mir nicht sicher, ob in der letzten Zeile "Foo" oder "Bar" steht. Sie erhalten definitiv "Foo" für die erste Zeile und "Bar" für die zweite, obwohl alle drei Variablen genau dieselbe Instanz mit genau demselben Status referenzieren.
Daher entmutigen die Designer der Sprache in unserem alternativen Universum diesen offensichtlich schlechten Code, indem sie das Verbergen von Eigenschaften verhindern. Jetzt müssen Sie als Programmierer genau das tun. Wie kommst du um die Begrenzung herum? Eine Möglichkeit besteht darin, die Eigenschaft von Bar anders zu benennen:
Völlig legal, aber es ist nicht das Verhalten, das wir wollen. Eine Instanz von Bar wird immer "Foo" für die Eigenschaft MyFooString erzeugen, wenn wir wollten, dass es "Bar" erzeugt. Wir müssen nicht nur wissen, dass unser IFoo speziell eine Bar ist, sondern wir müssen auch wissen, wie man die verschiedenen Accessors verwendet.
Wir könnten auch plausibel die Eltern-Kind-Beziehung vergessen und die Schnittstelle direkt implementieren:
Für dieses einfache Beispiel ist es eine perfekte Antwort, solange Sie sich nur darum kümmern, dass Foo und Bar beide IFoos sind. Der Verwendungscode, der in einigen Beispielen angegeben ist, kann nicht kompiliert werden, da ein Balken kein Foo ist und nicht als solcher zugewiesen werden kann. Wenn Foo jedoch eine nützliche Methode "FooMethod" hatte, die Bar benötigte, können Sie diese Methode jetzt nicht erben. Sie müssten entweder den Code in Bar klonen oder kreativ werden:
Dies ist ein offensichtlicher Hack, und obwohl einige Implementierungen von OO-Sprachspezifikationen kaum mehr als dies bedeuten, ist dies konzeptionell falsch. wenn die Verbraucher von Notwendigkeit Bar Foo Funktionalität aussetzen, sollte Bar sein ein Foo, nicht haben eine Foo.
Wenn wir Foo kontrollieren, können wir es natürlich virtuell machen und es dann außer Kraft setzen. Dies ist die konzeptionelle Best Practice in unserem aktuellen Universum, wenn erwartet wird, dass ein Mitglied außer Kraft gesetzt wird, und sie in jedem alternativen Universum gilt, in dem das Ausblenden nicht zulässig ist:
Das Problem dabei ist, dass der Zugriff auf virtuelle Mitglieder unter der Haube relativ teuer ist und Sie ihn normalerweise nur dann ausführen möchten, wenn Sie ihn benötigen. Das Fehlen des Versteckens zwingt Sie jedoch dazu, den Mitgliedern gegenüber pessimistisch zu sein, die ein anderer Codierer, der Ihren Quellcode nicht kontrolliert, möglicherweise neu implementieren möchte. Die "beste Praxis" für eine nicht versiegelte Klasse wäre, alles virtuell zu machen, es sei denn, Sie wollten dies ausdrücklich nicht. Es gibt Ihnen auch immer noch nicht das genaue Verhalten, sich zu verstecken. Die Zeichenfolge ist immer "Bar", wenn die Instanz eine Bar ist. Manchmal ist es wirklich nützlich, die Ebenen der verborgenen Zustandsdaten basierend auf der Ebene der Vererbung, auf der Sie arbeiten, zu nutzen.
Zusammenfassend ist das Ermöglichen des Versteckens von Mitgliedern das geringere dieser Übel. Wenn dies nicht der Fall ist, würde dies im Allgemeinen zu schlimmeren Gräueltaten führen, die gegen objektorientierte Prinzipien begangen werden, als wenn dies zulässig wäre.
quelle
IEnumerable
undIEnumerable<T>
, das in Eric Libberts Blog-Post zum Thema beschrieben wird.Ganz ehrlich, Eric Lippert, der Hauptentwickler im C # -Compiler-Team, erklärt es ziemlich gut (danke Lescai Ionel für den Link). .NETs
IEnumerable
undIEnumerable<T>
Schnittstellen sind gute Beispiele dafür, wann das Ausblenden von Mitgliedern nützlich ist.In den frühen Tagen von .NET hatten wir keine Generika. Das
IEnumerable
Interface sah also so aus:Diese Schnittstelle ermöglichte es uns,
foreach
eine Sammlung von Objekten zu überblicken. Wir mussten jedoch alle diese Objekte umwandeln, um sie ordnungsgemäß zu verwenden.Dann kamen Generika. Als wir Generika bekamen, bekamen wir auch eine neue Oberfläche:
Jetzt müssen wir keine Objekte mehr werfen, während wir sie durchlaufen! Woot! Wenn das Ausblenden von Mitgliedern nicht zulässig wäre, müsste die Benutzeroberfläche folgendermaßen aussehen:
Dies würde irgendwie albern sein, weil
GetEnumerator()
undGetEnumeratorGeneric()
in beiden Fällen ziemlich genau das tut gleiche Sache , aber sie haben leicht unterschiedliche Rückgabewerte. Sie sind in der Tat so ähnlich, dass Sie eigentlich immer die generische Form von verwenden möchtenGetEnumerator
, es sei denn, Sie arbeiten mit Legacy-Code, der geschrieben wurde, bevor Generics in .NET eingeführt wurden.Manchmal bietet das Verstecken von Mitgliedern mehr Platz für bösen Code und schwer zu findende Fehler. Manchmal ist es jedoch nützlich, wenn Sie beispielsweise einen Rückgabetyp ändern möchten, ohne den alten Code zu beschädigen. Dies ist nur eine der Entscheidungen, die Sprachentwickler treffen müssen: Belästigen wir die Entwickler, die diese Funktion rechtmäßig benötigen, und lassen sie aus, oder fügen wir diese Funktion in die Sprache ein und lassen sie von den Opfern des Missbrauchs los?
quelle
IEnumerable<T>.GetEnumerator()
verbirgtIEnumerable.GetEnumerator()
, ist dies nur, weil C # keine kovarianten Rückgabetypen beim Überschreiben hat. Logischerweise handelt es sich um eine Außerkraftsetzung, die vollständig mit LSP übereinstimmt. Versteckt wird, wenn Sie eine lokale Variablemap
in der Funktion einer Datei haben, die dies tutusing namespace std
(in C ++).Ihre Frage kann auf zwei Arten lauten: Entweder Sie fragen nach dem Gültigkeitsbereich von Variablen / Funktionen im Allgemeinen oder Sie stellen eine spezifischere Frage nach dem Gültigkeitsbereich in einer Vererbungshierarchie. Sie haben die Vererbung nicht speziell erwähnt, aber Sie haben schwer zu findende Fehler erwähnt, die im Zusammenhang mit der Vererbung eher nach Umfang als nach einfachem Umfang klingen. Daher beantworte ich beide Fragen.
Umfang im Allgemeinen ist eine gute Idee, weil es uns ermöglicht, unsere Aufmerksamkeit auf einen bestimmten (hoffentlich kleinen) Teil des Programms zu konzentrieren. Da lokale Namen immer gewinnen, wissen Sie genau, welche Teile lokal und welche an anderer Stelle definiert wurden, wenn Sie nur den Teil des Programms lesen, der sich in einem bestimmten Bereich befindet. Entweder verweist der Name auf etwas Lokales. In diesem Fall befindet sich der Code, der ihn definiert, direkt vor Ihnen, oder er verweist auf etwas außerhalb des lokalen Bereichs. Wenn es keine nicht-lokalen Referenzen gibt, die sich unter uns ändern könnten (insbesondere globale Variablen, die von überall geändert werden könnten), können wir ohne Bezugnahme beurteilen, ob der Teil des Programms im lokalen Bereich korrekt ist oder nicht zu irgendeinem Teil des Restes des Programms überhaupt .
Es kann gelegentlich zu einigen Fehlern führen, dies wird jedoch mehr als kompensiert, indem eine enorme Menge an ansonsten möglichen Fehlern verhindert wird. Abgesehen von der Erstellung einer lokalen Definition mit demselben Namen wie eine Bibliotheksfunktion (tun Sie das nicht), kann ich keine einfache Möglichkeit finden, Fehler mit lokalem Geltungsbereich einzuführen, aber der lokale Geltungsbereich ermöglicht es vielen Teilen desselben Programms, diese zu verwenden i ist der Indexzähler für eine Schleife, ohne sich gegenseitig zu überlasten, und lässt Fred im Flur eine Funktion schreiben, die eine Zeichenfolge mit dem Namen str verwendet, die Ihre Zeichenfolge mit demselben Namen nicht überlastet.
Ich habe einen interessanten Artikel von Bertrand Meyer gefunden, in dem es um Überladung im Zusammenhang mit Vererbung geht. Er bringt eine interessante Unterscheidung zwischen dem, was er syntaktisches Überladen nennt (was bedeutet, dass es zwei verschiedene Dinge mit demselben Namen gibt) und semantischem Überladen (was bedeutet, dass es zwei verschiedene Implementierungen derselben abstrakten Idee gibt). Eine semantische Überladung wäre in Ordnung, da Sie beabsichtigten, sie in der Unterklasse anders zu implementieren. Eine syntaktische Überladung wäre die zufällige Namenskollision, die einen Fehler verursachte.
Der Unterschied zwischen Überladung in einer beabsichtigten und einer fehlerhaften Vererbungssituation liegt in der Semantik (der Bedeutung), sodass der Compiler nicht weiß, ob das, was Sie getan haben, richtig oder falsch ist. In einer Situation mit einfachem Gültigkeitsbereich ist die richtige Antwort immer die lokale Sache, sodass der Compiler herausfinden kann, was die richtige Sache ist.
Bertrand Meyer schlägt vor, eine Sprache wie Eiffel zu verwenden, die solche Namenskonflikte nicht zulässt und den Programmierer zwingt, eine oder beide umzubenennen, wodurch das Problem gänzlich vermieden wird. Mein Vorschlag wäre, die Vererbung ganz zu vermeiden und das Problem auch ganz zu vermeiden. Wenn Sie eines dieser Dinge nicht tun können oder wollen, gibt es dennoch Möglichkeiten, die Wahrscheinlichkeit eines Problems mit der Vererbung zu verringern: Befolgen Sie das LSP (Liskov Substitution Principle), ziehen Sie die Komposition der Vererbung vor, behalten Sie es bei Ihre Vererbungshierarchien sind flach und halten die Klassen in einer Vererbungshierarchie klein. Außerdem können einige Sprachen möglicherweise eine Warnung ausgeben, auch wenn sie keinen Fehler ausgeben, wie dies bei einer Sprache wie Eiffel der Fall wäre.
quelle
Hier sind meine zwei Cent.
Programme können in Blöcke (Funktionen, Prozeduren) strukturiert werden, die in sich geschlossene Einheiten der Programmlogik sind. Jeder Block kann mit Namen / Bezeichnern auf "Dinge" (Variablen, Funktionen, Prozeduren) verweisen. Diese Zuordnung von Namen zu Dingen wird Bindung genannt .
Die von einem Block verwendeten Namen lassen sich in drei Kategorien einteilen:
Betrachten Sie zum Beispiel das folgende C-Programm
Die Funktion
print_double_int
hat einen lokalen Namen (lokale Variable)d
und ein Argumentn
und verwendet den externen globalen Namenprintf
, der im Gültigkeitsbereich liegt, aber nicht lokal definiert ist.Beachten Sie, dass
printf
dies auch als Argument übergeben werden kann:Normalerweise wird ein Argument verwendet, um Eingabe- / Ausgabeparameter einer Funktion (Prozedur, Block) anzugeben, wohingegen globale Namen verwendet werden, um auf Dinge wie Bibliotheksfunktionen zu verweisen, die "in der Umgebung existieren", und daher ist es bequemer, sie zu erwähnen nur wenn sie gebraucht werden. Die Verwendung von Argumenten anstelle von globalen Namen ist die Hauptidee der Abhängigkeitsinjektion. Sie wird verwendet, wenn Abhängigkeiten explizit angegeben werden müssen, anstatt durch Betrachten des Kontexts aufgelöst zu werden.
Eine andere ähnliche Verwendung von extern definierten Namen findet sich in Verschlüssen. In diesem Fall kann ein im lexikalischen Kontext eines Blocks definierter Name innerhalb des Blocks verwendet werden, und der an diesen Namen gebundene Wert bleibt (normalerweise) bestehen, solange der Block darauf verweist.
Nehmen Sie zum Beispiel diesen Scala-Code:
Der Rückgabewert der Funktion
createMultiplier
ist der Verschluß(m: Int) => m * n
, der die Parameter enthältm
und den externen Namenn
. Der Namen
wird aufgelöst, indem der Kontext betrachtet wird, in dem der Abschluss definiert ist: Der Name ist an das Argumentn
der Funktion gebundencreateMultiplier
. Beachten Sie, dass diese Bindung erstellt wird, wenn der Abschluss erstellt wird, dh wenncreateMultiplier
aufgerufen wird. Der Namen
ist also an den tatsächlichen Wert eines Arguments für einen bestimmten Funktionsaufruf gebunden. Vergleichen Sie dies mit dem Fall einer Bibliotheksfunktionprintf
, die vom Linker aufgelöst wird, wenn die ausführbare Datei des Programms erstellt wird.Zusammenfassend kann es nützlich sein, auf externe Namen innerhalb eines lokalen Codeblocks zu verweisen, damit Sie
Shadowing tritt ein, wenn Sie berücksichtigen, dass Sie in einem Block nur an relevanten Namen interessiert sind, die in der Umgebung definiert sind, z. B. an der
printf
Funktion, die Sie verwenden möchten. Wenn Sie zufällig einen lokalen Namen verwenden möchten (getc
,putc
,scanf
, ...) , die bereits in der Umwelt verwendet worden ist, können Sie einfach ignorieren sollen (Schatten) den globalen Namen. Wenn Sie also lokal denken, möchten Sie nicht den gesamten (möglicherweise sehr großen) Kontext berücksichtigen.In der anderen Richtung möchten Sie beim globalen Denken die internen Details der lokalen Kontexte (Kapselung) ignorieren. Daher müssen Sie Shadowing ausführen, da andernfalls das Hinzufügen eines globalen Namens jeden lokalen Block unterbrechen kann, der diesen Namen bereits verwendet hat.
Fazit: Wenn ein Codeblock auf extern definierte Bindungen verweisen soll, müssen Sie Schattierungen vornehmen, um lokale Namen vor globalen zu schützen.
quelle