Пролог: internal is new public


Каждый из нас мечтал о проекте, где всё будет сделано правильно. Это кажется вполне естественным. Как только ты узнаёшь о самой возможности писать хороший код, как только слышишь легенды о том самом коде, который можно легко читать и изменять, сразу загораешься тем самым «ну вот теперь я точно всё сделаю правильно, я ведь теперь умный и Макконнела читал».


image

Случился такой проект и в моей жизни. Очередной. Причём делаю я его под добровольным надзором, где за каждой моей строчкой следят. Соответственно, уже не только хотелось, но и надо было делать всё правильно. Одним из «правильно» было «чти инкапсуляцию и закрывайся по максимуму, потому что открыться всегда успеешь, а закрыться обратно потом будет поздно». И поэтому я везде, где только мог, стал использовать для классов модификатор доступа internal вместо public. И, естественно, когда ты начинаешь активно использовать новую для тебя фичу языка, возникают некоторые нюансы. О них по порядку и хочу рассказать.


Оскорбительно базовая справка

Исключительно для того, чтобы напомнить и обозначить.


  • Сборка — минимальная единица развёртывания в .NET и одна из основных единиц компиляции. Как правильно, это или .dll, или .exe. Говорят, её можно поделить на несколько файлов, которые называются модулями.
  • public — модификатор доступа, который обозначает, что им помеченное доступно вообще всем.
  • internal — модификатор доступа, который обозначает, что им помеченное доступно только внутри сборки.
  • protected — модификатор доступа, который обозначает, что им помеченное доступно только наследникам класса, в котором расположено помеченное.
  • private — модификатор доступа, который обозначает, что им помеченное доступно только тому классу, в котором оно находится. И никому больше.


Юнит-тесты и дружественные сборки


В C++ была такая странноватая фича, как friendly classes. Классы можно было назначить друзьями, и тогда граница инкапсуляции между ними стиралась. Подозреваю, что это не самая странная фича в C++. Возможно, даже в десятку самых странных не входит. Но выстрелить себе в ногу, связав несколько классов намертво, как-то слишком уж легко, а подходящий случай под эту фичу придумать очень тяжело.


Тем удивительнее было узнать, что в .NET существуют дружественные сборки, этакое переосмысление. То есть, вы можете сделать так, чтобы одна сборка видела то, что спрятано за internal-замком в другой сборке. Я когда узнал об этом, был несколько удивлён. Ну, как бы, зачем? В чём смысл? Кто будет намертво связывать две сборки, занявшись их разделением? Случаи, когда в любой непонятной ситуации лепят public, мы в этой статье не рассматриваем.


А потом в том же проекте я начал познавать одну из ветвей пути настоящего самурая: юнит-тестирование. А по фэн-шую юнит-тесты должны лежать в отдельной сборке. По тому же фэн-шую всё, что можно спрятать внутри сборки, нужно спрятать внутри сборки. Я встал перед весьма и весьма неприятным выбором. Или тесты будут лежать рядышком и уходить клиенту вместе с полезным для него кодом, или всё покроется ключевым словом public, как долго лежавший в сырости хлебушек.


И вот тут откуда-то из закромов моей памяти было добыто что-то про дружественные сборки. Оказалось, что если у вас есть сборка «YourAssemblyName», то можно написать вот так:


[assembly: InternalsVisibleTo("YourAssemblyName.Tests")]

И сборка «YourAssemblyName.Tests» будет видеть то, что помечено ключевым словом internal в «YourAssemblyName». Строчку эту можно вписать, чуть что, в AssemblyInfo.cs, который VS создаёт специально для хранения таких атрибутов.


Возвращение оскорбительно базовой справки
В .NET кроме уже встроенных атрибутов или ключевых слов вроде abstract, public, internal, static можно создавать свои. И вешать их на всё, что угодно: поля, свойства, классы, методы, события, и целые сборки. В C# вы для этого просто пишете имя атрибута в квадратных скобках перед тем, на что навешиваете. Исключение — сама сборка, так как в коде нет нигде прямого указания, что «Assembly begins here». Там ещё перед названием атрибута надо дописать assembly:

Таким образом, волки остаются сыты, овцы целы, всё, что можно, по-прежнему прячется внутри сборки, юнит тесты живут в отдельной сборке, как и положено, а фича, про которую я едва вспомнил, обретает повод её использовать. Возможно, единственный существующий повод.


Чуть не забыл один важный момент. Действия атрибута InternalsVisibleTo одностороннее.


protected < internal?


Итак, ситуация: A и B сидели на трубе.


using System;

namespace Pipe
{
    public class A
    {
        public String SomeProperty { get; protected set; }
    }

    internal class B
    {
        //ERROR!!! The accessibility modifier of the 'B.OtherProperty.set' accessor must be more 
        //restrictive than the property or indexer 'B.OtherProperty'
        internal String OtherProperty { get; protected set; }
    }
}

A было уничтожено в процессе код-ревью, так как не используется за пределами сборки, но позволяет себе зачем-то иметь модификатор доступа public, B вызвало ошибку компиляции, которая в первые минуты может ввести в ступор.


В принципе, сообщение об ошибке логично. Аксессор свойства не может раскрывать больше, чем само свойство. Любой отнесётся с пониманием, если компилятор даст по шапке за такое:


internal String OtherProperty { get; public set; }

Но вот претензии к этой строке сходу ломают мозг:


internal String OtherProperty { get; protected set; }

Замечу, что к этой строке претензий не будет:


internal String OtherProperty { get; private set; }

Если особо не задумываться, то в голове выстраивается следующая иерархия:


public > internal > protected > private

И эта иерархия вроде как даже работает. Кроме одного места. Там, где internal > protected. Чтобы понять суть претензий компилятора, давайте вспомним, какие ограничения накладывают internal и protected. internal — только внутри сборки. protected — только наследники. Заметьте, любые наследники. А если класс B пометить как public, то в другой сборке можно определить его наследников. И тогда акссесор set действительно получит доступ туда, куда его не имеет всё свойство. Так как компилятор C# параноидален, он даже возможности такой допустить не может.


Спасибо ему за это, но нам нужно дать наследникам доступ к аксессору. И специально для таких случаев есть модификатор доступа protected internal.


Эта справка уже не столь оскорбительна
  • protected internal — модификатор доступа, который обозначает, что им помеченное доступно внутри сборки или наследникам класса, в котором расположено помеченное.


Так что если мы хотим, чтобы компилятор позволил нам пользоваться этим свойством и задавать его в наследниках, нужно сделать так:


using System;

namespace Pipe
{
    internal class B
    {
        protected internal String OtherProperty { get; protected set; }
    }
}

А правильная иерархия модификаторов доступа выглядит приблизительно так:


public > protected internal > internal/protected > private

Интерфейсы


Итак, ситуация: A, I, B сидели на трубе.


namespace Pipe
{
    internal interface I
    {
        void SomeMethod();
    }

    internal class A : I
    {
        internal void SomeMethod()
        {
            //'A' does not implement interface member 'I.SomeMethod()'. 
            //'A.SomeMethod()' cannot implement an interface member because it is not public.
        }
    }

    internal class B : I
    {
        internal void SomeMethod()
        {
            //'B' does not implement interface member 'I.SomeMethod()'. 
            //'B.SomeMethod()' cannot implement an interface member because it is not public.
        }
    }
}

Сидели ровно и за пределы сборки не совались. Но были забракованы компилятором. Тут суть претензий ясна из сообщения об ошибке. Реализация интерфейса должна быть открытой. Даже если сам интерфейс закрыт. Было бы логично привязать доступ реализации интерфейса к его доступности, но чего нет, того нет. Реализация интерфейса должна быть public.


И у нас здесь два выхода. Первый: сквозь скрип и зубовный скрежет повесить на реализацию интерфейса модификатор доступа public. Второй: явная реализация интерфейса. Выглядит это так:


namespace Pipe
{
    internal interface I
    {
        void SomeMethod();
    }

    internal class A : I
    {
        public void SomeMethod()
        {
        }
    }

    internal class B : I
    {
        void I.SomeMethod()
        {
        }
    }
}

Обратите внимание, что во втором случае нет модификатора доступа. Кому в таком случае доступна реализация метода? Скажем так, никому. Проще показать на примере:


B b = new B();

