Modyfikatory dostępu — kto może użyć elementu

Automat udostępnia przyciski, ale nie pozwala przechodniom przestawiać licznika sprzedaży. Klasa z publicznym polem obok metody, która to pole sprawdza, nie pilnuje niczego — wystarczy jedno przypisanie, żeby ominąć wszystkie reguły.

C# public private internal 45–90 min
CEL LEKCJI

Czego się dziś nauczysz

  • Wskażesz, kto ma dostęp do składowej oznaczonej każdym z czterech modyfikatorów.
  • Podasz regułę domyślną dla składowej i dla klasy.
  • Zamkniesz pole i udostępnisz zamiast niego metodę.
  • Wyjaśnisz, dlaczego obiekt widzi prywatne pola innego obiektu tej samej klasy.
  • Rozpoznasz komunikat CS0122 i będziesz wiedział, co go powoduje.

Przygotowanie: lekcje 01–38. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Co się dzieje, gdy wszystko jest publiczne

Klasa Konto ma metodę pilnującą, żeby nie dało się wypłacić więcej, niż jest na koncie:

definicja typu
class Konto
{
    public decimal Saldo;

    public void Wyplata(decimal kwota)
    {
        if (kwota > Saldo)
        {
            Console.WriteLine("Za malo srodkow");
            return;
        }

        Saldo -= kwota;
    }
}
Program.cs
Konto k = new Konto();
k.Saldo = 100;

k.Wyplata(500);      // Za malo srodkow - dziala

k.Saldo = -9000;     // ...i tyle z pilnowania

Sprawdzenie w metodzie nic nie daje, jeśli obok stoi otwarte pole, które każdy może ustawić na dowolną wartość. Obiekt nie panuje nad własnym stanem.

Modyfikator dostępu pozwala powiedzieć, co jest wejściem dla innych, a co wewnętrznym mechanizmem.

definicja typu
class Konto
{
    private decimal saldo;              // tego nikt z zewnatrz nie tknie

    public void Wplata(decimal kwota)
    {
        if (kwota <= 0) return;
        saldo += kwota;
    }

    public void Wyplata(decimal kwota)
    {
        if (kwota <= 0 || kwota > saldo)
        {
            Console.WriteLine("Operacja odrzucona");
            return;
        }

        saldo -= kwota;
    }

    public decimal Stan()
    {
        return saldo;
    }
}
Program.cs
Konto k = new Konto();
k.Wplata(100);
k.Wyplata(500);            // Operacja odrzucona

// k.saldo = -9000;        // BLAD CS0122: 'Konto.saldo' jest niedostepne

Console.WriteLine(k.Stan());   // 100

Obudowa automatu

Automat z napojami ma przyciski i otwór na monety — to jego część publiczna. Licznik sprzedaży i kasa są w środku, za obudową. Nie dlatego, że to tajemnica, tylko dlatego, że przechodzień, który by je przestawiał, popsułby działanie całości. Klasa działa tak samo.

TEORIA

Cztery słowa i to, co naprawdę znaczą

ModyfikatorKto ma dostępTypowe użycie
publickażdy kod, także z innego projektumetody, które są wejściem do klasy
privatetylko wnętrze tej samej klasypola i metody pomocnicze — domyślny wybór
internalcały kod w tym samym projekcieelementy wspólne dla projektu, ukryte przed innymi
protectedta klasa i klasy, które po niej dziedzicząelementy dla klas pochodnych — lekcja 45

Co się dzieje, gdy nic nie napiszesz

Składowa klasy bez modyfikatora jest private. Sama klasa bez modyfikatora jest internal. Warto pisać modyfikator jawnie — czytelnik nie musi wtedy pamiętać dwóch różnych reguł domyślnych.

definicja typu
class Robot          // domyslnie internal
{
    int energia;                      // domyslnie private
    private int przejechane;          // to samo, tylko widac
    public void Rusz() { }            // widoczne z zewnatrz
    private void ZuzyjEnergie() { }   // mechanizm wewnetrzny
}

