Dlaczego języki programowania umożliwiają cieniowanie / ukrywanie zmiennych i funkcji?


31

Wiele najpopularniejszych języków programowania (takich jak C ++, Java, Python itp.) Ma pojęcie ukrywania / cieniowania zmiennych lub funkcji. Kiedy spotkałem się z ukrywaniem lub zacieniowaniem, były przyczyną trudnych do znalezienia błędów i nigdy nie widziałem przypadku, w którym uważam za konieczne korzystanie z tych funkcji języków.

Wydaje mi się, że lepiej zabronić ukrywania się i cieniowania.

Czy ktoś wie o dobrym wykorzystaniu tych pojęć?

Aktualizacja:
Nie mam na myśli enkapsulacji członków klasy (członków prywatnych / chronionych).


Dlatego wszystkie moje nazwy pól zaczynają się na F.
Pieter B

7
Myślę, że Eric Lippert miał fajny artykuł na ten temat. Och, czekaj, oto jest: blogs.msdn.com/b/ericlippert/archive/2008/05/21/…
Lescai Ionel

1
Proszę wyjaśnić swoje pytanie. Czy pytasz o informacje ukryte w ogóle, czy o konkretny przypadek opisany w artykule Lipperta, w którym klasa pochodna ukrywa funkcje klasy podstawowej?
Aaron Kurtzhals

Ważna uwaga: wiele błędów spowodowanych ukrywaniem / cieniowaniem wiąże się z mutacją (ustawienie niewłaściwej zmiennej i zastanawianie się, dlaczego zmiana „nigdy się nie zdarza”). Podczas pracy przede wszystkim z niezmiennymi referencjami ukrywanie / cieniowanie powoduje znacznie mniej problemów i jest znacznie mniej prawdopodobne, że spowoduje błędy.
Jack

Odpowiedzi:


26

Jeśli nie zezwalasz na ukrywanie i cieniowanie, masz język, w którym wszystkie zmienne są globalne.

Jest to wyraźnie gorsze niż dopuszczenie zmiennych lokalnych lub funkcji, które mogą ukrywać zmienne globalne lub funkcje.

Jeśli nie zezwalasz na ukrywanie i cieniowanie, ORAZ próbujesz „chronić” niektóre zmienne globalne, tworzysz sytuację, w której kompilator mówi programatorowi „Przykro mi, Dave, ale nie możesz użyć tej nazwy, jest już używana . ” Doświadczenie z COBOL pokazuje, że w tej sytuacji programiści niemal natychmiast uciekają się do wulgaryzmów.

Podstawowym problemem nie jest ukrywanie / cieniowanie, ale zmienne globalne.


19
Kolejną wadą zakazania cieniowania jest to, że dodanie zmiennej globalnej może uszkodzić kod, ponieważ zmienna była już używana w bloku lokalnym.
Giorgio

19
„Jeśli nie zezwalasz na ukrywanie i cieniowanie, masz język, w którym wszystkie zmienne mają charakter globalny”. - niekoniecznie: możesz mieć zmienne o zasięgu bez cieniowania, i wyjaśniłeś to.
Thiago Silva,

@ThiagoSilva: I wtedy twój język musi mieć sposób, aby powiedzieć kompilatorowi, że ten moduł JEST może uzyskać dostęp do zmiennej „frammis” tego modułu. Czy pozwalasz komuś ukryć / ukryć obiekt, o którym nawet nie wie, czy może mu powiedzieć, dlaczego nie wolno mu używać tego imienia?
John R. Strohm

9
@Phil, przepraszam, że się z tobą nie zgadzam, ale OP zapytał o „ukrywanie / cieniowanie zmiennych lub funkcji”, a słowa „rodzic”, „dziecko”, „klasa” i „członek” nie pojawiają się nigdzie w jego pytaniu. Wydaje się, że to sprawia, że ​​jest to ogólne pytanie dotyczące zakresu nazw.
John R. Strohm,

