Avatar ·

Модификаторы доступа в C#: public, private, protected, internal — в чем разница и когда что использовать?

Всем привет! Я только начал изучать C# и пишу свои первые классы. Споткнулся на модификаторах доступа. Вроде бы понимаю, что `public` — это "для всех", `private` — "только внутри класса", а `protected` — "для наследников". Но на практике у меня возникает путаница. Вот пример. У меня есть класс `BankAccount`:
class BankAccount
{
    public string AccountNumber { get; set; }
    private decimal balance = 0;
    protected decimal interestRate = 0.05m;

    public void Deposit(decimal amount)
    {
        balance += amount;
    }
}
Потом я создаю класс `SavingsAccount`, который наследуется от `BankAccount`:
class SavingsAccount : BankAccount
{
    public void ApplyInterest()
    {
        // Вот тут проблема: я могу использовать interestRate (protected),
        // но не могу использовать balance (private).
        // Это понятно. Но что делать, если я хочу дать доступ к balance
        // только наследникам, но не всем остальным?
    }
}
И еще я заметил, что если не писать модификатор вообще, то вроде бы тоже работает. Например:
class SomeClass
{
    int x = 10; // Без модификатора
}
В чем разница между `protected` и `protected internal`? И когда нужно использовать `internal`? Объясните, пожалуйста, на простых примерах, чтобы новичку было понятно. Заранее спасибо! <<>>

Краткий обзор модификаторов доступа в C#

Модификаторы доступа — это ключевые слова, которые управляют видимостью членов класса (полей, методов, свойств) и самих классов. Они определяют, из каких частей кода можно обращаться к этим членам. Вот основные правила:

  • public — доступен отовсюду: из любого класса, сборки, проекта. Это самый открытый уровень.
  • private — доступен только внутри того же класса или структуры. Даже наследники не видят private-члены.
  • protected — доступен внутри класса и во всех производных классах (наследниках).
  • internal — доступен только в пределах той же сборки (assembly). Если у вас проект и библиотека классов, internal-члены видны только внутри своего проекта.
  • protected internal — комбинация: доступен из наследников (даже из других сборок) ИЛИ из любой точки текущей сборки.
  • private protected — доступен только из наследников, но только если они находятся в той же сборке. Появился в C# 7.2.

Что значит "без модификатора"?

Если вы не указали модификатор доступа, используется значение по умолчанию:

  • Для членов класса (поля, методы, свойства) — private.
  • Для классов верхнего уровня (не вложенных) — internal.

В вашем примере class SomeClass { int x = 10; } поле x на самом деле является private. Поэтому оно доступно только внутри SomeClass.

Решение вашей проблемы с balance

Вы хотите, чтобы поле balance было доступно наследникам, но не всем остальным. Для этого подойдет protected. Измените объявление:

class BankAccount
{
    public string AccountNumber { get; set; }
    protected decimal balance = 0;  // Теперь наследники увидят
    protected decimal interestRate = 0.05m;

    public void Deposit(decimal amount)
    {
        balance += amount;
    }
}

class SavingsAccount : BankAccount
{
    public void ApplyInterest()
    {
        balance += balance * interestRate; // Работает!
    }
}

Теперь SavingsAccount может напрямую читать и изменять balance. При этом из внешнего кода (например, из Program.Main) вы не сможете написать account.balance — компилятор выдаст ошибку.

Когда использовать internal и protected internal?

internal полезен для скрытия реализации внутри проекта. Например, если у вас есть большой проект с несколькими модулями, но вы не хотите, чтобы внешние библиотеки имели доступ к некоторым классам. Пример:

// Внутри вашей библиотеки (DLL)
internal class InternalHelper
{
    public void DoWork() { }
}

// Внешний код эту библиотеку подключил, но InternalHelper не видит

protected internal — это "защищенный ИЛИ внутренний". Он открывает доступ шире, чем просто protected. Пример:

public class BaseClass
{
    protected internal int Value = 10;
}

// В той же сборке:
class SameAssemblyClass
{
    void Test()
    {
        var obj = new BaseClass();
        Console.WriteLine(obj.Value); // Доступно, потому что internal
    }
}

// В другой сборке, но наследник:
class DerivedInOtherAssembly : BaseClass
{
    void Test()
    {
        Console.WriteLine(Value); // Доступно, потому что protected
    }
}

Рекомендации по выбору

  • Начинайте с private — это самый безопасный уровень. Открывайте доступ только тогда, когда это действительно необходимо.
  • Используйте public только для API, которое вы готовы поддерживать вечно (изменение public-члена может сломать чужой код).
  • Для членов, которые нужны наследникам, но не внешнему миру, используйте protected.
  • internal — для внутренней инфраструктуры проекта.
<<>>

Дополнение: различие между инкапсуляцией и модификаторами

Важно понимать, что модификаторы доступа — это лишь один из инструментов инкапсуляции. Они защищают данные от случайного изменения, но не обеспечивают полную безопасность. Например, если у вас есть protected поле, наследник может его испортить. Поэтому часто рекомендуют делать поля private, а для наследников предоставлять protected методы или свойства.

В вашем примере с balance лучше сделать так:

class BankAccount
{
    private decimal balance = 0;

    protected decimal Balance // Свойство только для чтения наследниками
    {
        get { return balance; }
        set
        {
            if (value >= 0) balance = value;
            else throw new ArgumentException("Баланс не может быть отрицательным");
        }
    }

    public void Deposit(decimal amount)
    {
        balance += amount;
    }
}

Теперь наследники могут читать и изменять баланс через свойство Balance, но с проверкой. Это более безопасный подход, чем открытие поля напрямую.

Еще один совет: избегайте protected internal без необходимости — он расширяет область видимости сразу в двух направлениях, что может привести к непреднамеренному использованию. Начните с private, затем protected, и только если нужно — internal или public.