//'B' does not contain a definition for 'SomeMethod' and no accessible extension method //'SomeMethod' accepting a first argument of type 'B' could be found 
//(are you missing a using directive or an assembly reference?)
b.SomeMethod();

//OK
(b as I).SomeMethod();

Явная реализация интерфейса I означает, что пока мы явно не приведём переменную к типу I, методов реализующий этот интерфейс не существует. Каждый раз писать (b as I).SomeMethod() может быть излишней нагрузкой. Как и ((I)b).SomeMethod(). И я нашёл два способа это дело обойти. До одного додумался сам, а второй честно нагуглил.


Способ первый — фабрика:


    internal class Factory
    {
        internal I Create()
        {
            return new B();
        }
    }

Ну, или любой другой паттерн, который позволит вам спрятать этот нюанс.


Способ второй — методы расширения:


    internal static class IExtensions
    {
        internal static void SomeMethod(this I i)
        {
            i.SomeMethod();
        }
    }

Что удивительно, это срабатывает. Эти строки перестают выдавать ошибку:


B b = new B();
b.SomeMethod();

Ведь обращение идёт, как нам подсказывает IntelliSense в Visual Studio, не к методам явной реализации интерфейса, а к методам расширения. А к ним обращаться никто не запрещает. И методы расширения интерфейса можно вызывать на всех его реализациях.


Но остаётся один нюанс. Внутри самого класса нужно обращаться к этому методу через ключевое слово this, иначе компилятор не поймёт, что мы хотим обратиться к методу расширения:


    internal class B : I
    {
        internal void OtherMethod()
        {
            //Error!!!
            SomeMethod();
            //OK
            this.SomeMethod();
        }

        void I.SomeMethod()
        {
        }
    }

И так, и так, у нас или public, там где его быть не должно, но там он, кажется, вреда не причиняет, или немножко лишнего кода на каждый internal-интерфейс. Выберите меньшее зло себе по вкусу.


Рефлексия


Я об это больно стукнулся, когда попытался найти через рефлексию конструктор, который, естественно, был помечен, как internal, у internal-класса. И оказалось, что рефлексия не выдаст ничего, что не было бы public. И это, в принципе, логично.


Во-первых, рефлексия, если я правильно помню то, что писали умные люди в умных книгах, это про поиск информации в метаданных сборки. Которые, по идее, не должны выдавать лишнего (я так думал, по крайней мере). Во-вторых, основное применение рефлексии — сделать вашу программу расширяемой. Вы предоставляете посторонним какой-то интерфейс (возможно, даже в виде интерфейсов, фить-ха!). А они его реализуют и предоставляют плагины, моды, расширения в виде загружаемой на ходу сборки, из которой рефлексия их и достаёт. И само собой, ваше API будет public. То есть, смотреть на internal через рефлексию невозможно технически и бессмысленно с практической точки зрения.


Update. Тут в комментариях выяснилось, что рефлексия позволяет, если её явно попросить об этом, отрефлексировать вообще всё. Будь оно хоть internal, хоть private. Если вы не пишете какой-то инструмент для анализа кода, старайтесь так не делать, пожалуйста. Текст дальше всё ещё актуален для случаев, когда мы ищем открытые типы членов. И вообще, не проходите мимо комментариев, там много чего интересного.


На этом можно было бы и закончить с рефлексией, но давайте вернёмся к предыдущему примеру, где A, I, B сидели на трубе:


namespace Pipe
{
    internal interface I
    {
        void SomeMethod();
    }

    internal static class IExtensions
    {
        internal static void SomeMethod(this I i)
        {
            i.SomeMethod();
        }
    }

    internal class A : I
    {
        public void SomeMethod()
        {
        }

        internal void OtherMethod()
        {
        }
    }

    internal class B : I
    {
        internal void OtherMethod()
        {
        }

        void I.SomeMethod()
        {
        }
    }
}

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


И тут открывает дверь и тихонько крадётся рефлексия:


using Pipe;
using System;
using System.Reflection;

namespace EncapsulationTest
{
    public class Program
    {
        public static void Main(string[] args)
        {
            FindThroughReflection(typeof(I), "SomeMethod");
            FindThroughReflection(typeof(IExtensions), "SomeMethod");
            FindThroughReflection(typeof(A), "SomeMethod");
            FindThroughReflection(typeof(A), "OtherMethod");
            FindThroughReflection(typeof(B), "SomeMethod");
            FindThroughReflection(typeof(B), "OtherMethod");
            Console.ReadLine();
        }