3
@dylnmc, nie spodziewałem się, że będę żył wystarczająco długo, by spotkać młodego bata na tyle młodego, że nie dostanie oczywistego odniesienia do „2001: A Space Odyssey”.
John R. Strohm

15

Czy ktoś wie o dobrym wykorzystaniu tych pojęć?

Używanie dokładnych, opisowych identyfikatorów jest zawsze dobrym rozwiązaniem.

Mogę argumentować, że ukrywanie zmiennych nie powoduje wielu błędów, ponieważ posiadanie dwóch bardzo podobnych nazw zmiennych tego samego / podobnego typu (co byś zrobił, gdyby ukrywanie zmiennych zostało niedozwolone) prawdopodobnie spowoduje tyle błędów i / lub tak samo poważne błędy. Nie wiem, czy ten argument jest poprawny , ale przynajmniej można go poddać dyskusji.

Użycie jakiegoś węgierskiego zapisu do rozróżnienia pól od zmiennych lokalnych omija ten problem, ale ma swój wpływ na utrzymanie (i rozsądek programisty).

I (być może najprawdopodobniej powodem, dla którego ta koncepcja jest znana) języki znacznie łatwiej wdrażają ukrywanie / cieniowanie niż nie zezwalają na to. Łatwiejsza implementacja oznacza, że ​​kompilatory rzadziej zawierają błędy. Łatwiejsza implementacja oznacza, że ​​kompilatory zajmują mniej czasu na pisanie, co powoduje wcześniejsze i szersze przyjęcie platformy.


3
Właściwie nie, nie jest łatwiej wdrożyć ukrywanie i cieniowanie. W rzeczywistości łatwiej jest zaimplementować „wszystkie zmienne są globalne”. Potrzebujesz tylko jednej przestrzeni nazw i ZAWSZE eksportujesz nazwę, w przeciwieństwie do posiadania wielu przestrzeni nazw i konieczności decydowania dla każdej nazwy, czy ją wyeksportować.
John R. Strohm

5
@ JohnR.Strohm - Jasne, ale jak tylko będziesz mieć jakikolwiek zakres (czytaj: klasy), ukrywanie zakresów niższych jest już za darmo.
Telastyn

Określanie zakresu i klasy to różne rzeczy. Z wyjątkiem BASIC, każdy język, w którym zaprogramowałem, ma zakres, ale nie wszystkie mają jakieś pojęcie klas lub obiektów.
Michael Shaw

@ Michaelshaw - oczywiście powinienem był być bardziej jasny.
Telastyn

7

Aby upewnić się, że jesteśmy na tej samej stronie, metoda „ukrywa się” polega na tym, że klasa pochodna definiuje element członkowski o tej samej nazwie co element klasy podstawowej (który, jeśli jest metodą / właściwością, nie jest oznaczony jako wirtualny / nadpisywalny) ), a po wywołaniu z instancji klasy pochodnej w „kontekście pochodnym” używany jest element pochodny, natomiast jeśli jest wywoływany przez to samo wystąpienie w kontekście swojej klasy podstawowej, używany jest element klasy podstawowej. Różni się to od abstrakcji / zastępowania elementu, gdy element klasy podstawowej oczekuje, że klasa pochodna zdefiniuje zamiennik, oraz od modyfikatorów zakresu / widoczności, które „ukrywają” członka przed konsumentami poza pożądanym zakresem.

Krótka odpowiedź na pytanie, dlaczego jest to dozwolone, jest taka, że ​​nie zmuszałoby to deweloperów do łamania kilku kluczowych założeń projektowania obiektowego.

Oto dłuższa odpowiedź; po pierwsze, rozważ następującą strukturę klas w alternatywnym wszechświecie, w którym C # nie pozwala na ukrywanie członków:

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";}}
}

Chcemy anulować komentarz członka w pasku, a tym samym pozwolić Barowi na dostarczenie innego MyFooString. Nie możemy tego jednak zrobić, ponieważ stanowiłoby to naruszenie zakazu ukrywania członków w rzeczywistości alternatywnej. Ten konkretny przykład byłby powszechny dla błędów i jest doskonałym przykładem tego, dlaczego możesz chcieć go zakazać; na przykład, jakie dane wyjściowe konsoli uzyskałbyś, gdybyś zrobił następujące rzeczy?

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

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