Zasada, od której zaczynamy

Domyślnie wszystko jest private. Publiczne robimy dopiero to, czego ktoś z zewnątrz naprawdę potrzebuje — i dopiero wtedy, gdy wiemy, po co.

Łatwiej otworzyć niż zamknąć

Pole prywatne można w każdej chwili upublicznić i nic się nie zepsuje. Publiczne, z którego już ktoś korzysta, trudno zamknąć — trzeba poprawić wszystkie miejsca użycia. Dlatego zaczynamy od zamkniętego, a otwieramy na żądanie.

TEORIA

Prywatne znaczy „dla tej klasy”, nie „dla tego obiektu”

To zaskakuje niemal każdego przy pierwszym spotkaniu. Metoda obiektu widzi prywatne pola innego obiektu tej samej klasy:

definicja typu
class Gracz
{
    private int punkty;

    public void Dodaj(int ile)
    {
        punkty += ile;
    }

    public bool WygrywaZ(Gracz inny)
    {
        return punkty > inny.punkty;   // siegamy do prywatnego pola INNEGO obiektu
    }
}

To poprawny kod. private ogranicza dostęp do typu, a nie do pojedynczego egzemplarza — a klasa ma prawo znać wnętrze swoich obiektów. Bez tego nie dałoby się napisać porównania dwóch graczy bez otwierania pola na cały świat.

Dostęp nie zależy od pliku

Klasa Program zapisana w tym samym pliku co Konto nie widzi jego prywatnych pól. Liczy się granica klasy, a nie granica pliku. Komunikat CS0122 pojawi się także wtedy, gdy obie klasy stoją obok siebie w jednym Program.cs.

Metody prywatne

Prywatne bywają nie tylko pola. Metoda pomocnicza, która jest krokiem w środku większej czynności, nie musi być widoczna na zewnątrz:

definicja typu
class Raport
{
    public string Zbuduj(decimal kwota)
    {
        return $"{Naglowek()}\nDo zaplaty: {Sformatuj(kwota)}";
    }

    private string Naglowek()
    {
        return "RACHUNEK";
    }

    private string Sformatuj(decimal kwota)
    {
        return $"{kwota:F2} zl";
    }
}

Użytkownik klasy wywołuje jedną metodę Zbuduj. Pozostałe dwie są szczegółem, który wolno zmienić bez pytania nikogo o zgodę.

ROZSZERZENIE

internal, czyli granica projektu

internal otwiera składową dla całego projektu, ale zamyka ją przed kodem z zewnątrz. W programie konsolowym złożonym z jednego projektu zachowuje się dokładnie tak samo jak public — różnica pojawia się dopiero wtedy, gdy rozwiązanie ma kilka projektów, na przykład bibliotekę i korzystającą z niej aplikację.

definicja typu
// projekt: Biblioteka
public class Kalkulator          // widoczna dla innych projektow
{
    public int Dodaj(int a, int b) => a + b;

    internal int LicznikWywolan;  // widoczne tylko w projekcie Biblioteka
}

A klasy zaprzyjaźnione?

W C++ słowo friend pozwala wskazanej klasie lub funkcji sięgnąć do prywatnych składowych. C# nie ma takiego mechanizmu — i celowo, bo łamie on hermetyzację. Najbliższym odpowiednikiem jest właśnie internal. Przy testach jednostkowych z lekcji 56 dodaje się czasem wpis InternalsVisibleTo w pliku projektu, żeby projekt testowy widział składowe internal — to jedyne miejsce, w którym C# zbliża się do idei zaprzyjaźnienia.

protected na zapowiedź

Czwarty modyfikator, protected, otwiera składową dla klas dziedziczących po tej klasie. Dopóki nie ma dziedziczenia, nie ma po nim widocznej różnicy względem private. Wracamy do niego w lekcji 45.

PRZYKŁAD

Przykład: automat, którego nie da się oszukać

