İçeriğe geç / Skip to content / Zum Inhalt

C# Zugriffsmodifizierer: Türen abschließen oder offen lassen?

Ahmet Balaman

Zuletzt aktualisiert:

4 Min. Lesezeit

.NETC#OOPAccess ModifiersEncapsulationProgramming Fundamentals
C# Zugriffsmodifizierer: Türen abschließen oder offen lassen?

Die Lieblingslösung von Einsteigern: alles public machen. Es funktioniert, es gibt keine Fehler, und jeder kommt an alles heran. Großartig, oder?

Nein. Ganz und gar nicht.

Eines Tages setzt eine Kollegin, die Ihren Code nutzt, den Saldo direkt auf -1000. Oder jemand weist dem Altersfeld 500 zu. Oder das Passwortfeld landet in einer Logdatei.

Genau dafür gibt es Zugriffsmodifizierer – um dieses Chaos zu verhindern; die C#-Sprachreferenz zu den Zugriffsmodifizierern beschreibt alle sechs Zugriffsebenen.

Analogie aus dem Alltag: Ihr Zuhause

Denken Sie an Ihre Wohnung:

  • public: Ihre Tür – jeder sieht sie und kann eintreten (sehr gefährlich!)
  • private: Ihr Schlafzimmer – nur Sie dürfen hinein
  • protected: Das Wohnzimmer – nur Familienmitglieder dürfen hinein
  • internal: Der Gemeinschaftsbereich im Haus – nur Bewohner haben Zugang

Die Reichweite der Zugriffsmodifizierer: private am engsten, dann protected, dann internal im Projekt und public überall

Übertragen wir das nun auf Code.

public: für alle offen

public bedeutet „jeder darf zugreifen“ – von außerhalb der Klasse, aus dem Projekt, von überall.

public class User
{
    public string Name { get; set; }
    public string Password { get; set; }  // ⚠️ DANGER!
}

// Somewhere else...
User user = new User();
user.Password = "1234";  // Direct access - Bad!
Console.WriteLine(user.Password);  // Password readable - Very bad!

Weil password public ist, kann es jeder lesen und ändern. Das ist eine ernste Sicherheitslücke.

private: nur die Eigentümerin hat Zugriff

Auf private-Mitglieder kann nur innerhalb der Klasse zugegriffen werden, in der sie definiert sind. Von außen sieht und ändert sie niemand.

public class BankAccount
{
    public string AccountOwner { get; set; }
    private double _balance;  // Only this class can access
    private string _pin;      // Must never leak outside
    
    public BankAccount(string owner, double initialBalance, string pin)
    {
        AccountOwner = owner;
        _balance = initialBalance;
        _pin = pin;
    }
    
    public void Deposit(double amount)
    {
        if (amount > 0)
        {
            _balance += amount;
            Console.WriteLine($"✅ ${amount:F2} deposited. New balance: ${_balance:F2}");
        }
    }
    
    public bool Withdraw(double amount, string enteredPin)
    {
        if (enteredPin != _pin)
        {
            Console.WriteLine("❌ Wrong PIN!");
            return false;
        }
        
        if (amount > _balance)
        {
            Console.WriteLine("❌ Insufficient balance!");
            return false;
        }
        
        _balance -= amount;
        Console.WriteLine($"✅ ${amount:F2} withdrawn. Remaining balance: ${_balance:F2}");
        return true;
    }
    
    public double GetBalance()
    {
        return _balance;  // We allow reading, not modifying
    }
}

Verwendung:

BankAccount account = new BankAccount("John", 5000, "1234");

account.Deposit(1000);
// ✅ $1000.00 deposited. New balance: $6000.00

account.Withdraw(500, "1234");
// ✅ $500.00 withdrawn. Remaining balance: $5500.00

account.Withdraw(500, "4321");
// ❌ Wrong PIN!

// These DON'T WORK:
// account._balance = 1000000;  // ❌ Can't access private
// Console.WriteLine(account._pin);  // ❌ Can't access private

Den Saldo können Sie nicht direkt ändern. Sie müssen Methoden nutzen, um Geld einzuzahlen oder abzuheben – und diese Methoden führen die nötigen Prüfungen durch.