Z czubka mojej głowy tak naprawdę nie jestem pewien, czy dostaniesz „Foo” czy „Bar” w tej ostatniej linii. Zdecydowanie dostaniesz „Foo” dla pierwszego wiersza i „Bar” dla drugiego, mimo że wszystkie trzy zmienne odnoszą się dokładnie do tego samego wystąpienia z dokładnie tym samym stanem.

Tak więc projektanci języka, w naszym alternatywnym wszechświecie, zniechęcają do tego oczywiście złego kodu, uniemożliwiając ukrywanie własności. Teraz, jako programista, naprawdę musisz dokładnie to zrobić. Jak obejść to ograniczenie? Jednym ze sposobów jest inna nazwa właściwości Bar:

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

Idealnie legalne, ale nie takie zachowanie chcemy. Instancja Bar zawsze będzie produkować „Foo” dla właściwości MyFooString, gdy chcemy, aby produkowała „Bar”. Musimy nie tylko wiedzieć, że nasz IFoo to konkretnie Bar, musimy także wiedzieć, jak korzystać z innego akcesorium.

Możemy również, całkiem prawdopodobne, zapomnieć o relacji rodzic-dziecko i wdrożyć interfejs bezpośrednio:

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

W tym prostym przykładzie jest to idealna odpowiedź, o ile zależy ci tylko na tym, że Foo i Bar to IFoos. Kod użycia, o którym mowa w kilku przykładach, nie zostałby skompilowany, ponieważ pasek nie jest Foo i nie może być przypisany jako taki. Jednakże, jeśli Foo miał jakąś przydatną metodę „FooMethod”, której potrzebował Bar, teraz nie można dziedziczyć tej metody; musisz albo sklonować jego kod w Bar, albo uzyskać kreatywność:

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

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

To oczywisty hack i choć niektóre implementacje specyfikacji języka OO stanowią niewiele więcej, to jest to błędne koncepcyjnie; jeśli konsumenci Bar konieczności narazić funkcjonalność foo, bar powinien być Foo, nie mają do Foo.

Oczywiście, jeśli kontrolujemy Foo, możemy uczynić go wirtualnym, a następnie zastąpić. Jest to najlepsza praktyka konceptualna w naszym obecnym wszechświecie, gdy oczekuje się, że członek zostanie zastąpiony, i obejmowałby każdy alternatywny wszechświat, który nie pozwalałby na ukrywanie:

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

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

Problem polega na tym, że dostęp do elementów wirtualnych jest, pod maską, stosunkowo droższy w wykonywaniu, dlatego zazwyczaj chcesz to zrobić tylko wtedy, gdy jest to konieczne. Jednak brak ukrywania zmusza cię do pesymizmu co do elementów, które inny koder, który nie kontroluje twojego kodu źródłowego, może chcieć ponownie wdrożyć; „najlepszą praktyką” dla każdej niezamkniętej klasy byłoby uczynienie wszystkiego wirtualnym, chyba że specjalnie tego nie chcesz. Jest też jeszcze nie daje dokładnie zachowanie ukryciu; ciąg zawsze będzie miał wartość „Bar”, jeśli instancja to Bar. Czasami naprawdę przydatne jest wykorzystanie warstw danych o stanie ukrytym na podstawie poziomu dziedziczenia, na którym pracujesz.

Podsumowując, umożliwienie ukrywania się członków to mniejsze zło. Nie posiadanie go generalnie prowadzi do gorszych okrucieństw popełnianych wbrew zasadom obiektowym niż pozwala na to.


+1 za odpowiedź na aktualne pytanie. Dobrym przykładem użycia świata rzeczywistego z ukrycia jest członkiem IEnumerablei IEnumerable<T>interfejs, opisane w Erica Libberta na blogu na ten temat.
Phil