        private static void FindThroughReflection(Type type, String methodName)
        {
            MethodInfo methodInfo = type.GetMethod(methodName);
            if (methodInfo != null)
                Console.WriteLine($"In type {type.Name} we found {methodInfo}");
            else
                Console.WriteLine($"NULL! Can't find method {methodName} in type {type.Name}");
        }
    }
}

Изучите этот код, вбейте его в студию, если вам так хочется. Тут мы пытаемся с помощью рефлексии найти все методы из всех типов нашей трубы (namespace Pipe). И вот какие результаты нам это даёт:


In type I we found Void SomeMethod()
NULL! Can't find method SomeMethod in type IExtensions
In type A we found Void SomeMethod()
NULL! Can't find method OtherMethod in type A
NULL! Can't find method SomeMethod in type B
NULL! Can't find method OtherMethod in type B

Сразу скажу, что используя объект типа MethodInfo, найденный метод можно вызвать. То есть, если рефлексия что-то нашла, то нарушить инкапсуляцию чисто теоретически можно. И у нас кое-что найдено. Во-первых, тот самый public void SomeMethod() из класса A. Это было ожидаемо, что тут ещё сказать. У этой поблажки всё-таки могут быть последствия. Во-вторых, void SomeMethod() из интерфейса I. Это уже интереснее. Как бы мы не запирались, но абстрактные методы, размещённые в интерфейсе (или что на самом деле там размещает CLR) на самом деле являются открытыми. Отсюда вывод, вынесенный в отдельный абзац:


Смотрите внимательно кому и какие объекты типа System.Type вы отдаёте.


Но тут ещё один нюанс с этими двумя найденными методами, который я хотел бы рассмотреть. Методы internal-интерфейсов и открытые методы internal-классов можно найти с помощью рефлексии. Как человек разумный, я сделаю вывод, что они попадают в метаданные. Как человек опытный, я этот вывод проверю. И в этом нам поможет ILDasm.


Глянуть одним глазком в кроличью нору метаданных нашей трубы

Сборка была собрана в режипрме Release


TypeDef #2 (02000003)
-------------------------------------------------------
TypDefName: Pipe.I (02000003)
Flags : [NotPublic] [AutoLayout] [Interface] [Abstract] [AnsiClass] (000000a0)
Extends : 01000000 [TypeRef]
Method #1 (06000004)
-------------------------------------------------------
MethodName: SomeMethod (06000004)
Flags : [Public] [Virtual] [HideBySig] [NewSlot] [Abstract] (000005c6)
RVA : 0x00000000
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

TypeDef #3 (02000004)
-------------------------------------------------------
TypDefName: Pipe.IExtensions (02000004)
Flags : [NotPublic] [AutoLayout] [Class] [Abstract] [Sealed] [AnsiClass] [BeforeFieldInit] (00100180)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000005)
-------------------------------------------------------
MethodName: SomeMethod (06000005)
Flags : [Assem] [Static] [HideBySig] [ReuseSlot] (00000093)
RVA : 0x00002134
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
1 Arguments
Argument #1: Class Pipe.I
1 Parameters
(1) ParamToken : (08000004) Name : i flags: [none] (00000000)
CustomAttribute #1 (0c000011)
-------------------------------------------------------
CustomAttribute Type: 0a000001
CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
Length: 4
Value : 01 00 00 00 > <
ctor args: ()

CustomAttribute #1 (0c000010)
-------------------------------------------------------
CustomAttribute Type: 0a000001
CustomAttributeName: System.Runtime.CompilerServices.ExtensionAttribute :: instance void .ctor()
Length: 4
Value : 01 00 00 00 > <
ctor args: ()

TypeDef #4 (02000005)
-------------------------------------------------------
TypDefName: Pipe.A (02000005)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000006)
-------------------------------------------------------
MethodName: SomeMethod (06000006)
Flags : [Public] [Final] [Virtual] [HideBySig] [NewSlot] (000001e6)
RVA : 0x0000213c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #2 (06000007)
-------------------------------------------------------
MethodName: OtherMethod (06000007)
Flags : [Assem] [HideBySig] [ReuseSlot] (00000083)
RVA : 0x0000213e
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #3 (06000008)
-------------------------------------------------------
MethodName: .ctor (06000008)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
RVA : 0x00002140
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

