Введение в ООП с примерами на C#. Часть пятая. Всё о модификаторах доступа

Рассказывает Akhil Mittal


В прошлых статьях серии “Введение в ООП” мы рассматривали полиморфизм (а также нюансы использования его на практике), наследование и абстрактные классы. В этой части я постараюсь раскрыть все тонкости использования модификаторов доступа, которые знаю сам. Продолжаем погружаться в ООП!

Что такое модификаторы доступа?

Давайте в этот раз возьмём определение из Википедии (в русской Википедии статьи access modifiers нет, поэтому здесь приводим свой перевод — прим. перев.):

Модификаторы доступа (или спецификаторы доступа) — ключевые слова в объектно-ориентированных языках, которые задают (внезапно!) параметры доступа для классов, методов и прочих элементов. Модификаторы доступа — специфичная часть языков программирования для облегчения инкапсуляции компонентов.

Модификаторы public, private, protected

Каждый раз, когда мы создаём класс, мы хотим иметь возможность определять, кто и откуда может взаимодействовать с его членами. Иными словами, нам иногда нужно ограничивать доступ к некоторым членам класса. Есть одно простое правило — члены одного класса всегда имеют доступ друг к другу. Если же говорить про доступ извне, то стоит запомнить, что модификатор доступа по умолчанию – private, т.е. все члены класса доступны только изнутри него самого.

Традиционно сразу переходим к практике. Давайте попробуем выполнить следующий код:

using System;

namespace AccessModifiers
{
    class Modifiers
    {
        static void AAA()
        {
            Console.WriteLine("Modifiers AAA");
        }

        public static void BBB()
        {
            Console.WriteLine("Modifiers BBB");
            AAA();
        }
    }

     class Program
    {
        static void Main(string[] args)
        {
            Modifiers.BBB();
        }
    }
}

Результатом выполнения этого кода будет:

Modifiers BBB
Modifiers AAA

BBB() отмечен как public, соответственно его можно вызывать откуда угодно. Метод AAA() же никак не отмечен, значит, он является приватным. Однако для члена того же класса (ведь AAA() и BBB() принадлежат одному классу, верно?) это не имеет никакого значения.

Теперь попробуем получить доступ к AAA() напрямую:

class Program
    {
        static void Main(string[] args)
        {
            Modifiers.AAA();
            Console.ReadKey();
        }
    }

Вывод:

‘AccessModifiers.Modifiers.AAA()’ is inaccessible due to its protection level

Для внешних вызовов модификатор private — непреодолимая преграда. То же самое можно сказать и о модификаторе protected.

Модификаторы доступа и наследование

Снова попробуем выполнить код:

class ModifiersBase
    {
        static void AAA()
        {
            Console.WriteLine("ModifiersBase AAA");
        }
        public static void BBB()
        {
            Console.WriteLine("ModifiersBase BBB");
        }
        protected static void CCC()
        {
            Console.WriteLine("ModifiersBase CCC");
        }
    }

class ModifiersDerived:ModifiersBase
{
    public static void XXX()
    {
        AAA();
        BBB();
        CCC();
    }
}

class Program
{
    static void Main(string[] args)
    {
        ModifiersDerived.XXX();
        Console.ReadKey();
    }
}

Запускаем код и видим…

‘AccessModifiers.ModifiersBase.AAA()’ is inaccessible due to its protection level

Приватные члены недоступны даже дочерним классам. Публичные члены доступны всем, это понятно. Модификатор же protected по сути и обозначает, что член доступен только дочерним классам — вызов CCC() в примере выше не вызывает никаких ошибок.

Модификатор Internal для классов

Давайте рассмотрим следующий сценарий: мы создаём в новой библиотеке классов (назовём её AccessModifiersLibrary) класс ClassA и помечаем его как internal:

AccessModifiersLibrary.ClassA:

namespace AccessModifiersLibrary
{
    internal class ClassA
    {
    }
}

Теперь в созданном ранее файле попробуем выполнить:

using AccessModifiersLibrary;

namespace AccessModifiers
{
    class Program
    {
        static void Main(string[] args)
        {
            ClassA classA;
        }
    }
}

Compile time error: ‘AccessModifiersLibrary.ClassA’ is inaccessible due to its protection level

