Warum erlauben Programmiersprachen das Spiegeln / Verbergen von Variablen und Funktionen?

31

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

Simon
quelle
Aus diesem Grund beginnen alle meine Flurnamen mit F.
Pieter B
7
Ich denke, Eric Lippert hatte einen schönen Artikel dazu. Oh warte, hier ist es: blogs.msdn.com/b/ericlippert/archive/2008/05/21/…
Lescai Ionel
1
Bitte klären Sie Ihre Frage. Fragen Sie nach Informationen, die im Allgemeinen ausgeblendet werden, oder nach dem speziellen Fall, der im Artikel von Lippert beschrieben wird, in dem eine abgeleitete Klasse Funktionen der Basisklasse verbirgt?
Aaron Kurtzhals
Wichtiger Hinweis: Viele Fehler, die durch Ausblenden / Abschatten verursacht werden, sind mutiert (setzen Sie die falsche Variable und fragen Sie sich, warum die Änderung zum Beispiel "nie passiert"). Wenn Sie hauptsächlich mit unveränderlichen Referenzen arbeiten, verursacht das Ausblenden / Abschatten weitaus weniger Probleme und ist weitaus weniger fehleranfällig.
Jack

Antworten:

26

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.

John R. Strohm
quelle
19
Ein weiterer Nachteil des Verbots von Shadowing besteht darin, dass das Hinzufügen einer globalen Variablen den Code beschädigen kann, da die Variable bereits in einem lokalen Block verwendet wurde.
Giorgio
19
"Wenn Sie das Ausblenden und Abschatten nicht zulassen, haben Sie eine Sprache, in der alle Variablen global sind." - nicht unbedingt: Sie können Variablen mit Gültigkeitsbereich haben, ohne sie zu beschatten, und Sie haben es erklärt.
Thiago Silva
@ThiagoSilva: Und dann hat Ihre Sprache eine Möglichkeit haben , den Compiler zu sagen , dass dieses Modul IS zugreifen darf , dass Moduls „frammis“ Variable. Erlauben Sie jemandem, ein Objekt zu verbergen / zu beschatten, von dem er nicht einmal weiß, dass es existiert, oder erzählen Sie ihm davon, warum er diesen Namen nicht verwenden darf?
John R. Strohm
9
@Phil, entschuldigen Sie, dass ich mit Ihnen nicht einverstanden bin, aber das OP fragte nach "Verstecken / Abschatten von Variablen oder Funktionen", und die Wörter "Eltern", "Kind", "Klasse" und "Mitglied" tauchen nirgends in seiner Frage auf. Das scheint eine allgemeine Frage zum Geltungsbereich des Namens zu sein.
John R. Strohm
3
@dylnmc, ich hatte nicht erwartet, dass ich lange genug leben würde, um einem Whippersnapper zu begegnen, der jung genug war, um nicht die offensichtliche Referenz "2001: A Space Odyssey" zu bekommen.
John R. Strohm
15

Kennt jemand eine gute Verwendung dieser Konzepte?

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.

Telastyn
quelle
3
Nein, es ist NICHT einfacher, das Ausblenden und Abschatten zu implementieren. Es ist tatsächlich einfacher zu implementieren "alle Variablen sind global". Sie benötigen nur einen Namespace und exportieren IMMER den Namen, anstatt mehrere Namespaces zu haben und für jeden Namen zu entscheiden, ob er exportiert werden soll.
John R. Strohm
5
@ JohnR.Strohm - Sicher, aber sobald Sie irgendeine Art von Scoping haben ( sprich : Klassen), ist es kostenlos, wenn die Scopes die unteren Scopes verstecken.
Telastyn
Umfang und Klassen sind verschiedene Dinge. Mit Ausnahme von BASIC hat jede Sprache, in der ich programmiert habe, Gültigkeitsbereiche, aber nicht alle haben ein Konzept für Klassen oder Objekte.
Michael Shaw
@michaelshaw - natürlich hätte ich klarer sein sollen.
Telastyn
7

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:

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

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?

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

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:

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

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:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

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:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

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:

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

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.