InterfaceImpl #1 (09000001)
-------------------------------------------------------
Class : Pipe.A
Token : 02000003 [TypeDef] Pipe.I

TypeDef #5 (02000006)
-------------------------------------------------------
TypDefName: Pipe.B (02000006)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 01000011 [TypeRef] System.Object
Method #1 (06000009)
-------------------------------------------------------
MethodName: OtherMethod (06000009)
Flags : [Assem] [HideBySig] [ReuseSlot] (00000083)
RVA : 0x00002148
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #2 (0600000a)
-------------------------------------------------------
MethodName: Pipe.I.SomeMethod (0600000A)
Flags : [Private] [Final] [Virtual] [HideBySig] [NewSlot] (000001e1)
RVA : 0x0000214a
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

Method #3 (0600000b)
-------------------------------------------------------
MethodName: .ctor (0600000B)
Flags : [Public] [HideBySig] [ReuseSlot] [SpecialName] [RTSpecialName] [.ctor] (00001886)
RVA : 0x0000214c
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: Void
No arguments.

MethodImpl #1 (00000001)
-------------------------------------------------------
Method Body Token : 0x0600000a
Method Declaration Token : 0x06000004

InterfaceImpl #1 (09000002)
-------------------------------------------------------
Class : Pipe.B
Token : 02000003 [TypeDef] Pipe.I


Беглый осмотр показывает, что в метаданные попадает всё, как бы оно ни было помечено. Рефлексия ещё заботливо от нас прячет то, что посторонним видеть не положено. Так что вполне может быть, что лишние пять строк кода на каждый метод internal-интерфейса не такое уж и большое зло. Тем не менее, главный вывод остаётся прежним:


Смотрите внимательно кому и какие объекты типа System.Type вы отдаёте.


Но это уже, конечно, следующий уровень, после воцарения ключевого слова internal во всех местах, где нет необходимости в public.


P.S.


Знаете, что самое классное в использовании ключевого слова internal везде внутри сборки? Когда она разрастётся, вам придётся её поделить на две и больше. А в процессе вам придётся взять паузу на то, чтобы сделать некоторые типы открытыми. И вам придётся задуматься о том, какие именно типы достойны того, чтобы стать открытыми. Хотя бы мельком.


Это означает следующее: эта практика написания кода заставит вас ещё раз задуматься о том, какую форму примет архитектурная граница между новорожденными сборками. Что может прекраснее?


P.P.S.


Начиная с версии C# 7.2 появился новый модификатор доступа private protected. И я пока понятия не имею, что это такое, и с чем его едят. Так как не сталкивался на практике. Но буду рад узнать в комментариях. Но не копипаст из документации, а реальные случаи, когда этот модификатор доступа может понадобится.