Мы встретили эту ошибку из-за спецификатора доступа internal, который обозначает, что ClassA доступен только внутри AccessModifiersLibrary и ниоткуда больше. Впрочем, если мы уберём этот модификатор, ничего не изменится — internal является спецификатором по умолчанию.

Модификаторы для пространств имён

Давайте попробуем сделать с предыдущим кодом следующее:

public namespace AccessModifiers
{
    class Program
    {
        static void Main(string[] args)
        {

        }
    }
}

Конечно, это не скомпилируется:

Compile time error: A namespace declaration cannot have modifiers or attributes

Все пространства имён по умолчанию являются публичными, и мы не можем добавить к их объявлению никаких модификаторов, включая ещё один public.

Приватные классы

 namespace AccessModifiers
{
    private class Program
    {
        static void Main(string[] args)
        {

        }
    }
}

Если мы попробуем скомпилировать код, приведённый выше, то получим ошибку:

Compile time error: Elements defined in a namespace cannot be explicitly declared as private, protected, or protected internal

Всё правильно: классы могут быть либо public, либо internal.

Подробнее о модификаторах членов класса

Что будет, если мы захотим назначить члену класса больше одного модификатора доступа?

namespace AccessModifiers
{
    public class Program
    {
        static void Main(string[] args)
        {
        }

        public private void Method1()
        {

        }
    }
}

Будет ошибка компиляции:

Compile time error: More than one protection modifier

А как поведёт себя язык, если мы создадим public метод в internal классе?

namespace AccessModifiersLibrary
{
    internal class ClassA
    {
        public void MethodClassA(){}
    }
}
using AccessModifiersLibrary;

 namespace AccessModifiers
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ClassA classA = new ClassA();
            classA.MethodClassA();
        }
    }
}

Вывод после компиляции:

‘AccessModifiersLibrary.ClassA’ is inaccessible due to its protection level

The type ‘AccessModifiersLibrary.ClassA’ has no constructors defined

‘AccessModifiersLibrary.ClassA’ is inaccessible due to its protection level

‘AccessModifiersLibrary.ClassA’ does not contain a definition for ‘MethodClassA’ and
no extension method ‘MethodClassA’ accepting a first argument of type ‘AccessModifiersLibrary.ClassA’
could be found (are you missing a using directive or an assembly reference?)

Как много ошибок… Дело в том, что какими бы модификаторами не обладали члены internal класса, их всё равно нельзя вызвать оттуда, где не виден сам класс. А что будет, если мы попробуем сделать наоборот — вызвать private или internal метод у public класса?

AccessModifiersLibrary.ClassA:

namespace AccessModifiersLibrary
{
    public class ClassA
    {
        private void MethodClassA(){}
    }
}
using AccessModifiersLibrary;

 namespace AccessModifiers
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ClassA classA = new ClassA();
            classA.MethodClassA();
        }
    }
}

‘AccessModifiersLibrary.ClassA’ does not contain a definition
for ‘MethodClassA’ and no extension method ‘MethodClassA’ accepting a first argument
of type ‘AccessModifiersLibrary.ClassA’ could be found (are you missing a using directive or an assembly reference?)

Не-а, всё равно не работает. А если изменим модификатор метода на internal?

AccessModifiersLibrary.ClassA:

namespace AccessModifiersLibrary
{
    public class ClassA
    {
        Internal void MethodClassA(){}
    }
}

‘AccessModifiersLibrary.ClassA’ does not contain a definition for ‘MethodClassA’ and no extension
method ‘MethodClassA’ accepting a first argument of type ‘AccessModifiersLibrary.ClassA’ could be
found (are you missing a using directive or an assembly reference?)

Увы, так делать тоже нельзя.

Модификатор protected internal

namespace AccessModifiersLibrary
{
    public class ClassA
    {
        protected internal void MethodClassA()
        {

        }
    }

    public class ClassB:ClassA
    {
        protected internal void MethodClassB()
        {
            MethodClassA();
        }
    }

