1. Wprowadzenie
Przypominamy sobie zakres (scope)
Wyobraź sobie, że zmienne to pracownicy wielkiego biura, a metody, pętle i bloki kodu to pokoje i gabinety. Niektórych pracowników wpuszcza się tylko do swojego pokoju, a innych — do całego budynku. To, gdzie kto może przebywać — to właśnie jego zakres (scope).
Zakres określa, gdzie zadeklarowana zmienna jest “widoczna” w programie i gdzie można jej używać.
Podstawowe rodzaje zakresów
W C# można wyróżnić takie główne zakresy:
| Zakres | Przykład | Gdzie "widoczna" jest zmienna |
|---|---|---|
| Lokalny | Wewnątrz metody lub bloku | Tylko w tym bloku |
| Parametr metody | W sygnaturze metody | Tylko w tej metodzie |
| Zmienna klasy (pole) | W ciele klasy poza metodami | We wszystkich metodach tej klasy |
| Zmienna w pętli/warunku | Wewnątrz pętli/if |
Tylko w tych |
Przykład z wyjaśnieniami
public class Office
{
int buildingNumber = 50; // Pole klasy: widoczne we wszystkich metodach public void PrintInfo() {
int roomNumber = 101; // Zmienna lokalna: widoczna tylko w PrintInfo if (roomNumber > 100) {
int deskNumber = 5; // Widoczna tylko w tym bloku if Console.WriteLine(deskNumber);
} Console.WriteLine(deskNumber); //
Błąd! deskNumber tutaj już nie jest widoczna
}
}
2. Funkcje lokalne i zakres
Kto kogo "widzi"?
Kiedy deklarujesz funkcję lokalną wewnątrz metody (albo nawet w pętli czy warunku), ona znajduje się w tym samym zakresie co zmienne zadeklarowane wyżej. Funkcja lokalna to jakby “część tego samego pokoju”.
Przykład
Funkcja lokalna widzi zmienne z otaczającego zakresu
void PrintWithPrefix(string wiadomosc)
{
string
prefix = "[LOG]: "; void Print() {
Console.WriteLine(
prefix +
wiadomosc); // widzi obie zmienne!
} Print();
}
Tutaj zmienne prefix i wiadomosc są widoczne wewnątrz funkcji lokalnej Print, bo są zadeklarowane w tym samym lub szerszym zakresie.
Ile tu jest zakresów?
W powyższym przykładzie:
- jest zakres metody PrintWithPrefix
- w nim — zakres funkcji Print
3. Przechwytywanie zmiennych (Capture)
Przechwytywanie zmiennych — to sytuacja, gdy funkcja lokalna używa zmiennych, które zostały zadeklarowane poza tą funkcją, ale w tym samym zakresie.
Funkcje lokalne i anonimowe metody (wyrażenia lambda, do nich jeszcze dojdziemy) zapamiętują (czyli “przechwytują”) wszystkie zmienne, które były dla nich dostępne w momencie deklaracji.
Można powiedzieć, że funkcje robią jakby zdjęcie (capture) otaczającego świata — i mogą używać tych zmiennych nawet wtedy, gdy są wywoływane dużo później.
Schematycznie
Metoda Main
└─ zmienna x
└─ funkcja lokalna F() ← "przechwytuje" x
Przykład — najprostsze przechwycenie
void CounterExample()
{
int licznik = 0;
void Zwieksz()
{
licznik++; // Ta funkcja przechwytuje zmienną licznik
}
Zwieksz();
Zwieksz();
Console.WriteLine(licznik); // Wypisze 2
}
Tutaj po dwóch wywołaniach funkcji lokalnej Zwieksz wartość licznik zwiększa się do 2.
4. Zastosowanie przechwytywania zmiennych
Przechwytywanie zmiennych pozwala wygodnie “przekazywać” dane między zakresem metody a funkcjami lokalnymi, bez męczenia się z dodatkowymi parametrami.
Gdyby nie było przechwytywania, musiałbyś przekazywać wszystkie zmienne jako parametry:
void CounterExampleWithoutCapture()
{
int licznik = 0;
void Zwieksz(ref int c)
{
c++;
}
Zwieksz(ref licznik);
Zwieksz(ref licznik);
Console.WriteLine(licznik);
}
To niewygodne — po co ciągle pisać ref i psuć sygnaturę funkcji, skoro ona może łatwo “widzieć” zmienne z zewnątrz?
5. Funkcje lokalne i życie zmiennych po wyjściu z metody
Czy zmienne żyją dłużej niż metoda?
Jeśli przekazujesz funkcje lokalne (albo delegaty z lambdami) gdzieś poza bieżącą metodę, przechwycone zmienne automatycznie przestają “umierać” po wyjściu z metody. CLR (.NET virtual machine) się tym zajmie — wszystko co potrzebne będzie “trzymane na siłę” w pamięci.
Przykład: funkcje żyją poza metodą
Func<int> GetCounter()
{
int liczba = 0;
int Zwieksz()
{
liczba++;
return liczba;
}
return Zwieksz; // Zwracamy funkcję na zewnątrz!
}
var licznik = GetCounter();
Console.WriteLine(licznik()); // 1
Console.WriteLine(licznik()); // 2
Tutaj, nawet po zakończeniu metody GetCounter, zmienna liczba dalej żyje, bo zwrócona funkcja ją przechwyciła. To się nazywa closure (zamknięcie) — do tego jeszcze wrócimy w osobnym wykładzie, ale na poziomie funkcji lokalnych mechanizm jest taki sam.
6. Typowe błędy i ciekawe scenariusze
Nadpisanie zmiennej przed wywołaniem funkcji lokalnej
Czasem możesz się spotkać z tym, że zmienna, którą przechwytuje funkcja lokalna, zostaje zmieniona przed jej wywołaniem — i wtedy wynik może być inny niż się spodziewasz.
Przykład:
void Przyklad()
{
int x = 42;
void PrintX() { Console.WriteLine(x); }
x = 100; // Zmienna została zmieniona!
PrintX(); // Wypisze 100, a nie 42!
}
Sztuczka: Funkcja lokalna zawsze widzi najnowszą wartość zmiennej w momencie wywołania.
Przechwytywanie zmiennych w pętli for/foreach (jeszcze raz)
Klasyczny ból: jeśli piszesz logikę na rozmowie kwalifikacyjnej albo w dużym projekcie, zawsze sprawdzaj: czy nie przechwytuję “żywej” zmiennej z pętli i jak ona się zachowa.
GO TO FULL VERSION