Комментарии (22)


  1. unnutz
    28.03.2019 18:09

    Модификатор доступа private protected разрешает доступ для наследников исключительно внутри сборки.

    Документация.

    Ну а зачем может понадобиться? К примеру, один класс в нашей сборке реализует некий интерфейс. Другой класс наследует его, предоставляя чуть больше возможностей. Мы не хотим чтобы кто-то, кто будет использовать нашу сборку, наследовал один из наших классов и получил доступ к полю или методу, которое/ая используется для внутренних нужд, например, хранит некое состояние, изменив которое, дальнейшее поведение библиотеки может стать непредсказуемым.


  1. BratSin
    28.03.2019 19:15

    Спасибо, было интересно. Но у меня для вас плохая новость :)

    Assembly 1
    using System;
    
    namespace TestLibrary
    {
        internal class TestReflection
        {
            internal void SomeMethod()
            {
                // Do something
            }
        }
    }


    1. ColdPhoenix
      28.03.2019 19:40
      +1

      Таким же темпом и private вызываются.


    1. chelovekbeznika Автор
      28.03.2019 22:10
      +2

      Ну, тут три варианта:
      1) Вы открытым текстом просите положить болт на инкапсуляцию, потому что вы знаете, что вы делаете.
      2) Вы открытым текстом просите положить болт на инкапсуляцию, потому что вы думаете, что вы знаете, что вы делаете.
      3) Вы пишите какой-то инструмент анализа кода, которому действительно нужно залезть в потроха ваших сборок. Ещё один случай использования рефлексии, о котором я почему-то не подумал.
      Из этих трёх проблему представляет только второй вариант. И это является проблемой даже без рефлексии.


      1. BkmzSpb
        28.03.2019 23:10

        Вот, вспомнил и нашел — есть способ попросить не трогать приватные мемберы (msdn):
        [assembly: DisablePrivateReflection]
        Гуглопоиск быстро привел вот сюда.


        Не очень понятно, как себя поведут internal объекты.


        1. chelovekbeznika Автор
          28.03.2019 23:19

          Я знал, что комментарии к этой статье будут полезнее самой статьи.
          Что до internal, то я вижу дело так: рефлексия копает метаданные -> в метаданных из модификаторов доступа флаги Public/Non-public -> internal помечается как non-public. Отсюда вывод: надо взять Visual Studio и проверить. Но сто рублей на то, что от копания в internal это тоже защитит, я бы рискнул поставить.


          1. BkmzSpb
            29.03.2019 14:25

            В дополнение к предыдущему сообщению, только что наткнулся на вот такой вот атрибут (msdn)
            [assembly: SuppressIldasm]
            Не очень понятно, что оно делает, но вроде запрещает ildasm дизассемблировать сборку, но не ограничивает использование reflection.
            Есть подозрение, что это просто контракт — сборка принципиально не меняется, просто ildasm говорит "извините, но нет".


            Это слабо относится к теме инкапсуляции как таковой, но может, наверное, работать в сочетании с [DisablePrivateReflection] и [InternalsVisibleTo].


            1. chelovekbeznika Автор
              29.03.2019 14:29

              Это уже между уровнями «защищаемся от дураков чуть тщательнее» и «включаем полную паранойю вплоть до обфускации кода». Вопрос в том, уважают ли другие декомпиляторы этот атрибут.


      1. BratSin
        28.03.2019 23:16

        Не очень понял про забить болт на инкапсуляцию. Я всего лишь о том, что internal метод в internal классе имеет такую же область видимости, как public метод в internal классе. И как защита от рефлексии, это использовать бесполезно. То есть, немного докрутив метод FindThroughReflection() из статьи, можно вполне спокойно находить OtherMethod() класса B, будь он хоть internal, хоть public, хоть private.
        Не исключаю, что я вас не правильно понял. Тогда, пожалуйста, прокомментируйте вот этот абзац:

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

        И тут открывает дверь и тихонько крадётся рефлексия:
        На всякий случай, чтобы исключить возможное недопонимание, я не против модификатора internal. Сам его стараюсь применять, по тем же причинам, которые вы указали в начале статьи. Трюк с extension'ом — очень интересно, спасибо. Я имел в виду, что делать метод internal, когда класс и так уже internal, считаю лишним. И не понимаю, в чем не прав автор класса A.


        1. chelovekbeznika Автор
          28.03.2019 23:24

          С учётом того, что мы уже в комментариях накопали, не так уж и сильно он не прав, соглашусь. Но ещё пару моментов я по этому поводу вижу:
          1) Это собьёт с толку того, кто читает код и вызовет ненужные вопросы.
          2) Если модификатор доступа у класса изменится на public, то модификаторам метода лучше оставаться по умолчанию internal, пока программист не решит, что метод тоже можно открыть внешнему миру и явно не выскажет это пожелание, внеся изменение internal на public у метода в ближайшем коммите…

          А явно указывать методу GetMethods и прочим, чтобы они гребли и не открытые члены тоже без острой на той надобности, не очень хорошо. Там уже в комментариях написали, что если вы так делаете, и у вас потом что-то сломалось, что ССЗБ.


          1. BratSin
            28.03.2019 23:59

            Пункт 2, интересная мысль. Спасибо, пошел думать.


  1. pankraty
    28.03.2019 21:52
    +2

    Бесполезно с помощью модификаторов доступа пытаться защититься от того, что ваш класс "отрефлексируют". В метаданных сборки представлены все члены, даже самые приватные, иначе CLR не сможет с ними работать.


    Модификаторы доступа — это не более чем контракт. Если вы объявили член публичным, то потребители вашей библиотеки вправе рассчитывать, что этот член не будет слишком уж часто меняться. А вот если они через рефлексию доставали internal классы, и после обновления библиотеки все перестало работать — так они ССЗБ.


    Еще, модификатор класса "перекрывает" модификаторы его членов. Например, если класс (вложенный) объявлен как private, у него вполне легально может быть public конструктор, но использовать его можно будет только в пределах внешнего класса (опять же, не беря в расчет рефлексию).


  1. spyphy
    28.03.2019 22:23

    Мда, понаплодили в шарпе этих модификаторов доступа. Вот в Питоне их вообще нет, и чё, всё работает)


  1. a-tk
    28.03.2019 22:30
    +1

    А рефлексию-то зачем в статью приплели? Чтобы продемонстрировать, что можно наплевать на всё и пострелять из дробовика себе по ногам?
    Другие специикаторы доступа, даже тот же private, не защитят от рефлексии.
    Подскажу ещё один метод пострелять по ногам: перезапись уже скомпилированной сборки средствами Cecil-а или Postsharp-а. Тоже можно много чего интересного сделать.


    1. chelovekbeznika Автор
      28.03.2019 22:33

      Наткнулся, заинтересовался, начал копать и тут понеслось… Возможно, часть статьи, связанную с рефлексией и в самом деле стоило подсократить. Или наоборот, покопать поглубже и поосновательнее.


  1. EngineerSpock
    29.03.2019 08:03
    +1

    А причём тут инкапсуляция? Вся статья о сокрытии информации. Инкапсуляция это защита инвариантов и сокрытие информации лишь её часть. Учите матчасть.


  1. AndrewN
    29.03.2019 09:14

    Так и не понял, что плохого в

        internal class A : I
        {
            public void SomeMethod()
            {

    Если класс недоступен извне, то и методы его недоступны. Какая-то надуманная проблема…
    Замечание про рефлексию неуместно, на то она и рефлексия, она может и приватные методы найти (ужас-ужас)


    1. chelovekbeznika Автор
      29.03.2019 12:18

      Ответил выше BratSin'у. На самом деле, не так уж и страшно, если честно, но парочка аргументов в пользу более строгого подхода всё же нашлось.


    1. Feihoa
      29.03.2019 13:55

      Если класс вдруг станет public (что не есть гуд, разумеется), метод может остаться internal. Впрочем, проблема надуманная


  1. PsyHaSTe
    29.03.2019 17:33
    +4

    Если честно, у меня такие проблемы тоже были первый год или два работы. А потом я понял, что методы просто должны быть публичными. Модификатор internal вообще перестал использовать. Нужно в приватном методе сделать хитрую операцию над входной строкой? Нет, мы не тестируем приватный метод, мы выносим хелпер-метод StringHelper.MagicalStringTrim и тестируем его. Хотим потестировать еще что-то? Тоже берем и выносим.


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


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




    Я для себя выработал простое правило, если при работе с языком хочется странного, то стоит пересмотреть архитектуру и свой подход к решению проблемы. Например явная реализация нужна не для того, чтобы "спрятать" метод, а чтобы один класс мог реализовать методы разных интерфейсов, которые по стечению обстоятельств имеют одно имя и одну сигнатуру.


  1. asedovski
    29.03.2019 18:04

    То есть, смотреть на internal через рефлексию невозможно технически

    сюрприз-сюрприз :)

    или я не понял сказанного автором, или автор не понял прочитанного в умных книжках…


    1. chelovekbeznika Автор
      29.03.2019 18:43

      Доползу домой, внесу это уточнение в статью. На которое, кстати, уже обратили внимание выше. Но да, в том же Троелсене эта возможность напрямую не упоминается, так как кому в здравом уме может такое прийти в голову (в стандартной ситуации, инструменты анализа кода — отдельная песня). Хотя тот факт, что в метаданных хранятся не только открытые типы и их члены, там отражён, хоть и мельком. Надо будет ещё перечитать Рихтера.

      UPD. Добавил уточняющий абзац в статью.