KeithS
quelle
+1 zur Beantwortung der eigentlichen Frage. Ein gutes Beispiel für eine reale Verwendung des Versteckens von Mitgliedern ist das Interface IEnumerableund IEnumerable<T>, das in Eric Libberts Blog-Post zum Thema beschrieben wird.
Phil
Überschreiben verbirgt sich nicht . Ich bin mit @Phil nicht einverstanden, dass dies die Frage adressiert.
Jan Hudec
Mein Punkt war, dass das Überschreiben ein Ersatz für das Verstecken sein würde, wenn das Verstecken keine Option ist. Ich stimme zu, es verbirgt sich nicht und ich sage dies auch im ersten Absatz. Keine der Problemumgehungen für mein Szenario der alternativen Realität, in C # kein Versteck zu finden, ist versteckt. das ist der Punkt.
KeithS
Ich mag es nicht, wenn du dich versteckst oder beschattet. Die wichtigsten guten Verwendungen, die ich sehe, sind: (1) Umschweifen der Situation, in der eine neue Version einer Basisklasse ein Member enthält, das mit dem Konsumentencode in Konflikt steht, der für eine ältere Version entwickelt wurde [hässlich, aber notwendig]; (2) Fälschung von Dingen wie Kovarianz vom Rückgabetyp; (3) Umgang mit Fällen, in denen eine Basisklassenmethode für einen bestimmten Subtyp aufrufbar, aber nicht nützlich ist . Ersteres ist für den LSP erforderlich, letzteres jedoch nicht, wenn der Basisklassenvertrag angibt, dass einige Methoden unter bestimmten Bedingungen möglicherweise nicht nützlich sind.
supercat
2

Ganz ehrlich, Eric Lippert, der Hauptentwickler im C # -Compiler-Team, erklärt es ziemlich gut (danke Lescai Ionel für den Link). .NETs IEnumerableund IEnumerable<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 IEnumerableInterface sah also so aus:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Diese Schnittstelle ermöglichte es uns, foreacheine 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:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

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:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

Dies würde irgendwie albern sein, weil GetEnumerator()und GetEnumeratorGeneric()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öchten GetEnumerator, 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?

Phil
quelle
Während das formal IEnumerable<T>.GetEnumerator()verbirgt IEnumerable.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 Variable mapin der Funktion einer Datei haben, die dies tut using namespace std(in C ++).
Jan Hudec
2

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.

Michael Shaw
quelle
2

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:

  1. Lokal definierte Namen, zB lokale Variablen, die nur innerhalb des Bausteins bekannt sind.
  2. Argumente, die beim Aufrufen des Blocks an Werte gebunden sind und vom Aufrufer zur Angabe der Eingabe- / Ausgabeparameter des Blocks verwendet werden können.
  3. Externe Namen / Bindungen, die in der Umgebung definiert sind, in der der Block enthalten ist, und die sich innerhalb des Blocks im Gültigkeitsbereich befinden.

Betrachten Sie zum Beispiel das folgende C-Programm

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

Die Funktion print_double_inthat einen lokalen Namen (lokale Variable) dund ein Argument nund verwendet den externen globalen Namen printf, der im Gültigkeitsbereich liegt, aber nicht lokal definiert ist.

Beachten Sie, dass printfdies auch als Argument übergeben werden kann:

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

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:

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

Der Rückgabewert der Funktion createMultiplierist der Verschluß (m: Int) => m * n, der die Parameter enthält mund den externen Namen n. Der Name nwird aufgelöst, indem der Kontext betrachtet wird, in dem der Abschluss definiert ist: Der Name ist an das Argument nder Funktion gebunden createMultiplier. Beachten Sie, dass diese Bindung erstellt wird, wenn der Abschluss erstellt wird, dh wenn createMultiplieraufgerufen wird. Der Name nist also an den tatsächlichen Wert eines Arguments für einen bestimmten Funktionsaufruf gebunden. Vergleichen Sie dies mit dem Fall einer Bibliotheksfunktion printf, 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

  • müssen / wollen keine extern definierten Namen explizit als Argumente übergeben, und
  • Sie können Bindungen zur Laufzeit einfrieren, wenn ein Block erstellt wird, und später darauf zugreifen, wenn der Block aufgerufen wird.

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 printfFunktion, 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.

Giorgio
quelle