protected: Familienmitglieder haben Zugriff

protected bedeutet: zugänglich innerhalb der Klasse und aus Klassen, die von ihr erben.

public class LivingBeing
{
    public string Name { get; set; }
    protected int _health;  // Only LivingBeing and derivatives can access
    
    public LivingBeing(string name, int health)
    {
        Name = name;
        _health = health;
    }
    
    public void ShowStatus()
    {
        Console.WriteLine($"{Name}: Health {_health}");
    }
}

public class Player : LivingBeing
{
    public int Score { get; set; }
    
    public Player(string name) : base(name, 100)
    {
        Score = 0;
    }
    
    public void TakeDamage(int amount)
    {
        _health -= amount;  // ✅ Can access protected (derived class)
        Console.WriteLine($"💥 {Name} took {amount} damage! Remaining health: {_health}");
        
        if (_health <= 0)
        {
            Console.WriteLine($"☠️ {Name} died!");
        }
    }
    
    public void PickUpHealthPack()
    {
        _health += 25;  // ✅ Can access protected
        if (_health > 100) _health = 100;
        Console.WriteLine($"💚 {Name} picked up health pack! Health: {_health}");
    }
}

// Usage
Player player = new Player("John");
player.ShowStatus();  // John: Health 100

player.TakeDamage(30);    // 💥 John took 30 damage! Remaining health: 70
player.TakeDamage(50);    // 💥 John took 50 damage! Remaining health: 20
player.PickUpHealthPack();  // 💚 John picked up health pack! Health: 45

// This DOESN'T WORK:
// player._health = 9999;  // ❌ protected not accessible from outside

Auf das Feld _health konnten wir innerhalb der Klasse Player zugreifen, weil sie von LivingBeing erbt. Von außen ist der Zugriff jedoch nicht möglich. Die Syntax : LivingBeing ist Vererbung; wie sie mit virtual und override zusammenspielt, erkläre ich im Leitfaden zu Vererbung und Polymorphie.

internal: Zugriff auf Projektebene

Auf internal-Mitglieder kann nur innerhalb desselben Projekts (Assembly) zugegriffen werden. Aus anderen Projekten sind sie nicht erreichbar.

// Inside MyLibrary project
public class DataProcessor
{
    internal string _connectionString = "Server=localhost;Database=App;";
    
    internal void WriteLog(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
    
    public void SaveData(string data)
    {
        WriteLog($"Saving data: {data}");  // ✅ Access from same project OK
        // Save operations...
    }
}

// Another class in the same project
public class ReportGenerator
{
    private DataProcessor _processor = new DataProcessor();
    
    public void GenerateReport()
    {
        _processor.WriteLog("Generating report...");  // ✅ internal access OK
    }
}

Aus einem anderen Projekt dagegen:

// In a different project
DataProcessor processor = new DataProcessor();
processor.WriteLog("Test");  // ❌ Can't access internal from outside
processor.SaveData("Data");  // ✅ public method, accessible

Getter und Setter: kontrollierter Zugriff

Im Beitrag zu Klasse und Objekt haben wir gesehen, warum eine Property einem Field überlegen ist; jetzt gehen wir einen Schritt weiter. Sie können Getter und Setter einer Eigenschaft unterschiedliche Zugriffsstufen geben, wie die C#-Dokumentation zu Eigenschaften zeigt:

public class Product
{
    private decimal _price;
    
    public string Name { get; set; }
    
    // Everyone can read, only class can modify
    public decimal Price
    {
        get { return _price; }
        private set 
        { 
            if (value >= 0)
                _price = value;
        }
    }
    
    // Read-only property
    public string PriceText => $"${_price:N2}";
    
    // Only readable from within class
    private int _stock;
    public int Stock 
    { 
        get => _stock;
        internal set => _stock = value;  // Only same project can modify
    }
    
    public Product(string name, decimal price, int stock)
    {
        Name = name;
        Price = price;  // private set works (we're inside the class)
        _stock = stock;
    }
    