Program.cs
class Automat
{
    private decimal kasa = 0m;
    private int sprzedane = 0;
    private decimal wrzucone = 0m;
    private readonly decimal cena;

    public Automat(decimal cena)
    {
        this.cena = cena;
    }

    public void Wrzuc(decimal kwota)
    {
        if (!Poprawna(kwota))
        {
            Console.WriteLine($"Odrzucono monete {kwota:F2}");
            return;
        }

        wrzucone += kwota;
        Console.WriteLine($"Wrzucono {kwota:F2}, razem {wrzucone:F2}");
    }

    public void Kup()
    {
        if (wrzucone < cena)
        {
            Console.WriteLine($"Brakuje {cena - wrzucone:F2}");
            return;
        }

        decimal reszta = wrzucone - cena;
        kasa += cena;
        sprzedane++;
        wrzucone = 0m;

        Console.WriteLine($"Napoj wydany, reszta {reszta:F2}");
    }

    public string Raport()
    {
        return $"Sprzedano: {sprzedane}, w kasie: {kasa:F2} zl";
    }

    private bool Poprawna(decimal kwota)
    {
        return kwota == 0.50m || kwota == 1m || kwota == 2m || kwota == 5m;
    }
}

class Program
{
    static void Main()
    {
        Automat a = new Automat(3.50m);

        a.Wrzuc(2m);
        a.Wrzuc(0.30m);      // nie ma takiej monety
        a.Kup();             // za malo
        a.Wrzuc(2m);
        a.Kup();

        Console.WriteLine();
        Console.WriteLine(a.Raport());

        // a.kasa = 1000m;       // CS0122 - niedostepne
        // a.Poprawna(0.10m);    // CS0122 - metoda prywatna
    }
}
wynik w konsoli
Wrzucono 2,00, razem 2,00
Odrzucono monete 0,30
Brakuje 1,50
Wrzucono 2,00, razem 4,00
Napoj wydany, reszta 0,50

Sprzedano: 1, w kasie: 3,50 zl

Co dzieje się po kolei

  • Wszystkie cztery pola są prywatne. Z zewnątrz dostępne są tylko trzy czynności: wrzuć, kup, pokaż raport. Tyle wystarczy do korzystania z automatu.
  • Poprawna jest prywatna, bo to wewnętrzna reguła automatu. Gdy lista akceptowanych monet się zmieni, nikt poza tą klasą tego nie zauważy.
  • Po sprzedaży wrzucone wraca do zera, a kasa i sprzedane rosną. Trzy pola zmieniają się razem i zawsze w komplecie — bo zmienia je jedna metoda.
  • Dwa zakomentowane wiersze na końcu to sedno lekcji: kod spoza klasy nie ma jak zepsuć jej stanu. Odkomentuj je i przeczytaj komunikat CS0122.
  • cena jest readonly i prywatna — ustalona przy tworzeniu automatu, niezmienna przez cały czas jego istnienia.
ELEMENTY WBUDOWANE

Zestawienie elementów

ElementZasięgUwagi
publicwszędzieWejście do klasy. Każda składowa publiczna to obietnica, którą trudno potem wycofać.
privatewnętrze tej samej klasyDomyślny wybór dla pól i metod pomocniczych. Obowiązuje na poziomie klasy, nie obiektu.
internalten sam projektW programie z jednym projektem nieodróżnialne od public.
protectedta klasa i klasy pochodneSens pojawia się dopiero przy dziedziczeniu (lekcja 45).
brak modyfikatora przy składowejjak privateDomyślna reguła dla pól i metod w klasie.
brak modyfikatora przy klasiejak internalInna reguła domyślna niż dla składowych — stąd zamieszanie.
CS0122„Składowa jest niedostępna ze względu na poziom ochrony” — to komunikat tej lekcji.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
Publiczne pole obok metody, która je sprawdzaSprawdzenie da się ominąć jednym przypisaniem. Albo pole jest prywatne, albo sprawdzanie nie ma sensu.
Wszystko public „na wszelki wypadek”Każda składowa publiczna wiąże ci ręce przy późniejszych zmianach. Otwieraj to, co potrzebne, nie wszystko.
Wszystko private, także metody wejścioweKlasa, z której nie da się skorzystać. Publiczne ma być to, co stanowi jej przeznaczenie.
Oczekiwanie, że private chroni przed innym obiektem tej samej klasyNie chroni — dostęp obowiązuje na poziomie klasy. To celowe i potrzebne przy porównaniach.
Liczenie na to, że wspólny plik daje dostępGranicą jest klasa, nie plik. CS0122 pojawi się też w jednym Program.cs.
protected w klasie bez dziedziczeniaKompiluje się, ale zachowuje się jak private i myli czytelnika.
ZADANIA