    public class ClassC
    {
        public void MethodClassC()
        {
            ClassA classA=new ClassA();
            classA.MethodClassA();
        }
    }
}
using AccessModifiersLibrary;

 namespace AccessModifiers
{
    public class Program
    {
        public static void Main(string[] args)
        {
            ClassC classC=new ClassC();
            classC.MethodClassC();
        }
    }
}

Этот код компилируется без ошибок. Модификатор internal proteted (как не слишком сложно догадаться) даёт понять, что метод доступен как для вызовов из того же файла, в котором он объявлен, так и для вызовов из дочерних классов.

Protected поля

Здесь всё будет немного сложнее. Давайте напишем следующий код:

 namespace AccessModifiers
{
    class AAA
    {
        protected int a;
        void MethodAAA(AAA aaa,BBB bbb)
        {
            aaa.a = 100;
            bbb.a = 200;
        }
    }
     class BBB:AAA
     {
         void MethodBBB(AAA aaa, BBB bbb)
         {
             aaa.a = 100;
             bbb.a = 200;
         }
     }
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }
}

Если мы его запустим, то получим ошибку:

Cannot access protected member ‘AccessModifiers.AAA.a’ via a qualifier of type ‘AccessModifiers.AAA’;
the qualifier must be of type ‘AccessModifiers.BBB’ (or derived from it)

Совершенно неочевидно, правда? Компилятор ругается на строчку aaa.a = 100 из метода MethodBBB. Почему никаких ошибок не вызывает метод MethodAAA понять достаточно просто — поле a объявлено в том же файле, в том же классе, в котором к нему и происходит обращение, это не может быть ошибкой. Почему в классе BBB доступен член bbb.a тоже понятно — модификатор protected прямо разрешает использовать члены родительского класса в дочернем как свои. Почему же вызов aaa.a = 100 из метода MethodBBB под запретом? Пожалуй, это стоит просто запомнить.

(От редакции) Скорее всего, это сделано, чтобы нельзя было делать следующим образом:

class AAA{
    protected int a = VALUE;
}
class BBB:AAA{
    public static int getAllTheShit(AAA aaa){
        return aaa.a;
    }
}

//....
public static void Main(string[] args)
{
    theSecret = new AAA();
    theSecret.a /// error
    BBB.getAllTheShit(theSecret) /// azazaz vzlomali

}
 

Приоритет модификаторов

 namespace AccessModifiers
{
    class AAA
    {

    }
    public class BBB:AAA
     {

     }
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }
}

Compile time error: Inconsistent accessibility: base class ‘AccessModifiers.AAA’ is less accessible than class ‘AccessModifiers.BBB’

К дочернему классу не может быть большего доступа, чем к родительскому. Как вы понимаете, public предоставляет гораздо больший доступ, чем модификатор по умолчанию internal. Причём нельзя делать даже так:

 namespace AccessModifiers
{
    class AAA
    {

    }
    public class BBB
     {
        public AAA MethodB()
        {
            AAA aaa= new AAA();
            return aaa;
        }
     }
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }
}

Inconsistent accessibility: return type ‘AccessModifiers.AAA’ is less accessible than method ‘AccessModifiers.BBB.MethodB()’

Или так:

 namespace AccessModifiers
{
    class AAA
    {

    }
    public class BBB
    {
        public AAA aaa;
    }
    public class Program
    {
        public static void Main(string[] args)
        {
        }
    }
}

Inconsistent accessibility: field type ‘AccessModifiers.AAA’ is less accessible than field ‘AccessModifiers.BBB.aaa’

Подведём итоги:

  • Модификатор доступа по умолчанию для членов класса — private;
  • Модификатор доступа internal значит, что доступ разрешён только из того же файла;
  • У пространств имён нет и не может быть модификаторов доступа (можно считать, что они все public);
  • Классы могут иметь только два модификатора доступа — internal (по умолчанию) и public
  • Модификатор protected internal значит, что доступ есть как из того же файла, так и из дочерних классов
  • Родительский класс не может быть менее доступен, чем дочерний
  • Возвращаемое значение метода не может быть менее доступно, чем сам метод
  • Поле не может быть более доступно, чем его тип

Работу с константами и sealed классами (которая тоже осуществляется за счёт модификаторов доступа) мы разберём в следующей статье.

Diving into OOP (Day 5): All About C# Access Modifiers