    public void ApplyDiscount(int percent)
    {
        Price = _price * (100 - percent) / 100;  // private set works
        Console.WriteLine($"🏷️ {percent}% discount applied! New price: {PriceText}");
    }
}

Verwendung:

Product laptop = new Product("MacBook Pro", 2499, 10);

Console.WriteLine(laptop.Price);  // ✅ Read: 2499
Console.WriteLine(laptop.PriceText);  // ✅ $2,499.00

// laptop.Price = 100;  // ❌ ERROR! private set

laptop.ApplyDiscount(10);
// 🏷️ 10% discount applied! New price: $2,249.10

Praxisbeispiel: ein System zur Benutzerverwaltung

Setzen wir nun alle Zugriffsmodifizierer gemeinsam ein:

public class User
{
    // Public: Everyone can see
    public int Id { get; private set; }
    public string Username { get; private set; }
    public string Email { get; private set; }
    public DateTime RegistrationDate { get; private set; }
    
    // Private: Only this class
    private string _passwordHash;
    private int _failedLoginAttempts;
    private const int MAX_FAILED_ATTEMPTS = 3;
    
    // Protected: This class and derivatives
    protected bool _isActive;
    protected DateTime? _lastLoginDate;
    
    // Internal: Within project
    internal string _sessionToken;
    
    public User(int id, string username, string email, string password)
    {
        Id = id;
        Username = username;
        Email = email;
        _passwordHash = HashPassword(password);
        RegistrationDate = DateTime.Now;
        _isActive = true;
        _failedLoginAttempts = 0;
    }
    
    public bool Login(string password)
    {
        if (!_isActive)
        {
            Console.WriteLine("❌ Account is disabled!");
            return false;
        }
        
        if (_failedLoginAttempts >= MAX_FAILED_ATTEMPTS)
        {
            Console.WriteLine("🔒 Account locked! Too many failed attempts.");
            return false;
        }
        
        if (HashPassword(password) != _passwordHash)
        {
            _failedLoginAttempts++;
            Console.WriteLine($"❌ Wrong password! ({_failedLoginAttempts}/{MAX_FAILED_ATTEMPTS})");
            return false;
        }
        
        _failedLoginAttempts = 0;
        _lastLoginDate = DateTime.Now;
        _sessionToken = Guid.NewGuid().ToString();
        
        Console.WriteLine($"✅ Welcome, {Username}!");
        return true;
    }
    
    public bool ChangePassword(string oldPassword, string newPassword)
    {
        if (HashPassword(oldPassword) != _passwordHash)
        {
            Console.WriteLine("❌ Current password is wrong!");
            return false;
        }
        
        if (newPassword.Length < 6)
        {
            Console.WriteLine("❌ Password must be at least 6 characters!");
            return false;
        }
        
        _passwordHash = HashPassword(newPassword);
        Console.WriteLine("✅ Password changed successfully!");
        return true;
    }
    
    private string HashPassword(string password)
    {
        // In real application, use secure hash algorithm
        return Convert.ToBase64String(
            System.Text.Encoding.UTF8.GetBytes(password + "salt123")
        );
    }
    
    public void ShowProfile()
    {
        Console.WriteLine($"\n👤 USER PROFILE");
        Console.WriteLine($"ID: {Id}");
        Console.WriteLine($"Username: {Username}");
        Console.WriteLine($"Email: {Email}");
        Console.WriteLine($"Registration: {RegistrationDate:MM/dd/yyyy}");
        Console.WriteLine($"Status: {(_isActive ? "Active ✅" : "Inactive ❌")}");
        Console.WriteLine($"Last Login: {_lastLoginDate?.ToString("MM/dd/yyyy HH:mm") ?? "Never logged in"}");
    }
}

// Admin class - Inherits from User
public class Admin : User
{
    public string Permission { get; set; }
    
    public Admin(int id, string username, string email, string password, string permission) 
        : base(id, username, email, password)
    {
        Permission = permission;
    }
    
    public void BanUser(User user)
    {
        // protected field access - OK because we're a derived class
        // But user._isActive = false; WON'T WORK - different instance
        Console.WriteLine($"🚫 {user.Username} has been banned!");
    }
    