Zadania

ZAD 1Zamknij pole★☆☆

Weź klasę Konto z publicznym polem Saldo i metodą Wyplata. Ustaw saldo z Main na wartość ujemną i pokaż, że program to przyjmuje. Następnie zmień pole na prywatne, dodaj metodę Stan() i zapisz treść błędu, który pojawia się teraz przy próbie zmiany salda.

ZAD 2Modyfikator jawnie★☆☆

Napisz klasę z czterema składowymi: polem bez modyfikatora, polem private, metodą bez modyfikatora i metodą public. Z Main spróbuj użyć każdej z nich i zapisz, które próby się nie kompilują.

ZAD 3Metoda pomocnicza★★☆

Napisz klasę Haslo z metodą publiczną bool Poprawne(string tekst) oraz trzema metodami prywatnymi sprawdzającymi po jednym warunku: długość co najmniej osiem znaków, obecność cyfry i obecność wielkiej litery. Publiczna metoda ma tylko łączyć wyniki trzech prywatnych.

ZAD 4internal w praktyce★★☆

W jednym projekcie utwórz klasę Licznik z polem private int wartosc, metodą internal void Zwieksz() i metodą public int Odczytaj(). Z Main spróbuj sięgnąć po każdą z nich, zapisz, która próba się nie kompiluje, i wyjaśnij w komentarzu, dlaczego internal zachowuje się tutaj tak samo jak public.

ZAD 5Porównanie graczy★★☆

Napisz klasę Gracz z prywatnym polem punktów, metodą Dodaj oraz metodą bool WygrywaZ(Gracz inny) sięgającą do prywatnego pola przekazanego gracza. Uruchom program i wyjaśnij w komentarzu, dlaczego to się kompiluje, mimo że pole jest prywatne.

ZAD 6Sejf na kod★★★

Napisz klasę Sejf z prywatnym kodem ustawianym w konstruktorze i prywatnym licznikiem nieudanych prób. Metoda bool Otworz(string kod) ma otwierać sejf przy poprawnym kodzie i zerować licznik, a przy trzech błędnych próbach z rzędu blokować sejf na stałe — także przed poprawnym kodem. Z zewnątrz ma być dostępne wyłącznie otwieranie i sprawdzenie, czy sejf jest zablokowany. Sprawdź program na czterech scenariuszach, w tym na poprawnym kodzie podanym po blokadzie.

PODSUMOWANIE

Co trzeba zapamiętać

  • private zamyka składową we wnętrzu klasy, public otwiera ją dla każdego.
  • Składowa bez modyfikatora jest prywatna, klasa bez modyfikatora — internal.
  • Zaczynamy od prywatnego; publiczne robimy to, co ma być wejściem do klasy.
  • Publiczne pole obok metody sprawdzającej sprawia, że sprawdzenie da się ominąć.
  • private działa na poziomie klasy — obiekt widzi prywatne pola innego obiektu tej samej klasy.
  • Granicą dostępu jest klasa, a nie plik; CS0122 pojawia się także w jednym pliku.
  • internal otwiera składową w obrębie projektu i jest najbliższym odpowiednikiem friend z C++.

Dokumentacja: Microsoft Learn — temat tej lekcji.