Модификаторы доступа в C#
Модификаторы доступа определяют, откуда можно обращаться к классу и его членам: из того же класса, из наследников, из своей сборки или из любого кода. В C# их шесть: public, private, protected, internal, protected internal и private protected. Ниже каждый из них показан на примерах, добавлена сравнительная таблица и значения по умолчанию.

Содержание
Зачем нужны модификаторы доступа
Модификатор доступа задаёт уровень видимости типа или его члена: поля, свойства, метода, конструктора, события. Он реализует принцип инкапсуляции: внутреннее устройство класса скрывается, а наружу открывается только то, что нужно для работы. Благодаря этому внутренний код можно менять, не ломая тех, кто пользуется классом. Основы подхода описаны в статье про инкапсуляцию в C#.
Нарушение доступа выявляется на этапе компиляции: если обратиться к закрытому члену снаружи, программа не соберётся, и вы увидите ошибку вида "недоступен из-за его уровня защиты". Это удобно, ведь ошибка находится до запуска, а не во время работы.
Public: доступ отовсюду
Член с модификатором public виден любому коду, который имеет доступ к самому типу, в том числе из других сборок. Публичными делают то, что составляет интерфейс класса: методы, которые должны вызывать другие части программы, и свойства для чтения данных.
public class Account
{
public string Owner = "Иван"; // поле, открытое всем
public decimal Balance { get; private set; } // читать можно всем
public void Deposit(decimal amount)
{
if (amount > 0)
Balance += amount;
}
}
var acc = new Account();
acc.Deposit(500);
Console.WriteLine(acc.Balance); // 500
Открывать поля напрямую считается плохой практикой: любой код сможет записать в них что угодно. Вместо открытых полей используют свойства, о них подробно рассказано в статье про свойства get и set.
Private: только внутри класса
Модификатор private самый строгий. Член виден только внутри того типа, где объявлен, и недоступен даже наследникам. Закрытыми обычно делают поля, вспомогательные методы и всё, что относится к деталям реализации.
public class Counter
{
private int _value; // скрытое состояние
public void Increment() => _value++;
public int Current => _value;
private void Reset() // вспомогательный метод
{
_value = 0;
}
}
var c = new Counter();
c.Increment();
// c._value = 100; // ошибка компиляции: поле недоступно
Console.WriteLine(c.Current); // 1
Соглашение по именованию: закрытые поля принято начинать с подчёркивания и строчной буквы (_value), а открытые члены - с заглавной (Value). Это не требование языка, но так проще отличать уровни доступа в коде.
Protected: внутри класса и наследников
Член с модификатором protected доступен в самом классе и в производных классах, причём в наследнике обращение допустимо через экземпляр самого наследника. Снаружи иерархии такой член недоступен. Модификатор нужен, когда базовый класс хочет поделиться данными именно с потомками.
public class Animal
{
protected string Name;
public Animal(string name) => Name = name;
protected void Log(string message) =>
Console.WriteLine($"[{Name}] {message}");
}
public class Dog : Animal
{
public Dog(string name) : base(name) { }
public void Bark()
{
Log("Гав!"); // доступ к protected-методу базового класса
}
}
var dog = new Dog("Рекс");
dog.Bark();
// dog.Log("..."); // ошибка: метод защищён
Подробнее о том, как классы наследуют друг друга, можно прочитать в статье про наследование в C#. Злоупотреблять protected не стоит: защищённые поля делают наследников зависимыми от внутренностей базового класса, поэтому чаще открывают защищённые методы и свойства.
Internal: внутри сборки
Модификатор internal делает член или тип видимым в пределах одной сборки (одного проекта), но скрывает его от других сборок. Если вы пишете библиотеку, то internal хорошо подходит для вспомогательных классов, которые не должны попасть в публичный интерфейс пакета.
// В проекте библиотеки
internal class PriceCalculator
{
internal decimal ApplyDiscount(decimal price, decimal percent)
=> price - price * percent / 100;
}
public class Shop
{
private readonly PriceCalculator _calc = new();
public decimal GetPrice(decimal price) => _calc.ApplyDiscount(price, 10);
}
Из другого проекта класс PriceCalculator не виден, а Shop доступен. Если нужно дать доступ конкретному проекту, например тестовому, в сборку добавляют атрибут InternalsVisibleTo.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyLibrary.Tests")]
Protected internal и private protected
Два комбинированных модификатора часто путают. Protected internal разрешает доступ в двух случаях: из той же сборки (любой код) или из наследников в других сборках. Это объединение условий. Private protected, наоборот, сужает доступ: член виден только наследникам и только внутри своей сборки. Это пересечение условий.
public class Base
{
protected internal int A; // своя сборка ИЛИ наследники из других сборок
private protected int B; // наследники И только из этой же сборки
}
public class Derived : Base
{
public void Show()
{
Console.WriteLine(A); // доступно
Console.WriteLine(B); // доступно, если Derived в той же сборке
}
}
Модификатор private protected появился в C# 7.2. Его используют редко, в основном в больших библиотеках, где нужно ограничить расширение классов только своей сборкой.
Таблица сравнения модификаторов
Наглядно видно, где каждый уровень доступа разрешает обращение. Знак "да" означает, что доступ есть.
| Модификатор | Тот же класс | Наследник в той же сборке | Не наследник в той же сборке | Наследник в другой сборке | Любой код в другой сборке |
|---|---|---|---|---|---|
| public | да | да | да | да | да |
| protected internal | да | да | да | да | нет |
| internal | да | да | да | нет | нет |
| protected | да | да | нет | да | нет |
| private protected | да | да | нет | нет | нет |
| private | да | нет | нет | нет | нет |
Порядок строк идёт от самого открытого к самому закрытому. Чем ниже строка, тем меньше кода сможет обратиться к члену. Хорошее правило: начинайте с самого строгого уровня и расширяйте доступ только тогда, когда это действительно необходимо.
Значения по умолчанию
Если модификатор не указан, компилятор подставляет уровень по умолчанию, и он зависит от того, что вы объявляете.
| Что объявляется | Доступ по умолчанию |
|---|---|
| Класс, структура, перечисление, интерфейс верхнего уровня | internal |
| Вложенный класс или структура | private |
| Поле, свойство, метод, конструктор класса и структуры | private |
| Член интерфейса | public |
| Член перечисления | public |
| Делегат верхнего уровня | internal |
Несмотря на то, что язык подставляет значения сам, явное указание модификатора делает код понятнее и считается хорошим тоном. Исключение - члены интерфейсов: писать public для них не нужно, а до C# 8 явное указание модификатора вообще запрещалось.
class Helper // без модификатора: internal
{
int count; // без модификатора: private
void Run() { } // private
class Inner { } // вложенный: private
}
interface IShape
{
double Area(); // неявно public
}
Ограничения и правила
- Тип верхнего уровня может быть только public или internal. Модификаторы private и protected допустимы лишь для вложенных типов.
- Член не может быть доступнее, чем его тип. Нельзя объявить публичный метод, который возвращает internal-класс, получится ошибка о несогласованной доступности.
- Наследник не может расширить доступ при переопределении метода: override должен сохранять тот же уровень.
- Для свойств можно задать разный доступ у get и set, например public get и private set, но уровень аксессора должен быть строже, чем у самого свойства.
- Для статических членов модификаторы работают так же, как для обычных; о статических членах есть отдельная статья про ключевое слово static.
public class Product
{
public string Title { get; set; } = "";
public decimal Price { get; private set; } // изменять можно только внутри класса
public void SetPrice(decimal value)
{
if (value < 0) throw new ArgumentException("Цена не может быть отрицательной");
Price = value;
}
}
Как выбирать уровень доступа
На практике достаточно простого алгоритма. Сначала сделайте член private. Если он нужен потомкам, поднимите до protected. Если нужен остальным классам проекта, но не внешним пользователям, используйте internal. Если он является частью публичного контракта, делайте public. Комбинированные модификаторы нужны очень редко.
Такой подход уменьшает площадь публичного API, упрощает рефакторинг и снижает риск случайно сломать данные. Если вы только знакомитесь с классами, полезно сначала прочитать материал про работу с классами и затем вернуться к этой теме.
Частые вопросы
Какой модификатор стоит у класса, если ничего не указать?
Для класса верхнего уровня это internal, то есть он виден в пределах своего проекта. Для вложенного класса по умолчанию действует private.
Чем protected отличается от private?
Private доступен только внутри самого класса, а protected ещё и в классах-наследниках. Если наследование не планируется, выбирайте private.
Чем internal отличается от public?
Public виден из любых сборок, internal только из той, где объявлен. Для приложения из одного проекта разницы почти нет, а вот у библиотек это принципиально.
В чём разница между protected internal и private protected?
Protected internal объединяет условия: подойдёт либо своя сборка, либо наследник. Private protected требует оба условия сразу: только наследник и только из своей сборки.
Можно ли сделать конструктор private?
Да, и это распространённый приём. Закрытый конструктор запрещает создавать объекты снаружи, его используют в паттерне Singleton, во вспомогательных статических классах и при создании объектов через фабричные методы.
Можно ли обойти private через рефлексию?
Технически да, рефлексия способна прочитать и изменить закрытые члены при достаточных правах. Но это нарушает инкапсуляцию, ломается при рефакторинге, поэтому в обычном коде такой приём не применяется.