Przesłanianie nie ukrywa się. Nie zgadzam się z @Phil, że dotyczy to pytania.
Jan Hudec

Chodziło mi o to, że zastąpienie byłoby substytutem ukrywania, gdy ukrywanie nie jest opcją. Zgadzam się, to nie ukrywa się i mówię to samo w pierwszym akapicie. Żadne z obejść mojego scenariusza alternatywnej rzeczywistości ukrywania się w C # nie ukrywa się; o to chodzi.
KeithS,

Nie lubię twoich zastosowań cieniowania / ukrywania się. Głównymi dobrymi zastosowaniami, które widzę, są (1) kłótnie wokół sytuacji, w której nowa wersja klasy podstawowej zawiera element członkowski, który jest w konflikcie z kodem konsumenta zaprojektowanym wokół starszej wersji [brzydkie, ale konieczne]; (2) fałszowanie rzeczy, takich jak kowariancja typu powrotu; (3) zajmowanie się przypadkami, w których metodę klasy bazowej można wywołać na określonym podtypie, ale nie jest ona użyteczna . LSP wymaga tego pierwszego, ale nie wymaga tego drugiego, jeśli umowa klasy podstawowej określa, że ​​niektóre metody mogą nie być przydatne w niektórych warunkach.
supercat

2

Szczerze mówiąc, Eric Lippert, główny programista w zespole kompilatorów C #, wyjaśnia to całkiem dobrze (dzięki Lescai Ionel za link). .NET IEnumerablei IEnumerable<T>interfejsy są dobrymi przykładami przydatności ukrywania członków.

Na początku .NET nie mieliśmy generycznych. Więc IEnumerableinterfejs wyglądał następująco:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Ten interfejs pozwolił nam foreachprześcignąć zbiór obiektów, jednak musieliśmy rzucić wszystkie te obiekty, aby móc z nich prawidłowo korzystać.

Potem przyszedł rodzajowy. Kiedy otrzymaliśmy ogólne, otrzymaliśmy również nowy interfejs:

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

Teraz nie musimy rzucać przedmiotami podczas iteracji! Woot! Teraz, jeśli ukrywanie członków nie było dozwolone, interfejs musiałby wyglądać mniej więcej tak:

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

Byłoby to trochę głupie, ponieważ GetEnumerator()iw GetEnumeratorGeneric()obu przypadkach robią prawie dokładnie to samo , ale mają nieco inne zwracane wartości. Są tak bardzo podobne, że prawie zawsze chcesz domyślnie używać ogólnej formy GetEnumerator, chyba że pracujesz ze starszym kodem, który został napisany przed wprowadzeniem generycznych do .NET.

Czasami ukrywanie członków daje więcej miejsca na nieprzyjemny kod i trudne do znalezienia błędy. Jednak czasami jest to przydatne, na przykład gdy chcesz zmienić typ zwracany bez łamania starszego kodu. To tylko jedna z tych decyzji, które muszą podjąć projektanci języka: Czy przeszkadzamy programistom, którzy zgodnie z prawem potrzebują tej funkcji i pomijamy ją, czy też włączamy tę funkcję do języka i łapiemy strach przed ofiarami jej niewłaściwego użycia?


Chociaż formalnie IEnumerable<T>.GetEnumerator()ukrywa IEnumerable.GetEnumerator(), jest tak tylko dlatego, że C # nie ma kowariantnych typów zwracanych podczas przesłonięcia. Logicznie jest to zastąpienie, w pełni zgodne z LSP. Ukrywanie ma miejsce, gdy masz zmienną lokalną mapw funkcji w pliku, który ma using namespace std(w C ++).
Jan Hudec

2

Twoje pytanie można odczytać na dwa sposoby: albo ogólnie pytasz o zakres zmiennej / funkcji, albo zadajesz bardziej szczegółowe pytanie dotyczące zakresu w hierarchii dziedziczenia. Nie wspomniałeś konkretnie o dziedziczeniu, ale wspomniałeś o trudnych do znalezienia błędach, które w kontekście dziedziczenia brzmią bardziej jak zakres, niż zwykły zakres, więc odpowiem na oba pytania.

