Pull to refresh

Comments 26

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

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

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

namespace TestLibrary
{
    internal class TestReflection
    {
        internal void SomeMethod()
        {
            // Do something
        }
    }
}

Assembly 2
using System;
using System.Reflection;

namespace TestInheritance
{
    class Program
    {
        static void Main(string[] args)
        {
            Assembly assembly = Assembly.LoadFrom("TestLibrary.dll");

            Type type = assembly.GetType("TestLibrary.TestReflection");
            ConstructorInfo typeCtor = type.GetConstructor(new Type[] { });
            MethodInfo someMethod = type.GetMethod("SomeMethod",  BindingFlags.Instance | BindingFlags.NonPublic);
            
            object typeObject = typeCtor.Invoke(null);
            someMethod.Invoke(typeObject, null);
        }
    }
}

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

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

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

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


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

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

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


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

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

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

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

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


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


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

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

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

Так и не понял, что плохого в
    internal class A : I
    {
        public void SomeMethod()
        {

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

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

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

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


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


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




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

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

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

Ну например, если вы хотите сериализовать List<T>, то без рефлексии приватных членов не обойдется, потому что содержимое списка хранится в T[] m_items. И BinaryFormatter именно этим и занимается, обходит все поля (приватные-публичные, пофиг), и рекурсивно их сериализует.

А зачем юнит-тестам иметь доступ к каким-то внутренностям класса, к которым не имеют доступ все остальные? Как мне кажется, юнит-тесты должны тестировать контракт этого класса с внешним миром и если этот контракт выполняется, то какая разница как именно это внутри этого класса реализовано? И если вы изменили какое-то внутреннее поведение класса, но контракт остался прежним, то ничего в юнит-тестах менять не надо, ведь поведение класса с точки зрения внешнего наблюдателя никак не изменилось. Но в вашем случае, придется переписывать юнит-тесты, что, на мой взгляд, не совсем корректно.
Возникает вопрос: что должны проверять юнит-тесты — что класс выполняет задачу правильно и sum(2,2) возвращает 4 или что задача А решена конкретно способом Б, а не В?
Если какая-то внутренняя логика требует отдельного тестирования, то может вынести ее (логику) в отдельный класс?
Есть сборка. Она большая. И имеет на это право, так как сборка — это единица развёртывания, а для логического группирования классов есть пространства имён. И у этой сборки маленький контракт с внешним миром сводящийся к нескольким простым действиям. У нас много-много классов internal, которые мы не хотим оставлять без юнит-тестов. Особенно, если мы угораем по TDD, где 100% покрытие кода, написание тестов до самого кода и прочий хардкор. Вот тут-то и встаёт дилемма, которую я описал в статье.

Если какая-то внутренняя логика требует отдельного тестирования, то может вынести ее (логику) в отдельный класс?

Да, так и нужно сделать. Но, скорее всего, этот класс будет internal, так как в контракт с внешним миром не входит. Речь не о том, чтобы тестировать потроха классов. Речь о том, чтобы тестировать классы и их, скажем так, не очень внутренние методы.

private protected нужен для построения закрытых иерархий, но большой ценности в нём нет.


Например, у вас есть класс Foo и наследники: Bar, Baz. Как сделать так, чтобы никто не смог добавить другого наследника? Можно сделать конструктор internal Foo(), тогда из другой сборки его никто не вызовет.


Если сделать конструктор private protected Foo(), его никто не сможет вызвать, кроме наследников в той же сборке. Если ваш Foo абстрактный, то разницы нет вообще.

Sign up to leave a comment.

Articles