Wednesday, March 5, 2014

Модификатор доступа protected internal

Здравствуйте, уважаемые читатели. В этой статье мы поговорим о таких модификаторах доступа, как protected, internal и protected internal. Я выбрал именно эти модификаторы, так как множество программистов не понимают, как работает модификатор доступа protected internal, если честно, то принцип работы этого модификатора до сих пор вводит меня в ступор. Недавно для меня стало открытием то, что некоторые разработчики считают, что для классов нельзя задать модификатор доступа protected и protected internal. Это верно для классов верхнего уровня, но неверно для nested типов, или, другими словами, для вложенных типов данных (структур и классов). Для вложенных классов по умолчанию задается модификатор доступа  private, если не указан другой модификатор доступа. Для вложенных классов мы можем задать такие модификаторы доступа, как private, internal, protected, public и protected internal. Я решил описать данные модификаторы доступа в основном через модификатор доступа protected internal, так как он работает, мягко говоря, странно.
Рассмотрим таблицу доступа для этих типов, которая была взята с MSDN.
Для того чтобы показать, как это работает, я создал такую структуру проекта:
Класс BaseClass сборки AssemblyA выглядит следующим образом:
public class BaseClass
{
       internal virtual void TestInternal()
       {
             Console.WriteLine("Test internal modifier");
       }

       protected virtual void TestProtected()
       {
             Console.WriteLine("Test protected modifier");
       }

       protected virtual internal void TestProtectedInternal()
       {
             Console.WriteLine("Test protected internal modifier");
       }
}
Сборку AssemblyB мы сделаем дружественной для сборки AssemblyA.  Это можно сделать с помощью добавления в файл AssemblyInfo.cs сборки AssemblyA таких строк:
[assembly: InternalsVisibleTo("AssemblyB"),
                    InternalsVisibleTo("AssemblyE")]
Я сразу добавил две дружественные сборки, чтобы удобнее демонстрировать уровни доступа. Посмотрим, какие функции нам будут доступны.
public class ClassA
{
       public void Test()
       {
             var b = new BaseClass();
             //b.TestProtected() not accessed because protected
             b.TestInternal();
             b.TestProtectedInternal();
       }
}
Метод b.TestProtected() не доступен по той причине, что модификатор доступа protected ограничивается содержащим классом или типами, которые являются произвольными от данного класса. Поскольку мы не наследуемся от класса BaseClass, поведение соответствует действительности и оно верное.
Метод b.TestInternal() доступен по той причине, что сборка AssemblyB дружественная к сборке AssemblyA.
Если бы вы спросили у меня несколькими днями ранее, почему доступен метод b.TestProtectedInternal(),  я бы не знал, что вам ответить. По логике, этот методе не должен быть доступен, так как он объявлен как protected internal и наше условие должно удовлетворять критериям protected AND internal. Но как показывает практика, это условие работает по принципу protected OR internal. То есть, у нас доступ будет либо protected, либо internal.  Если мы посмотрим IL код, который получился в сборке AssemblyA, то увидим интересный момент: 
.method famorassem hidebysig newslot virtual
        instance void TestProtectedInternal () cil managed
    {
        // Method begins at RVA 0x206c
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: nop
        IL_0001: ldstr "Test protected internal modifier"
        IL_0006: call void [mscorlib]System.Console::WriteLine(string)
        IL_000b: nop
        IL_000c: ret
    } // end of method BaseClass::TestProtectedInternal
У нас метод обозначен ключевым словом famorassem. Оказывается, CLR имеет понятие ProtectedAndInternal, но C# не имеет синтаксиса для его спецификации. Если посмотреть в CLR с помощью рефлексии, то можно увидеть, что там присутствуют оба понятия: FamANDAssem и FamOrAssem ("Family" – этот термин CLR эквивалентен в C# модификатору protected и "Assem internal). Теперь все становится на свои места. В нашем примере метод  b.TestProtectedInternal() доступен по той причине, что CLR представили его в виде эквивалентного в C# модификатора internal.
Давайте рассмотрим сборку AssemblyC, которая не является дружественной к сборке AssemblyA, и наследуемся от класса BaseClass.
public class ClassB : BaseClass
{
       public void Test()
       {
             TestProtected();
             //TestInternal() not accessed because not friend assembly
             TestProtectedInternal();
       }
}
Здесь результат интуитивно понятен и поддается логике. Функция TestProtected() доступна, так как мы наследовались от класса BaseClass, а если мы посмотрим таблицу выше с описанием модификаторов доступа, то увидим, что так и должно быть. Функция TestInternal() нам недоступна, поскольку сборка AssemblyC не является дружественной сборке AssemblyA, о чем и было написано выше. Функция TestProtectedInternal() доступна по причине, описанной о термине CLR FamOrAssem. Нам доступна эта функция, так как в данном случае с C# это будет интерпретироваться как модификатор доступа protected.
Сборку AssemblyD мы также не будем делать дружественной сборке AssemblyA, но в данном примере мы сделаем override функциям, описанным в классе BaseClass.
public class ClassC : BaseClass
{
       public void Test()
       {
             var c = new ClassC();
             c.TestProtected();
             c.TestProtectedInternal();
       }

       protected override void TestProtected()
       {
             Console.WriteLine("ClassC TestProtected()");
       }

       protected override void TestProtectedInternal()
       {
             Console.WriteLine("ClassC TestProtectedInternal()");
       }
}
Отличие от примера, который приведен выше для сборки AssemblyCнебольшое; основное внимание я хотел бы уделить функции TestProtectedInternal().
В примере мы не можем прописать модификатор internal после protected, так как пример у нас не заработает. Он работает в данном контексте как protected. Это нужно просто запомнить.
И последней мы рассмотрим сборку AssemblyE, которую сделаем дружественной сборке AssemblyA. Здесь мы также применим наследование от класса BaseClass и переопределим доступные функции с базового класса.
public class ClassD : BaseClass
{
       public void Test()
       {
             TestInternal();
             TestProtected();
             TestProtectedInternal();
       }

       internal override void TestInternal()
       {
             Console.WriteLine("ClassD TestInternal()");
       }

       protected override void TestProtected()
       {
             Console.WriteLine("ClassD TestProtected()");
       }

       protected internal override void TestProtectedInternal()
       {
             Console.WriteLine("ClassD TestProtectedInternal()");
       }
}
По поводу функций TestInternal() и TestProtected() не должно возникнуть вопросов. Первая доступна благодаря тому, что сборка AssemblyE является дружественной сборкой. Вторая функция доступна из-за наследования. А вот модификатор доступа protected internal стал доступен благодаря видимости, которую мы прописали в классе AssemblyInfo.cs сборки AssemblyA.
Приведу описание области видимости, которая была задана в сборке AssemblyA.
[assembly: InternalsVisibleTo("AssemblyB"),
                    InternalsVisibleTo("AssemblyE")]
Давайте посмотрим, какие факты мы можем извлечь из данной статьи по поводу модификатора protected internal.

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

No comments:

Post a Comment