    public void ViewSystemLogs()
    {
        // internal field access - OK because we're in the same project
        Console.WriteLine($"📋 Admin {Username} viewing logs...");
        Console.WriteLine($"🔑 Session Token: {_sessionToken}");
    }
}

Verwendung:

// Regular user
User john = new User(1, "john_dev", "[email protected]", "password123");

john.Login("wrong");      // ❌ Wrong password! (1/3)
john.Login("wrong");      // ❌ Wrong password! (2/3)
john.Login("password123"); // ✅ Welcome, john_dev!

john.ShowProfile();

john.ChangePassword("password123", "newPass456");
// ✅ Password changed successfully!

// These DON'T WORK:
// john._passwordHash = "hack";  // ❌ private
// john._isActive = false;       // ❌ protected

// Admin operations
Admin admin = new Admin(0, "superadmin", "[email protected]", "admin123", "FullAccess");
admin.Login("admin123");
admin.ViewSystemLogs();

Übersichtstabelle

Modifizierer Gleiche Klasse Abgeleitete Klasse Gleiches Projekt Außerhalb
public ✅ ✅ ✅ ✅
private ✅ ❌ ❌ ❌
protected ✅ ✅ ❌ ❌
internal ✅ ❌* ✅ ❌
protected internal ✅ ✅ ✅ ❌
private protected ✅ ✅** ❌ ❌

*Eine abgeleitete Klasse kann auf internal zugreifen, wenn sie im selben Projekt liegt.
**Nur abgeleitete Klassen im selben Projekt haben Zugriff.

Wann verwenden Sie was?

Situation Empfehlung
Soll von außen erreichbar sein public
Nur innerhalb der Klasse private (betrachten Sie das als Standard)
Abgeleitete Klassen sollen zugreifen protected
Gemeinsam im Projekt genutzt internal
Sensible Daten (Passwort, Token) private + kontrollierter Zugriff über Methoden

Einführung in die Kapselung

Zugriffsmodifizierer sind letztlich die Bausteine eines der vier Prinzipien aus der Einführung in OOP: der Kapselung.

Kapselung besagt:

  • Verbergen Sie die Daten (private)
  • Bieten Sie kontrollierten Zugriff (Methoden und Eigenschaften)
  • Schützen Sie den inneren Zustand des Objekts

Als Nächstes folgt in der Reihe Vererbung und Polymorphie, wo protected seine eigentliche Stärke zeigt. Für die Seite der Abstraktion gibt es den Beitrag Interface vs. abstrakte Klasse.

Fazit: Denken Sie standardmäßig private

Die goldene Regel: Beginnen Sie mit private und öffnen Sie nur, was nötig ist.

// ❌ Bad - Everything public
public class Bad
{
    public string password;
    public int balance;
}

// ✅ Good - Minimum necessary access
public class Good
{
    private string _password;
    private int _balance;
    
    public int Balance => _balance;  // Read-only
    
    public void ValidatePassword(string password) { ... }
    public void UpdateBalance(int amount) { ... }
}

Sauberer und sicherer Code! 🚀


Dies ist der vierte Beitrag der Reihe „OOP mit C#“. Im vorherigen Beitrag ging es um Konstruktoren; als Nächstes folgen Vererbung und Polymorphie.

Häufig gestellte Fragen

Was gilt standardmäßig, wenn ich keinen Zugriffsmodifizierer schreibe?

Für Klassenmitglieder (Fields, Properties, Methoden) ist der Standard private. Für eine auf Namespace-Ebene deklarierte Klasse ist der Standard internal; soll sie außerhalb des Projekts nutzbar sein, müssen Sie ausdrücklich public schreiben.

Was ist der Unterschied zwischen protected internal und private protected?

protected internal bedeutet „abgeleitete Klasse ODER gleiche Assembly“; eines von beiden genügt. private protected bedeutet „abgeleitete Klasse UND gleiche Assembly“; nur abgeleitete Klassen im selben Projekt haben Zugriff.

Worin unterscheiden sich private set und init?

private set erlaubt Zuweisungen innerhalb der Klasse jederzeit. init erlaubt die Zuweisung nur während der Objekterzeugung (im Konstruktor oder per Object Initializer) und danach nie wieder – die richtige Wahl für unveränderliche Objekte.

Kommentare