Ogólnie zakres jest dobrym pomysłem, ponieważ pozwala nam skupić się na jednej konkretnej (miejmy nadzieję małej) części programu. Ponieważ pozwala zawsze wygrywać nazwy lokalne, jeśli czytasz tylko część programu, która jest w danym zakresie, wiesz dokładnie, które części zostały zdefiniowane lokalnie, a co gdzie indziej. Albo nazwa odnosi się do czegoś lokalnego, w którym to przypadku kod, który ją definiuje, znajduje się tuż przed tobą, lub jest odniesieniem do czegoś poza lokalnym zakresem. Jeśli nie ma żadnych nielokalnych odniesień, które mogłyby zmienić się pod nami (szczególnie zmienne globalne, które można zmienić z dowolnego miejsca), możemy ocenić, czy część programu w zasięgu lokalnym jest poprawna, czy nie bez odwoływania się do dowolnej części reszty programu .

Może to czasami prowadzić do kilku błędów, ale rekompensuje to więcej, zapobiegając ogromnej ilości możliwych błędów. Poza tworzeniem definicji lokalnej o tej samej nazwie co funkcja biblioteki (nie rób tego), nie widzę łatwego sposobu na wprowadzenie błędów w zakresie lokalnym, ale zasięg lokalny pozwala wielu częściom tego samego programu korzystać i jako licznik indeksu dla pętli bez zapychania się nawzajem i pozwala Fredowi napisać funkcję, która używa łańcucha o nazwie str, który nie zapycha łańcucha o tej samej nazwie.

Znalazłem interesujący artykuł Bertranda Meyera, który omawia przeciążanie w kontekście dziedziczenia. Wprowadza interesujące rozróżnienie między tym, co nazywa przeciążeniem składniowym (co oznacza, że ​​istnieją dwie różne rzeczy o tej samej nazwie) i przeciążeniem semantycznym (co oznacza, że ​​istnieją dwie różne implementacje tego samego abstrakcyjnego pomysłu). Przeciążenie semantyczne byłoby w porządku, ponieważ zamierzałeś wprowadzić go inaczej w podklasie; przeciążenie składniowe to przypadkowe zderzenie nazwy, które spowodowało błąd.

Różnica między przeładowaniem w sytuacji dziedziczenia, która jest zamierzona, a która jest błędem, jest semantyka (znaczenie), więc kompilator nie ma możliwości dowiedzenia się, czy to, co zrobiłeś, jest dobre, czy złe. W przypadku zwykłego zakresu właściwą odpowiedzią jest zawsze sprawa lokalna, więc kompilator może dowiedzieć się, co jest właściwe.

Sugestią Bertranda Meyera byłoby użycie języka takiego jak Eiffel, który nie pozwala na takie konflikty nazw i zmusza programistę do zmiany nazwy jednego lub obu, unikając w ten sposób problemu. Moją sugestią byłoby całkowite uniknięcie dziedziczenia, a także całkowite uniknięcie problemu. Jeśli nie możesz lub nie chcesz robić żadnej z tych rzeczy, możesz jeszcze zmniejszyć prawdopodobieństwo wystąpienia problemu z dziedziczeniem: postępuj zgodnie z LSP (Zasada podstawienia Liskowa), preferuj kompozycję zamiast dziedziczenia, zachowaj twoje hierarchie dziedziczenia są płytkie, a klasy w hierarchii spadkowej małe. Ponadto niektóre języki mogą wyświetlać ostrzeżenie, nawet jeśli nie spowodują błędu, tak jak w przypadku języka Eiffel.


2

Oto moje dwa centy.

Programy mogą być podzielone na bloki (funkcje, procedury), które są samodzielnymi jednostkami logiki programu. Każdy blok może odnosić się do „rzeczy” (zmiennych, funkcji, procedur) przy użyciu nazw / identyfikatorów. To odwzorowanie nazw na rzeczy nazywa się wiązaniem .

Nazwy używane przez blok dzielą się na trzy kategorie:

  1. Lokalnie zdefiniowane nazwy, np. Zmienne lokalne, które są znane tylko w bloku.
  2. Argumenty, które są powiązane z wartościami podczas wywoływania bloku i mogą być użyte przez program wywołujący do określenia parametru wejścia / wyjścia bloku.
  3. Zewnętrzne nazwy / powiązania, które są zdefiniowane w środowisku, w którym blok jest zawarty i są objęte zakresem w tym bloku.

Rozważmy na przykład następujący program C.

#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);
}

Funkcja print_double_intma nazwę lokalną (zmienna lokalna) di argument noraz używa zewnętrznej, globalnej nazwy printf, która jest w zasięgu, ale nie jest zdefiniowana lokalnie.

Zauważ, że printfmożna go również przekazać jako argument:

#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);
}

Zwykle argument służy do określania parametrów wejściowych / wyjściowych funkcji (procedura, blok), podczas gdy nazwy globalne są używane w odniesieniu do rzeczy takich jak funkcje biblioteczne, które „istnieją w środowisku”, dlatego wygodniej jest o nich wspomnieć tylko wtedy, gdy są potrzebne. Używanie argumentów zamiast nazw globalnych jest główną ideą wstrzykiwania zależności , która jest używana, gdy zależności muszą być jawne, a nie rozwiązywane przez spojrzenie na kontekst.

Kolejne podobne zastosowanie nazw zewnętrznych można znaleźć w zamknięciach. W tym przypadku nazwa zdefiniowana w kontekście leksykalnym bloku może być użyta w obrębie bloku, a wartość powiązana z tą nazwą będzie (zwykle) istnieć tak długo, jak długo blok się do niej odwołuje.

Weźmy na przykład ten kod Scala:

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

Zwracaną wartością funkcji createMultiplierjest zamknięcie (m: Int) => m * n, które zawiera argument mi nazwę zewnętrzną n. Nazwę nrozwiązuje się, patrząc na kontekst, w którym zdefiniowano zamknięcie: nazwa jest powiązana z argumentem nfunkcji createMultiplier. Zauważ, że to powiązanie jest tworzone podczas tworzenia zamknięcia, tj. Kiedy createMultiplierjest wywoływane. Zatem nazwa njest powiązana z rzeczywistą wartością argumentu dla określonego wywołania funkcji. Porównaj to z przypadkiem funkcji biblioteki typu printf, która jest rozwiązywana przez linker, gdy budowany jest plik wykonywalny programu.

Podsumowując, przydatne może być odwołanie się do zewnętrznych nazw wewnątrz lokalnego bloku kodu, abyś mógł

  • nie potrzebuję / nie chcę jawnie definiować nazw zewnętrznych jako argumentów, oraz
  • można zablokować powiązania w czasie wykonywania, gdy blok jest tworzony, a następnie uzyskać do niego dostęp później, gdy blok zostanie wywołany.

Zacienianie pojawia się, gdy weźmiesz pod uwagę, że w bloku interesują Cię tylko odpowiednie nazwy zdefiniowane w środowisku, np. printfFunkcja, której chcesz użyć. Jeśli przez przypadek chcesz użyć lokalnej nazwy ( getc, putc, scanf, ...), który został już użyty w środowisku, to proste chcesz zignorować (cień) nazwę globalnego. Więc myśląc lokalnie, nie chcesz brać pod uwagę całego (być może bardzo dużego) kontekstu.

Z drugiej strony, myśląc globalnie, chcesz zignorować wewnętrzne szczegóły lokalnych kontekstów (enkapsulacja). Dlatego potrzebujesz shadowingu, w przeciwnym razie dodanie nazwy globalnej może uszkodzić każdy blok lokalny, który już używał tej nazwy.

Podsumowując, jeśli chcesz, aby blok kodu odwoływał się do powiązań zdefiniowanych zewnętrznie, potrzebujesz shadowingu, aby chronić lokalne nazwy przed nazwami globalnymi.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.