Самая первая моя статья на Хабре «вышла комом». Спасибо всем кто оставил критические комментарии. Благодарю за науку! В том числе учтя вашу критику, написал вторую версию.
Убрал юмор - основная претензия по которой заминусовали.
На то была причина
Меня натуральным образом ограбили на 3 годовых зарплаты, была жуткая депрессия, в том числе поэтому как выход взялся на статью, чтобы отвлечься + получить положительные эмоции от общения с коллегами. А когда очень плохо, то начинаю сильно юморить — видимо такой мой защитный психологический механизм. И, да, как сейчас смотрю уже спокойным взглядом, этого юмора в статье было сильно излишне (+ еще были отсылки к старым афоризмам и мемам, которые, похоже, уже вышли из моды, но и они лишние). Извините, был не адекватен.
Главная же причина необходимости второй версии вовсе не в стилистических правках. А в том, что проект заметно продвинулся в технической части: новые серьезные фичи + небольшие коррекции старых.
Другая важная причина в том, что акценты были расставлены по другому, поэтому название было изменено на более адекватное, как более отражающее суть (старое скорее путало). И даже сделан ребрендинг названий проектов также на более отражающие суть.
И если бы не вторая причина, то здесь можно было бы просто рассказать о новых фичах. А так, второй виток/итерация (подобно тому как это принято в методологии разработки ПО), после этапа переосмысления первого, породили трансформацию, которую трудно выразить просто добавлением новых глав — проще представить новую версию со систематическим изложением материала.
Тот же текст (по большей части начальный), что «пережил» переход из первой версии, был существенно сокращен, а также отредактирован.
Добавлен раздел с ответами на задаваемые вопросы и много примеров (см. Приложение).
hand made!
Тут на хабре прочитал, что одним из верных маркеров ИИ статей (которые все разоблачают, ненавидят, минусуют) являются «ёлочные кавычки» и длинное тире. Беда! Как раз их и использую (на punto-switcher давно поставил замену: "! -> «» и -! -> — и др.). Ответственно заявляю: это «кожаная» статья! Выстраданная много-трудными человеко-часами.
Предисловие
Предполагается, что тема будет интересна тем кто любит четкие контракты в своих проектах, строгость и чистоту в инкапсуляции, новые подходы в ООП. А также тем, кто уважает функциональное программирование (ФП).
Будет параллель с friend-ми в C++, именно с нее и начну после предисловия. Однако, это лишь ассоциация-приквел к основной теме.
Как адепт ФП, всегда стараюсь выстроить так систему классов приложения, чтобы классы в идеальном случаем имели только readonly (get-only) публичные свойства.
Примечание
Для менее искушенного читателя лишь замечу — гляньте на тот же string C# класс. Как же проще понимать логику программы при работе со string экземплярами, когда знаешь, что их невозможно изменить.
А беззаботность в многопоточности? Недаром ФП ныне один из основных трендов в развитии языков.
А что делать? Кремневая микроэлектроника уже уперлась в физический предел и дальнейший прогресс идет по пути наращивания числа ядер, многопоточности. Благодаря же неизменяемости в ФП, хороший компилятор с функционального языка умеет сам автоматически распараллеливать, создавать на выходе многопоточное приложение из кода, где вручную этого даже не было предусмотрено.
Не раз в процессе разработке приложений на C# со сложной логикой, где по обыкновению следую ФП readonly принципу, сталкиваюсь с тем, что это, увы, не всегда удается. Конечно, ради принципа, можно усложнить архитектуру, потерять в производительности... Но хочется не впадать в крайности (типа «науки ради науки»), а найти компромисс. Нередко в качестве которого из C++ опыта «стучались» friend-ы.
Friend-ы, конечно, не входит в инструментарий академического ФП, но эта фича позволяет минимизировать «ущерб», когда пытаемся подражать стилю ФП в не ФП языках (точнее в нечистых ФП языках, ибо функциональные фичи сейчас есть везде). Пусть свойство все же изменяемое, но мы локализуем случаи, когда такие его изменения допустимы, не допуская до него вообще всех.
Помимо ФП тут скорее даже уместнее еще говорить в терминах инкапсуляции, открытости/закрытости... Но именно ФП + «friend подача» от плюсов стало побудительным мотивом, вдохновило на сей труд. Как результат которого предлагается elvis-модификатор доступа.
Зачем нужен этот модификатор?
Да, собственно, за тем же самым что и другие модификаторы.
Ведь так же можно спросить: зачем нужны private, protected… когда (гипотетически) пусть будет только и только public?
Ниже есть специальный раздел (см. Приложение), где более подробно обсуждается применения модификатора с массой примеров. Там также см., имхо, интересный «Общий вывод из примера».
Но сначала, «для разогрева», рассмотрим еще один способ, который исторически первым пришел в голову. Пусть он не совсем «то», более ограниченный и менее лаконичный, но он не требует подключения к проекту специального анализатора или расширения. Возможно, кому-то будет тоже интересен.
Через интерфейс
Как это принято, когда требуется продемонстрировать к-л концепцию, берется простейший пример-задача. Пусть можно предлагать альтернативные решения этой задачи, не важно, тут целью будет именно демонстрация на простейшем «hello world» примере.
И этот пример будет сквозным образом проходить через всю статью (но в «Приложении» см. раздел с большим числом других примеров).
Пусть есть условный «я» (class Me) и у меня есть друг (class MyFriend).
И пусть, с одной стороны, у меня есть 100 рублей (св-во Money).
А с другой, пусть следуя принципу, что у друзей все должно быть «налапопам», готов разделить с ним эти рубли:
class Me { public decimal Money { get; set; } = 100; } class MyFriend { public void AcceptMoney(in Me me) { decimal half = me.Money / 2; me.Money = half; Money += half; } public decimal Money { get; private set; } = -40; }
Однако, очевидно, в такой реализации мои рубли открыты для всех, а не только для друга.
В плюсах тут можно было бы объявить класс MyFriend как friend для класса Me, где Me.Money было бы свойством открытым только для чтения, а вот к закрытому полю под этим свойством класс MyFriend как раз и имел бы доступ.
Но вот как можно решить через интерфейс:
(чисто для сравнения, добавим еще свойство UnsharedMoney)
///////// Реализиция через интерфейс: class Me { public interface IFriend { // Setter static protected void setMoney(in Me self, in decimal value) => self.Money = value; } public decimal Money { get; private set; } = 100; public decimal UnsharedMoney { get; private set; } = 100_000; } class MyFriend : Me.IFriend { public void AcceptMoney(in Me me) { decimal half = me.Money / 2; Me.IFriend.setMoney(me, half); Money += half; //me.UnsharedMoney = me.Money = 0; // obviously 2 compile errors } public decimal Money { get; private set; } = -40; } ///////// Тестируем: static class Test { public static void Run() { var me = new Me(); var myPoorFriend = new MyFriend(); log(me, myPoorFriend); myPoorFriend.AcceptMoney(me); log(me, myPoorFriend); } static void log(in Me me, MyFriend friend) => Console.WriteLine($"me: {me.Money}; friend: {friend.Money}"); }
выполнив тест, ожидаемо получим:
me: 100; friend: -40 me: 50; friend: 10
Очевидно, здесь доступ будет разрешен только и только для друзей (тех, кто является наследником от Me.IFriend интерфейса).
Причем доступ к Me.Money для друга открыт, а к Me.UnsharedMoney закрыт.
А посмотрев на IFriend в IDE, например, в Visual Studio:

Сразу видим кто и где допущен.
Более того, ведь не обязательно тут должен быть именно сеттер (setMoney), это избыточно «щедро» и опасно.
Чтобы не вводить друга в искушение, можно так сделать:
class Me { public interface IFriend { static protected decimal TakeMyHalfMoney(Me self) { decimal half = self.Money / 2; self.Money -= half; return half; } } public decimal Money { get; private set; } = 100; public decimal UnsharedMoney { get; private set; } = 100_000; } class MyFriend : Me.IFriend { public void AcceptMoney(in Me me) => Money += Me.IFriend.TakeMyHalfMoney(me); public decimal Money { get; private set; } = -40; }
Здесь уже дали более ограниченный доступ, разрешив другу делать только и только то, что хотели, иначе в варианте с setMoney(..) он теоретически мог загнать меня в минус, например, вызвав setMoney(me, -1000).
Анализ, сравнение с C++, плюсы и минусы
• Большой плюс:
IFriend дает доступ не ко всем закрытым членам, а позволяет тонкую настройку, где можно обернуть это логикой, защитить от опасных операций (как для случая доступа к непосредственному сеттеру, когда можно загнать в минус). В плюсах же для friend-а открылся бы доступ и к неприкосновенному UnsharedMoney (к приватному полю под этим свойством).
• Минус:
В C++ весь контроль над друзьями находится на стороне класса (в отличии от наследования MyFriend : Me.IFriend), что по идее более правильно.
Хотя, с другой стороны, интерфейс может иногда быть и плюсиком, если класс находится в другой библиотеке, а мы хотим воспользоваться его дружелюбными фичами, которые он предоставляет.
И вам не докучают с просьбами добавить в друзья (или лезут в код, редактируя ваш C++ класс) — один раз написали C# класс, определив границы его дружелюбия, и отдыхаете.
Есть и определенный контроль. IDE в помощь — она всегда покажет разработчику Me.IFriend интерфейса кто подключился в друзья (см. последний скриншот).
• Большой минус:
В отличии от плюсов, не можем сделать другом только для конкретного метода другого класса.
В общем несколько разные подходы с определенным балансом своих плюсов и минусов.
Elvis модификатор доступа
Для того чтобы сделать доступ только для конкретного метода, а также контроль друзей на стороне класса, можно воспользоваться такими Roslyn технологиями как Analyzers или/и Incremental Source Generators.
Более того, далее предлагается отбросить концепцию friend-ов как начально-ассоциативную «ступень» и перейти к концепции модификатора.
Хотя сам термин «друг» как устоявшийся и ясный оставим в нашем арсенале (см. далее).
Ссылка на проект: github link
Описывать внутреннюю реализацию этого Roslyn проекта здесь нет смысла — обычный проект такого рода. Речь в статье совсем не о техниках Roslyn.
А о том какой инструментарий предоставляется и как его использовать. Каков предлагаемый функционал, а не второстепенное дело деталей его реализации (по ним см. код на гитхабе).
Вот на этом далее и сосредоточимся.
Элвис атрибут.
Это наш «главный герой»: [OnlyYou] атрибут (можно было назвать и OnlyFor, но просто в честь известной песни ▶ а-ля Элвиса, см. историческую справку), вот его полная реализация:
[AttributeUsage( AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, | AttributeTargets.Field | AttributeTargets.Constructor | AttributeTargets.Event , AllowMultiple = true)] class OnlyYouAttribute : Attribute { public OnlyYouAttribute(Type type, params string[] members) { } }
Как видим, атрибут можно применять для любого метода/индексера, свойства/поля, конструктора, события (в общем мембера), класса или интерфейса, статического или не статического.
Для C# версии 11 и выше (как напомнил @a-tk), можно использовать более лаконичный вариант — generic атрибут. В вашем проекте в зависимости от версии C# можете использовать либо вышеприведенный атрибут, либо его generic вариант:
[AttributeUsage(...)] class OnlyYouAttribute<T> : Attribute { public OnlyYouAttribute(params string[] members) { } }
Далее для лаконичности будем использовать generic вариант.
Теперь можем следующим образом избавится от того недостатка, когда не было возможности сделать другом только и только конкретный метод другого класса:
class Me { public protected interface IFriend { [OnlyYou<MyFriend>(nameof(MyFriend.AcceptMoney))] static decimal TakeMyHalfMoney(Me self) {...} } ... } class MyFriend : Me.IFriend { public void AcceptMoney(in Me me) => Money += Me.IFriend.TakeMyHalfMoney(me); // ok public void CantAcceptMoney(in Me me) => Money += Me.IFriend.TakeMyHalfMoney(me); // err ... }
Здесь комментарии err и ok показывают, где будет ошибка компиляции, а где она пройдет успешно.
Только этот пример избыто усложненный — теперь интерфейс по сути не нужен (см. следующий пример).
Analyzer проверяет вызовы (использование) мемберов, которые отмечены данными атрибутами. И только в тех местах вызовы этих мемберов будут разрешены, которые описаны в параметрах [OnlyYou] атрибутов. Для всех же остальных вызовов будет ошибка компиляции.
Для одного мембера, как видно из определения атрибута, можно задать несколько таких атрибутов.
Вот более чистый (без Me.IFriend интерфейса) пример использования этого атрибута:
class Me { public decimal Money { get; private set; } = 100; [OnlyYou<MyFriend>(nameof(MyFriend.AcceptMoney))] public decimal TakeMyHalfMoney() { decimal half = Money / 2; Money -= half; return half; } } class MyFriend { public void AcceptMoney(in Me me) => Money += me.TakeMyHalfMoney(); // ok public void CantAcceptMoney(in Me me) => Money += me.TakeMyHalfMoney(); // err public decimal Money { get; private set; } } class NotMyFriend { public void AcceptMoney(in Me me) => Money += me.TakeMyHalfMoney(); // err public decimal Money { get; private set; } }
В Visual Studio это так выглядит:

Аналогично для свойств:
class Me { [OnlyYou<MyFriend>(nameof(MyFriend.AccessMoney))] public decimal Money { get; set; } = 100; } class MyFriend { public void AccessMoney(in Me me) // all ok { var half = me.Money / 2; me.Money = half; me.Money += half; me.Money -= half; me.Money *= half; ++me.Money; --me.Money; me.Money++; me.Money--; } public void CantAccessMoney(in Me me) // all err { var half = me.Money / 2; // err me.Money = half; // err // ... err } }
И картинка из Visual Studio:

Коль какое-то публичное свойство отмечено элвис-атрибутом, то также как и с методами, пользоваться им могут только друзья.
Более того, именно для свойств определен еще[OnlyYouSet] атрибут (также в 2-х вариантах — для C#10 и C#11+, приводим последний):
[AttributeUsage( AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Property | AttributeTargets.Field , AllowMultiple = true)] public class OnlyYouSetAttribute<T> : Attribute { public OnlyYouSetAttribute(params string[] members) { } }
Как можно догадаться из названия он относится к set; части свойства и только друзьям дает доступ к ней. Изменять свойство (если у свойства предусмотрен set; и он публичный) могут только друзья. Про get; же ничего не сказано, поэтому следуя принципу «что не запрещено, то разрешено» читать публичное свойство разрешено ВСЕМ.
Итак, превращаем свойство в get-only для всех кроме избранных, только им разрешаем изменять его:
class Me { [OnlyYouSet<MyFriend>(nameof(MyFriend.SetMoney))] public decimal Money { get; set; } = 100; } class MyFriend { public void SetMoney(in Me me) // all ok { var half = me.Money / 2; // ok me.Money = half; // ok me.Money += half; // ok ... // all ok } public void CantSetMoney(in Me me) { var half = me.Money / 2; // ok me.Money = half; // err ... // all err } } class NotMyFriend { public void CantSetMoney(in Me me) { var half = me.Money / 2; // ok me.Money = half; // err ... // all err } }
Все то же самое справедливо и полей.
Можно аналогично определить и [OnlyYouGet] — ограничиваем только по get;, а set; разрешаем всем. Однако, на практике (по крайней мере моей) необходимость в only-set свойствах встречаются исключительно редко, поэтому такой атрибут пока не был введен (но сообщите, пожалуйста, если думаете, что он нужен).
Generic случаи
Говоря о [OnlyYou*<MyFriend>] и [OnlyYou*(typeof(MyFriend))], стоит заметить, что имеются случаи, когда даже в C# 11+ применим только вариант с typeof — это случаи, когда MyFriend является generic классом:
class Me<T> { T Value { get; set; } //[OnlyYou<MyFriend<>>] // compile err, invalid syntax [OnlyYou(typeof(MyFriend<>))] // ok public T GetValue() => Value; } class MyFriend<T> { void Test(Me<T> me) { var v = me.GetValue(); // ok } } class NotMyFriend<T> { void Test(Me<T> me) { var v = me.GetValue(); // err } }
Более серьезный тест для generic есть в тестах проекта (на гитхабе).
А в Приложении есть практический пример, см. Шаблон «Снимок/Хранитель» (Memento) (на гитхабе тоже есть, как и все приведенные здесь примеры).
Поддержка generic тоже одна из новых фич.
Elvis-модификаторы доступа, определение и другая терминология
Семейство [Only*] атрибутов (уже приведенных и следующих далее) на классе и/или мемберах, как, думаю, уже все давно догадались и предлагается называть elvis-модификаторами доступа (Elvis access modifiers).
Так же предлагается ввести еще такую терминологию:
Про термин «друг» (класс
MyFriendв примерах) вопросов нет, с единственным уточнением, что в качестве друга может пониматься не только класс целиком, но и лишь его мембер.А вот того с кем хотят все дружить, кто всех привлекает (класс
Meв примерах), назову «аттрактором» (придумайте лучше? социофил? другофил(фу!)?).
Кардинальное архи-положительное отличие от плюсов, что здесь уже не позволяем залезать в приватную часть класса. То что приватно и должно оставаться приватным. Тем более что всякого приватного у класса может быть очень много, и выставлять все это хозяйство на показ (как в плюсах) тот еще «эксгибиционизм». Да еще и все это (надо - не надо) C++ друзья могут курочить как угодно, внося хаос, так что может так случится, что с такими «друзьями» и врагов не понадобится.
Немного о roslyn проектах
Написал как Analyzer (ElvisModifiersAnalyzer) так и Incremental Source Generators (ElvisModifiersGenerator). Это предварительные демонстрационные проекты.
Где ElvisModifiersGenerator сейчас сделан только для методов. Собственно и начал с генератора (плюс в том, что для генератора не нужно отдельного проекта с атрибутами, он сам их внедряет в компиляцию целевого проекта). Однако генератор не всегда ожидаемо работал со студией — при компиляции проекта, к которому подключен генератор, все ок, всегда выдает нужные ошибки. А вот при просмотре файлов, IntelliSense студии не всегда их подчеркивает красным, помогает перезапуск студии, но это не очень удобно.
Поэтому попробовал сделать Analyzer. И вот с ним уже все ок. Так что решил остановится на варианте с Analyzer и далее уже только его развивал. В статье описывается именно его функционал.
Для простейшего профайлинга сгенерировал 1000 cs-файлов с простыми классами друзей, добавил в проект и не заметил каких-либо тормозов при редактировании кода в Visual Studio (для определенности: Visual Studio 2022, 17.14.16).
Было беспокойство, что при редактировании кода будет пере-анализироваться весь проект. Ибо как в этом плане ведут себя аналайзеры, как-то не встречал чтобы про это писали в доках по ним. Добавил в аналайзер короткий (в 100ms) консольный бип, фактически трещетку. И убедился, что аналайзер действует очень экономно — анализирует только тот файл на который смотрим. И даже похоже учитывает прокрутку, т.е. только тот кусок что видим. При открытии файла выдает очень короткую дробь, а при первой (только первой) прокрутке можно услышать одиночные щелчки. При редактировании — та же очень короткая дробь, несмотря на > 1000 зависимых файлов.
Случай перегруженных методов
У [OnlyYou<T>(method1, prop1, field1,..)] аргументами идет список имен мемберов. Конкретно для методов же, как известно, возможна перегрузка. Однако в этом атрибуте такие перегруженные методы не различаются — атрибут принимает только имена. Так что если method1 перегружен, то все его варианты будут френдами.
Если все же хотим различать перегруженные методы, то нужно реализовать более тонкий подход — например, передавать не просто имена методов, а сигнатуры, например: "method1(string, int)".
Однако, такой подход со строкой сигнатуры не очень нравятся. Ибо здесь нужно и не ошибиться в ее составлении, и в процессе разработки следить за тем, чтобы в этой строке всегда была именно актуальная сигнатура, и следить за регистром букв...
Поэтому пошел другим путем. Были определены еще 2 атрибута:
[AttributeUsage( // the same as for OnlyYouAttribute AttributeTargets.Class | AttributeTargets.Interface | AttributeTargets.Method | AttributeTargets.Property, | AttributeTargets.Field | AttributeTargets.Constructor | AttributeTargets.Event , AllowMultiple = true)] class OnlyAliasAttribute<T> : Attribute { public OnlyAliasAttribute(params string[] aliases) { } } [AttributeUsage( AttributeTargets.Method | AttributeTargets.Constructor , AllowMultiple = true)] class AliasAttribute : Attribute { public AliasAttribute(string alias) { } }
Атрибут [OnlyAlias] играет ту же роль что и [OnlyYou], только вторым аргументом теперь передаем не имена методов, а их алиасы. Алиас же для метода можно назначить с помощью атрибута [Alias].
Пример:
class Me { public const string AcceptMul = nameof(AcceptMul); public const string CtorWithArg = nameof(CtorWithArg); public decimal Money { get; private set; } = 100; [OnlyAlias<MyFriend>(AcceptMul, CtorWithArg)] public decimal TakeMyHalfMoney() { decimal half = Money / 2; Money -= half; return half; } } class MyFriend { public void AcceptMoney(in Me me) => Money += me.TakeMyHalfMoney(); // err [Alias(Me.AcceptMul)] public void AcceptMoney(in Me me, int mul) => Money += mul * me.TakeMyHalfMoney(); // ok public decimal Money { get; private set; } = -40; [Alias(Me.CtorWithArg)] public MyFriend(Me me) { Money = me.TakeMyHalfMoney(); // ok } public MyFriend() { var me = new Me(); Money = me.TakeMyHalfMoney(); // err } }
Так что для случаев перегрузки методов, можно использовать этот инструментарий.
Рекомендую именно так определять алиасы — константами в классе аттрактора. Такое единственное место определения убережет от возможной ошибки в значениях алиаса в обоих атрибутах (в принципе можно даже добавить специальное правило в аналайзер, которое будет заставлять именно так делать).
Алиасами можно помечать и конструкторы, собственно это сейчас (пока) единственный вариант задавать правила для конструкторов, даже если они не перегружены.
В пару к [OnlyYouSet] аналогично определен [OnlyAliasSet] с точно таким же функционалом, только для случая алиасов.
Далее, чтобы не обговаривать везде что имеются ввиду как [OnlyYou/Set] так и [OnlyAlias/Set], будем говорить только об [OnlyYou/Set], понимая, что все то же самое будет справедливо и для [OnlyAlias/Set].
В качестве примеров рассматривали методы и свойства, но все то же самое справедливо и для полей, индексеров (this[index]), конструкторов (создавать объекты могут только те кому разрешено) и событий (подписываться могут только те кому разрешено).
В случае применения к классам/интерфейсам
Как видно из определения [OnlyYou] атрибута его можно применять и к классам/интерфейсам. С очевидным смыслом — разрешать доступ к мемберам аттрактора только избранным (далее еще немного «порастекаюсь по древу», а потом будет сразу общий пример кода).
[OnlyYou] на классе-аттракторе «бьет» [OnlyYou]на принадлежащих ему мемберах. Т.е. если у аттрактора запрещено все кроме некоторых привилегированных случаев, то на его мемберах уже никто не сможет пролезть, пытаясь определить на них атрибуты разрешающие доступ к уже запрещенным случаям на классе.
А вот добавить на мемберах дополнительные ограничения — без вопросов.
Как если у короля (класса-аттрактора) есть враги (недружественные типы), то если его подданные (его мемберы) вдруг попытаются с ними дружить, то это будет расценено как предательство, и СБ королевства (аналайзер) это будет пресекать.
NB. В принципе, можно даже добавить в аналайзер правило, согласно которому запрещено использовать [OnlyYou] на мемберах, если параметр типа применяемого атрибута не один из тех, что разрешены в атрибутах на классе аттракторе.
Пример:
[OnlyYou<MyFriend>] //[OnlyYou<MyFriend>(nameof(MyFriend.CanInvoke1))] // тоже вариант class Me { public decimal Money { get; set; } = 100; public void Method1() { } [OnlyYou<MyFriend>( // излишне nameof(MyFriend.CanInvoke1))] [OnlyYou<NotMyFriend>( // no effect nameof(NotMyFriend.Some1))] public void Method2() { } } class MyFriend { public void CanInvoke1(in Me me) => me.Method2(); // ok public void CanInvoke2(in Me me) => me.Method1(); // ok public void CanSet(in Me me) => me.Money = 200; // ok public void CantInvoke(in Me me) => me.Method2(); // err } class NotMyFriend { public void Some1(in Me me) => me.Method1(); // err public void Some2(in Me me) => me.Method2(); // err public void SomeSet(in Me me) => me.Money = 0; // err }
Хотя тут можно было реализовать и другой принцип, типа: «вассал моего вассала не мой вассал».
Комбинаторика нескольких атрибутов на одной «сущности»
Для примера рассмотрим такой случай:
Пусть для одного и того же MyFriend класса на один и тот же перегруженный AcceptMoney метод (для неперегруженного вопросов нет) из нашего примера выше, навесили сразу и [OnlyAlias] и [OnlyYou] атрибуты:
class Me { public const string AcceptMul = nameof(AcceptMul); public decimal Money { get; private set; } = 100; [OnlyYou<MyFriend> (AcceptMoney)] [OnlyAlias<MyFriend>(AcceptMul)] public decimal TakeMyHalfMoney() { decimal half = Money / 2; Money -= half; return half; } }
Тогда имеем дилемму:
[OnlyYou] разрешает оба варианта AcceptMoney,
[OnlyAlias] же, со своей стороны, разрешает только метод под AcceptMul алиасом.
Кто будет прав?
Сделал по принципу «или» — объединение множеств разрешенных методов первого и второго атрибутов, а не пересечение. Т.е. в нашем примере оба перегруженных метода будут разрешены.
Этот же «или» принцип работает вообще для всех всевозможных вариантах одновременного навешивания атрибутов. Иначе (если взять «и», т.е. пересечение) было бы странно, если, скажем, имелось бы 2 атрибута на разноименные методы (или методы в разных классах), ведь тогда пересечение этих условий доступа было бы пустым множеством.
В общем, почти такой же «или» принцип как это обычно делается для всяких опций (к примеру для регулярных выражений: RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Multiline | ...).
Поведение в иерархии наследования
Это поведение не вытекает каким-то естественным образом. В этом разделе собственно и описывается как это поведение было реализовано в аналайзере.
Прежде всего сделано, чтобы модификаторы на мемберах класса никак не ограничивали эти мемберы во внутренних вызовах:
class Me { [OnlyYou<MyFriend1>(..)] public decimal SomeProp { get; set; } = 100; [OnlyYou<MyFriend2>(..)] public void SomeMethod() { } void AnyMethod() { SomeMethod(); // ok SomeProp = 0; // ok } }
Далее сделано, чтобы также не было ограничений и во всех производных классах:
class Me // из предыдыщего примера { ... } class Me2 : Me { void AnyMethod2() { SomeMethod(); // ok SomeProp = 0; // ok } void AnyMethod3(Me me) { me.SomeMethod(); // ok me.SomeProp = 0; // ok } }
И вот с производными классами вопрос уже обсуждаемый. Правильно ли так? При строгом подходе, коль в базовом прописаны определенные разрешения и Me2 в них явно не входит, то может вместо ok нужно выдавать ошибки?
У меня нет обширной практики, которая бы подсказала как тут лучше (поделитесь своим мнением, если у кого тут есть соображения). Можно, конечно, добавить опциональный флаг в атрибуты (типа bool isStrict), или завести еще одно семейство атрибутов… А пока решение не принято (нужно ли тут вообще суетиться?), изменить поведение на строгое все же можно: для этого в коде класса аналайзера (файл ElvisModifiersAnalyzer.cs), в его начале раскомментировать:
//#define STRICT_OU
Следующий тип случаев с иерархией, реализован так:
class Me { [OnlyYou<IFriend>(..)] public void SomeMethod() { } } interface IFriend { void UseMe(Me me); } class Friend1 : IFriend { void UseMe(Me me) => me.SomeMethod(); // ok } class Friend2 : IFriend { void UseMe(Me me) => me.SomeMethod(); // ok } class Friend3 { void UseMe(Me me) => me.SomeMethod(); // err }
Т.е. [OnlyYou*] правила у мембера с указанием интерфейса (IFriend) распространяет эти правила на все реализации этого интерфейса.
И, наконец, еще один тип случаев:
interface IMe { //[OnlyYou<IFriend>] [OnlyYou<Friend>] void SomeMethod(); } class Me1 : IMe { public void SomeMethod() { } } class Me2 : IMe { public void SomeMethod() { } } //interface IFriend { } class Friend // : IFriend { void UseMe(Me1 me) => me.SomeMethod(); // ok void UseMe(Me2 me) => me.SomeMethod(); // ok } class NotFriend { void UseMe(Me1 me) => me.SomeMethod(); // err void UseMe(Me2 me) => me.SomeMethod(); // err }
Правила определенные на интерфейсе аттрактора (IMe), автоматически пролонгируются и для всех реализаций этого интерфейса.
Также ничто не мешает комбинировать последний и предпоследний типы случаев (с одновременным IFriend иIMe) — соответственно скомбинируется поведение.
По идее, то, как сделано для интерфейсов нужно бы сделать и для наследования от (базовых) классов. Однако, пока «not implemented», тем более что при необходимости всегда можно для базового класса создать интерфейс (авто-операция в IDE) и задать правила там.
Некоторые комментарии по аналайзеру и vsix расширение
Вот и описан базовый функционал.
Только в аналайзере все же больше всяких нюансов. Множество примеров ситуаций использования можно найти в github репозитории — см. там TestAnalyzerLib11 либу (обычная библиотека).
Аналайзер достаточно плотно покрыт юнит тестами — проверяется 125 кейса (или 250 если считать, что проверяется как вариант для C# 10-, так и вариант для C# 11+). См. ElvisModifiersAnalyzer.Tests проект, который на самом деле все юнит тесты создает из исходных файлов TestAnalyzerLib11 либы. Эти кейсы в этой либе обозначены через специальные комментарии в исходном коде, благодаря чему проект юнит тестов парсит файлы исходного кода и понимает какие реакции аналайзера и в каком конкретно месте должны быть.
Таким образом, TestAnalyzerLib11 либа «два в одном» — можно открыть ее в студии и вживую посмотреть работу аналайзера, и в то же время она источник для юнит тестов.
Есть открытые вопросы: их примеры можно найти поиском по !!! и ???строкам в сольюшене (а то и добавьте что от себя).
• Это упомянутый момент с базовыми классами (сделать как для интерфейсов).
• Также для базовых классов имеющих конструктор на котором поставили правила, если он (явно) используется в конструкторах производных классов, то чтобы правила распространялись и на эти конструкторы (как пример, тогда можно было бы сделать чуть более лаконичной реализацию «Шаблон «Посредник» (Mediator)» — см. далее).
• Упомянутый момент с STRICT_OU.
• Также см. определенный, но пока не используемый аналайзером Exclude(You) атрибут. Про его предполагаемое назначение можно почитать в файле ExcludeAttribute.cs + ниже в примере с шаблоном «Строитель» (Builder).
• Еще одним пунктом может быть добавление OnlyYou* подобного семейства (OnlyNs* атрибуты для классов и мемберов), позволяющего селективно задавать правила доступа к функционалу для целых неймспейсов.
Feel free, если кто заинтересовался этой темой (особенно если кто хорошо шарит в анализаторах/генераторах), и желает подключится к развитию этого проекта (стать контрибьютером).
Весь код в репозитории. При компиляции аналайзера создаются ElvisModifiers.Analyzer nuget пакет и крошечный ElvisModifiersLib пакетик с атрибутами. Они опубликованы, так что welcome для подключения к вашим проектам. Достаточно подключить только ElvisModifiers.Analyzer пакет, ElvisModifiersLib подключится автоматически.
Атрибуты в ElvisModifiersLib не имеют неймспейса (т.е. к-л using для их использования не требуется), но при необходимости вы можете назначить алиас для них в .csproj файле вашего проекта.
Хотя и назвал это демонстрационным проектом, но его вполне можно использовать. В отличии от к-л сторонней либы, которая при ее использовании может подвести вас, если она «сырая», FriendAnalyzer ничего в принципе не может нехорошего привнести в ваш прекрасный код, а может лишь выдавать ошибки компиляции, чем попросит вас сделать ваш код еще прекрасней, понятней, яснее.
А возможно эти фичи будут вам столь угодны, что захотите установить расширение в студию, и больше не заморачиваться с установкой пакетов аналайзера — тогда, как воскликнул бы Якубович, «расширение в студию!»
В общем, как поняли, сделано и расширение.
Единственное, для расширения ElvisModifiersLib либа уже автоматом не подтянется в ваш проект, поэтому, хоть ElvisModifiers.Analyzer пакет уже не надо будет ставить, а вот ElvisModifiersLib пакетик с атрибутами необходимо будет установить.
Чтобы решить задачу автоматического подключения атрибутов, пробовал совместить аналайзер и генератор «в одном флаконе», наследуя ElvisModifiersAnalyzer класс не только от DiagnosticAnalyzer но и от IIncrementalGenerator интерфейса. В реализации интерфейса в зависимости от C# версии целевого проекта, инжектиться либо код с generic версией атрибутов, либо без.
Однако, такой подход показал себя крайне нестабильным — приходилось постоянно компилировать и перегружать студию, чтобы ошибки подсвечивались как надо. Возможно есть какие-то тонкости о которых не в курсе, а возможно Microsoft не рассчитывала на такие сценарии. Не стал далее копать, но если кому интересно, в файле ElvisModifiersAnalyzer.cs, в его начале, можете раскомментировать //#define INJECT_ATTR и попробовать.
Но это пока скорее абстрактные исследования с неясными перспективами, ибо расширение с таким автоматическим инжектом атрибутов, скорее всего было бы полезно только для тех, кто сильно проникся новыми модификаторами и хочет их использовать везде-везде.
Если же без фанатизма, то чтобы каждый новый проект вашего сольюшена автоматически цеплял аналайзер, можете в root директорию сольюшена добавить/дополнить Directory.Build.props файл следующего содержания:
<Project> <ItemGroup> <PackageReference Include="ElvisModifiers.Analyzer" Version="*"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
Заключение
Elvis-модификаторы, позволяют задавать новые правила-ограничения.
Ограничения важны. Они суть законы. Как в физике, где формула-закон жестко связывая разные величины тем самым накладывает ограничение, и пущенный камень уже летит не рамдомно, а с ограничением — по параболе.
Тут можно только помечтать (как минимум мне; простите эту слабость), чтобы для этого типа правил-ограничений в языке ввели какую-то более удобную синтаксическую конструкцию нежели вот так на атрибутах и аналайзере (если это вдруг окажется достаточно любезным народу, в этом плане см. далее в «Приложении»: «Общий вывод из примера»).
В некотором смысле, законы-ограничения и определяют тот или иной язык программирования. Одно из сильнейших ограничений — неизменяемость, а какая красота порождается в ФП. Или в ООП та же инкапсуляция (и прочее).
Хотите полной свободы — программируйте в машинных кодах. Неудобно? Кто бы спорил.
Законы из хаоса наводят порядок и красоту, везде: в физике (пристрастен к ней, см. P.S.), в других науках... в человеческом обществе и, конечно, в языках программирования.
Что было бы, если бы камень который вы уронили не упал на землю, а ударил по голове? Пол в квартире внезапно проваливался?... Каждый хочет контролировать свою жизнь, жить в среде, которую он может предсказывать. Законы именно это и позволяют. Так же и в программной среде. И, надеюсь, Elvis-модификаторы, сделают эту среду чуточку более предсказуемой, внесут, больше управляемости в этот важный аспект вашей жизни, дорогие коллеги.
Понравился стих (по памяти, кто написал гугл точно не смог определить):
Не терпит красота канона,
Но и без формы гибнет красота,
А форма требует закона.
Ссылки:
код на github
ElvisModifiers.Analyzer и ElvisModifiersLib nuget пакеты
vsix расширение
P.S. Хочу спросить: будут ли вам интересны статьи на другие темы?
Как минимум, могу предложить статьи по следующим 2-м темам:
1) По квантовой механике. Взглянуть на нее с нетипичной (но вполне академической, не фриковой) точки зрения. Что, надеюсь, поможет лучше понимать ее суть и необычности не специалистам.
Сам физик по базовому образованию. Правда, по специальности не работал — оканчивал в трудные нулевые, так что быстро ушел из нищей аспирантуры (куда был приглашен как красно-дипломник) в коммерческое программирование. А квантовая механика мой любимый раздел физики (ностальгирую).
2) Осенью в сентябре торкнуло отрефрешить свои знания по Haskell, ибо весьма «давненько не брал в руки шашки» и порядком все подзабыл. Чисто академический интерес. Выбрал самую тоненькую книжку «О Haskell по-человечески» (2-е издание), всего 226 страниц крупного текста (pdf версия для планшетов/мобильников). Однако, оказалось что после 24 главы текст обрывается (автор пропал прекратив работу над 2-м изданием), а материал следующих 7-ми глав (более объемные с под-главами) добирал уже в первом издании от 2014.
Но не в этом суть. А в том, что по мере чтения делал конспект в Obsidian (точнее перед чтением перенес книгу в obsidian, где и читал тут же редактируя ее). При всем уважении к автору и благодарности к нему, имхо, у него все же недостаточно чисто писательского мастерства, много лишних фраз, также как и нестрогих формулировок.
Составляя конспект, в меру своих способностей, перерабатывал текст и ликвидировал эти недостатки. Что существенно сократило объем текста.
Некоторые места имели логические лакуны, вызывающие вопросы для более пытливого взгляда — дополнил их.
Вопрос: интересно ли это высокой публике?
Если да, то как лучше этот материл представить на хабре? Если выкладывать по главам, то получится где-то 28 публикаций, хотя можно сразу несколько глав компоновать, тогда будет в разы меньше.
Или может как одну статью + просто приложить архив obsidian базы (можно и то другое)? Насколько люди пользуются obsidian?
Или может выложить на гитхабе? Единственное, в obsidian не стандартный markdown, а более расширенный (и пользуюсь этим), так что не проверял как на гитхабе такой extra-markdown будет выглядеть (в принципе, у obsidian есть еще плагины позволяющие экспортировать куда угодно: html, fb2..., но не пробовал).
Приложение.
A. Ответы на замечания/вопросы
B. Больше примеров
C. Историческая справка
A. Ответы на замечания/вопросы
Приведу ответы на некоторые замечания/вопросы от коллег (огромная благодарность):
Friend нарушает инкапсуляцию и поэтому нам не нужен.
В плюсах отчасти так, и как раз именно это критиковал в статье (как «эксбиционизм»). Однако, даже в плюсах friend вполне себе является стандартом и находит свою полезную нишу применения.
Если же спорить именно о friend, мое мнение: любой инструмент можно как правильно использовать, так и извращенно до абсурда. В общем, как и везде, нужно пользоваться головой.
Только в статье речь не о плюсовых friend-ах, они проходят лишь как начальная ассоциация. А предлагается инструмент лишенный их недостатков — новый модификатор доступа, дополняющий стандартную коллекцию модификаторов. Именно поэтому, чтобы не было этой путаницы, переименовал статью и сделал ребрендинг названий проектов.
И без этого жили, можно вполне обойтись тем что есть.
В принципе так. Так же как можно обходиться и без любых новых фич в языках, даже без ООП — того же C достаточно.
Совсем не предлагается втыкать куда только можно. Как и всякий инструмент, имеет свою специализацию, подходящие случаи применения.
По моему опыту, полезен для задач со сложной логикой. В своих проектах стараюсь следовать ФП «дао-пути», чтобы все классы имели только readonly публичные свойства. И чтобы каждый класс имел доступ только к тому что ему нужно, особенно если это методы с side эффектом. Когда же это не удается сделать тотально (усложняет архитектуру, непроизводительно,…), а это где-то 2-8 случаев на сотню классов, то тут и полезен elvis-модификатор.
Но не только ФП соображения.
Такой модификатор особенно полезен в крупных проектах с четкой архитектурой, где важно контролировать зависимости и предотвращать непреднамеренное использование внутренних API.
В больших, командных проектах иногда видел, что чуть ли не все свойства (надо - не надо) имеют публичные get-set. Вот смотришь на сотни/тысячи таких классов (особенно на чужие, да и на свои сколько-то времени спустя) и чешешь репу — да где же только в этом огромном коде с массой классов (и кратно большим числом мемберов) не сетятся все эти свойства и вызываются все эти методы? Intellisense же при большом числе классов, с их массой мемберов в каждом, тут слабо помогает, к тому же intellisense работает в моменте пока смотришь и не убережет от нарушения неявного (никак не прописанного) контракта в любой момент. С модификаторами же все прозрачно, не тысячи возможных нитей-связей за которые могут все дергать, а одна/две явно прописанные, которые уже не нарушишь.
Описанная ситуация напоминает пресловутый антипаттерн глобального состояния, и усугубляется тем, что это состояние размазано по массе классов и попробуй разберись, как оно меняется, если каждый может изменять каждого. И описываемые в статье инструменты могут помочь тут если и не достичь ФП идеала, то навести больше порядка.
Может приглянутся архитектору, тим-лиду... при проектировании системы классов, когда таких классов тысячи (а мемберов кратно больше). Убережет тех кто потом пишет код (особенно новичков) от неправомерного использования мемберов/классов. В большой системе мало кто все видит и понимает в целом, и вследствие этого есть такой риск. Сделает архитектуру строже и чище. Что, конечно, не отрицает (но дополняет) и другие варианты решения данной проблемы.
Лично мне нравятся такие тонкие инструменты. Как инструменты ювелира (vs инструменты слесаря), ими можно всякую тонкую, так сказать, «часовую механику» налаживать, точнее моделировать предметную область.
Так же см. раздел «Больше примеров».
Связность
Не увеличивается ли связность?
Наверное все же имели ввиду «связанность» (сам путаю эти названия).
Если мембер где-то используется, то эта связь так и так есть. Декларация разрешающая ее тут лишь «документирует» этот факт.
А вот, так сказать, потенциальная связанность, вследствие вводимого ограничения, кардинально, многократно уменьшается.
В то время как связность, которая считается благом, как раз увеличивается, в том смысле, что накладываемые ограничения делают связанные классы более изолированными, не только семантически, но уже и синтаксически модульными (см. далее общий вывод под примером «Шаблон «Посредник» (Mediator)»).
Более того, если хотите, в аналайзер нетрудно добавить выдачу варнинга, если в модификаторе прописано использование для к-л класса/мембера, но не используется.
Тогда помимо документирования это может, с одной стороны, служить как TODO для разработчиков в начале разработки, а с другой, на финише разработки, как сигнал, что данный мембер то ли лишний, то ли его можно убрать в приватные.
B. Больше примеров
В статье был единственный демонстрационный пример. Попробую набросать еще разных простых примеров (теоретических, реальные примеры из моей практики в силу их специфичности потребовали бы долгого ввода в тему, да и в силу новизны проекта мало еще сам где использовал).
И, еще раз, это не значит, что не может быть других решений. Здесь предлагаются решения именно с помощью elvis-модификаторов.
Строгие контракты между связанными классами
⠀⠀
Когда классы реализуют общий протокол или паттерн:
class Parser { // Только Tokenizer может использовать этот метод [OnlyYou<Tokenizer>(nameof(Tokenizer.Reparse))] public void ResetState() { ... } } class Tokenizer { // Использует Parser.ResetState() когда нужно public void Reparse() { parser.ResetState(); // Разрешено } }
class Logger { // Только класс LogManager может инициализировать логгер [OnlyYou<LogManager>(...)] public void Initialize(CoreConfig config) { ... } // Только классы LogWriter и LogReader могут использовать [OnlyYou<LogWriter>(...)] [OnlyYou<LogReader>(...)] public string FormatEntry(LogEntry entry) { ... } }
Разделение ответственности внутри модуля: когда логически связанные классы должны работать вместе, но не раскрывать детали наружу:
class Database { [OnlyYou<QueryOptimizer>(...)] public Statistics GetStats() { ... } } class QueryOptimizer { // Использует статистику для оптимизации запросов }
Безопасное разделение уровней абстракции
// Низкоуровневый компонент class GraphicsBuffer { [OnlyYou<GraphicsRenderer>(...)] public IntPtr GetNativeHandle() { ... } } // Высокоуровневый компонент class GraphicsRenderer { // Единственный, кто может работать с нативными ресурсами }
Тестирование без нарушения инкапсуляции
Вместо internal или рефлексии:
class Cache { // Только тестовый класс может очищать кэш [OnlyYou<CacheTests>(...)] public void ClearForTesting() { ... } }
Всякие случаи реализации структур данных с алгоритмами:
namespace DataStructures; class RedBlackTree { [OnlyYou<RedBlackTreeIterator>(...)] public Node RotateLeft(Node n) { ... } } class RedBlackTreeIterator { // Может использовать внутренние операции дерева }
Реализация шаблонов проектирования
Пройдемся по некоторым шаблонам проектирования.
Состояние (State).
Делаем чтобы методы перехода между состояниями были доступны только контексту. Обычно вынуждены делать методы перехода публичными (или internal), так что любой класс может вызвать методы на состоянии, что нежелательно.
Решение с модификатором:
// Банковский счет - контекст class BankAccount { private IAccountState _state; public decimal Balance { get; private set; } public BankAccount() { _state = new ActiveState(this); } // Только классы состояний могут менять состояние счета [OnlyYou<IAccountState>] public void SetState(IAccountState newState) { _state = newState; Console.WriteLine($"Состояние изменено на {newState.GetType().Name}"); } // Только классы состояний могут изменять баланс [OnlyYou<IAccountState>] public void UpdateBalance(decimal amount) { Balance += amount; } // Публичные методы - интерфейс для клиентов public void Deposit(decimal amount) => _state.Deposit(amount); public void Withdraw(decimal amount) => _state.Withdraw(amount); public void Freeze() => _state.Freeze(); public void Close() => _state.Close(); } // Интерфейс состояния interface IAccountState { void Deposit(decimal amount); void Withdraw(decimal amount); void Freeze(); void Close(); } // Активное состояние class ActiveState : IAccountState { private BankAccount _account; public ActiveState(BankAccount account) { _account = account; } public void Deposit(decimal amount) { // Может обновлять баланс, потому что разрешено модификатором // ... здесь любая необходимая бизнес-логика _account.UpdateBalance(amount); Console.WriteLine($"Внесено {amount}, баланс: {_account.Balance}"); } public void Withdraw(decimal amount) { ... } public void Freeze() { ... } public void Close() { ... } } // Замороженное состояние class FrozenState : IAccountState { ... } // Закрытое состояние class ClosedState : IAccountState { ... }
Преимущества такого подхода:
Контролируемый переход состояний:
class FraudDetectionService { public void CheckForFraud(BankAccount account) { // Ошибка компиляции! Нельзя напрямую менять состояние account.SetState(new FrozenState(account)); // err // Только через публичный интерфейс account.Freeze(); // Правильно - через бизнес-логику } }
Безопасное изменение внутренних данных:
class ExternalService { public void ProcessPayment(BankAccount account) { // Ошибка компиляции! Нельзя напрямую менять баланс account.UpdateBalance(1000); // err // Только через методы состояния account.Deposit(1000); // Правильно - через бизнес-логику } }
Явные отношения между классами
// Модификаторы явно документируют, что только эти классы // могут управлять внутренним состоянием BankAccount [OnlyYou<IAccountState>]
Сценарий использования:
static void Main() { var account = new BankAccount(); // Клиентский код - работает с публичным интерфейсом account.Deposit(1000); // Внесено 1000, баланс: 1000 account.Withdraw(500); // Снято 500, баланс: 500 // Система безопасности решает заморозить счет account.Freeze(); // Счет заморожен // Попытка снять деньги account.Withdraw(100); // Невозможно снять средства - счет заморожен // Внести все еще можно account.Deposit(200); // Внесено на замороженный счет 200, баланс: 700 // После разбирательства счет закрывают account.Close(); // Счет закрыт // Дальнейшие операции невозможны account.Withdraw(100); // Невозможно снять средства - счет закрыт // Но! Никто не может случайно "разморозить" счет, // минуя бизнес-логику account.SetState(new ActiveState(account)); // Ошибка компиляции! }
Шаблон «Фабричный метод» (Factory Method)
Только фабрика может создавать документы:
abstract class Document { public abstract void Open(); public abstract void Save(); } class TextDocument : Document { public const string Type = nameof(TextDocument); // Только фабрика может создавать документы [OnlyYou<DocumentFactory>] public TextDocument() { } public override void Open() => Console.WriteLine("Opening text document"); public override void Save() => Console.WriteLine("Saving text document"); } class SpreadsheetDocument : Document { public const string Type = nameof(SpreadsheetDocument); // Только фабрика может создавать документы [OnlyYou<DocumentFactory>] public SpreadsheetDocument() { } public override void Open() => Console.WriteLine("Opening spreadsheet"); public override void Save() => Console.WriteLine("Saving spreadsheet"); } // Фабрика - единственная, кто может создавать документы class DocumentFactory { public Document CreateDocument(string type) { return type switch { TextDocument.Type => new TextDocument(), // Разрешено SpreadsheetDocument.Type => new SpreadsheetDocument(), // Разрешено _ => throw new ArgumentException() }; } } // Клиентский код не может создать документ напрямую class Client { void Test() { var doc_try = new TextDocument(); // Ошибка компиляции! Client не DocumentFactory // Правильно: var factory = new DocumentFactory(); var doc = factory.CreateDocument(TextDocument.Type); } }
Шаблон «Посредник» (Mediator)
Только медиатор ответственен за создание коллег и только коллеги могут нотифицировать медиатора:
interface IMediator { // Только коллеги могут отправлять сообщения через медиатор [OnlyYou<Colleague>] void Notify(object sender, string eventName); } abstract class Colleague { readonly IMediator _mediator; //[OnlyYou<IMediator>] !!! not implemented protected Colleague(IMediator mediator) => _mediator = mediator; protected void Send(string eventName) => _mediator.Notify(this, eventName); } class Button : Colleague { // Только медиатор может создавать коллег [OnlyYou<IMediator>] public Button(IMediator mediator) : base(mediator) { } public void Click() { Console.WriteLine("Button clicked"); Send("buttonClicked"); // Разрешено как наследнику Colleague } } class TextBox : Colleague { [OnlyYou<IMediator>] public TextBox(IMediator mediator) : base(mediator) { } public string Text { get; private set; } = ""; public void SetText(string text) { Text = text; Send("textChanged"); } } // Конкретный медиатор class MyDialogMediator : IMediator { readonly Button _okButton; readonly TextBox _nameField; public MyDialogMediator() { _okButton = new Button(this); _nameField = new TextBox(this); } public void Notify(object sender, string eventName) { if (sender is TextBox && eventName == "textChanged") { // Включаем кнопку, если поле не пустое if (!string.IsNullOrEmpty(_nameField.Text)) { Console.WriteLine("Enabling OK button"); } } // ... } } // Никто другой не может созавать коллег и отправлять сообщения class Outsider { void TryHack() { var dialog = new MyDialogMediator(); var button = new Button(dialog); // Ошибка компиляции! Outsider не IMediator dialog.Notify(button, "hack event"); ; // Ошибка компиляции! } }
Общий вывод из примера:
Модификаторы позволяют делать так, что группа классов, не только семантически (в голове у разработчика), но теперь и синтаксически образует целостный концепт, без «торчащих ниток» за которые могут дергать кто угодно.
[OnlyYou*] от одних классов, подобно «ребрам», создают узкие направления к другим классам (направленный граф), даже могут «прикрепляться» в конкретных местах к этим классам.
Все видели модели молекул из шариков и ребер. Если сравнивать классы с атомами, то модификаторы позволяют синтаксически задавать некий мета-уровень над классами, более высокий уровень ООП абстракции, скрепляя классы в «молекулы/графы» еще одним способом. Под другими способами имеются ввиду агрегация и наследование.
Шаблон «Строитель» (Builder)
// Только строитель может устанавливать свойства [OnlyYouSet<PizzaBuilder>] class Pizza { readonly List<string> _Toppings = new(); public IEnumerable<string> Toppings => _Toppings; // Только строитель может создавать пиццу [OnlyYou<PizzaBuilder>] public Pizza() { } public string Dough { get; set; } = "Default"; public string Sauce { get; set; } = "Default"; // Только строитель может добавлять [OnlyYou<PizzaBuilder>] public void AddToppings(string topping) => _Toppings.Add(topping); public string Description => $"Dough: {Dough}; Sauce: {Sauce};..."; } //[OnlyYou<PizzaDirector>] class PizzaBuilder { Pizza _pizza = new Pizza(); // Только директор может устанавливать ингредиенты [OnlyYou<PizzaDirector>] public void SetDough(string dough) { _pizza.Dough = dough; } [OnlyYou<PizzaDirector>] public void SetSauce(string sauce) { _pizza.Sauce = sauce; } [OnlyYou<PizzaDirector>] public void AddTopping(string topping) { _pizza.AddToppings(topping); } //[Exclude] // Доступен всем - получить результат можно всегда public Pizza Build() => _pizza; } // Директор - единственный, кто может управлять строителем class PizzaDirector { private PizzaBuilder _builder; public PizzaDirector(PizzaBuilder builder) { // Директор получает строителя и теперь только он им управляет _builder = builder; } public void MakeMargherita() { // Разрешено - мы внутри PizzaDirector _builder.SetDough("thin"); _builder.SetSauce("tomato"); _builder.AddTopping("mozzarella"); _builder.AddTopping("basil"); } public void MakePepperoni() { // Разрешено _builder.SetDough("thick"); _builder.SetSauce("tomato"); _builder.AddTopping("pepperoni"); _builder.AddTopping("cheese"); } } // Клиент теперь защищён от случайных ошибок class Client { void OrderPizza() { var try_pizza = new Pizza(); // Ошибка компиляции! try_pizza.Dough = "Some toxic"; // Ошибка компиляции! var builder = new PizzaBuilder(); var director = new PizzaDirector(builder); director.MakePepperoni(); var pizza = builder.Build(); // ok // но: builder.SetDough("Some toxic"); // Ошибка компиляции! builder.AddTopping("мухамор"); // Ошибка компиляции! // Клиент может только получить готовый продукт Console.WriteLine($"Got pizza: {pizza.Description}"); // и получить (уже только для чтения) свойства var dough = pizza.Dough; // ok } }
Вот и здесь теперь синтаксически целостный концепт, а не набор классов.
Кстати, здесь пригодился бы Exclude(You) атрибут, особенно если бы у класса PizzaBuilder помимо Build() метода было бы не 3, а очень много других Set-подобных методов. Тогда можно было бы поставить [OnlyYou<PizzaDirector>] целиком на класс, а внутри класса использовать Exclude(You) атрибут только и только на Build(), отменяющий правило класса исключительно на этом методе. Именно в этом идея Exclude(You) атрибута (как говорилось, пока не реализовано, даже его название пока условное).
Шаблон «Снимок/Хранитель» (Memento)
Который представим в generic версии:
[OnlyYou(typeof(Originator<>))] class Memento<T> // Снимок/Хранитель { // Только Originator может создавать Memento public Memento(T state) => State = state; // Только Originator может читать состояние public T State { get; } } class Originator<T> // Создатель { public T State { get; set; } // Только Caretaker может создавать Снимок [OnlyYou(typeof(Caretaker<>))] public Memento<T> Save() => new Memento<T>(State); // Только Caretaker может восстанавливать Снимок [OnlyYou(typeof(Caretaker<>))] public void Restore(Memento<T> memento) => State = memento.State; } // Может хранить Memento, но не может читать или изменять его class Caretaker<T> // Опекун { private Stack<Memento<T>> _history = new (); public void SaveState(Originator<T> originator) => _history.Push(originator.Save()); public void Undo(Originator<T> originator) { if (_history.Count > 0) { var memento = _history.Pop(); originator.Restore(memento); var state = memento.State; // Ошибка компиляции! Caretaker не Originator } } } // Использование class Program { static void Main() { var origin = new Originator<string>(); var care = new Caretaker<string>(); origin.State = "A"; care.SaveState(origin); origin.State = "B"; care.Undo(origin); // Восстановит состояние "A" // Но: var memento = origin.Save(); // Ошибка компиляции! var state = memento.State; // Ошибка компиляции! memento = new Memento<string>("bad state"); // Ошибка компиляции! } }
Можно и дальше идти по шаблонам, перебирая и улучшая их (оказалось увлекательное занятие), но, пожалуй, остановлюсь на этом, статья «не резиновая». Если кому интересно и неленивый может попробовать перебрать остальные основные шаблоны.
Реализация механизмов платформы/фреймворка
Лишь наметим некоторые направления. Не прорабатывал, поэтому скорее как гипотезы.
Системы привязки данных (data binding)
Обычно это делается через интерфейс INotifyPropertyChanged:
// Типичная модель с уведомлениями class Person : INotifyPropertyChanged { // Проблема: это событие публично, каждый может подписаться // хотя нужно только системе привязки public event PropertyChangedEventHandler PropertyChanged; private string _Name; public string Name { get => _Name; set { if (_Name != value) { _name = value; OnPropertyChanged(nameof(Name)); } } } // Проблема: этот метод публичный, его может вызвать кто угодно, // хотя он нужен только системе привязки public void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } class EvilService { public void MessWithPerson(Person person) { // Можно вызвать уведомление без реального изменения person.OnPropertyChanged(nameof(Person.Name)); // Или вызвать для несуществующего свойства person.OnPropertyChanged("NonExistentProperty"); // Подписаться на событие person.PropertyChanged += (s, e) => { // Evil action } }
Защититься можно разрешив для отмеченных мемберов доступ только интерфейсу IDataBindingSystem(условное название) фреймворка системы привязки данных.
Сериализаторы,
где хотим регулировать доступ к свойствам/полям.
Более явно, чем рефлексия — проверяется на этапе компиляции.
На этом, пожалуй, хватит примеров. Надеюсь немного «растопил лед», расшевелил вашу фантазию, чтобы тоже могли прикидывать варианты применения, пусть поначалу, как и со всяким новым инструментом, это может быть непривычно.
C. Историческая справка
Все написав и опубликовав: код, пакеты, статью, ai-сгенерив элвис-обложку и элвис-иконки для пакетов; решил отвлечься и узнать больше об истории песни «Only You» от Элвиса Пресли, ну как всю жизнь считал. Однако похоже это одно из массовых заблуждений (а-ля эффект Манделы). Инет пестрит ссылками приписывающими эту песню Элвису:
https://only-you-presley.skysound7.com
Elvis Presley - Only You (lyrics)
...
В реальности, эта песня все же похоже за авторством группы The Platters (хотя некоторые в постах продолжают спорить упорно приписывая ее Элвису).
Что делать? Пусть будет как будет. Бывает, что виртуальный факт перевешивает реальный, история от этого только интереснее и богаче, главное знать ее полностью — и мнимую и реальную часть (как в комплексных числах).
В конце-концов, это песня эпохи и стиля Элвиса и «утиный тест» в применении к нему, очевидно, проходит.
Да и вообще, никто не отменял авторского произвола называть как угодно, даже без всяких привязок, а тут привязка ничуть не хуже (имхо, даже покрепче), чем у сово-натяжного названия «элвис-оператор».
А вот еще ИИ спешит на помощь:
Imagine Elvis Singing in The Platters’ Voice ONLY YOU | A WISH AI Video
или:
Elvis Presley - Only You in This Room | AI Inspired Vintage Love Ballad | 1950s Romantic Rock & Soul
для тех у кого сложности с ютубом (google drive):
Так что будем идти в ногу со временем, ведь ИИ артефакты это «стильно, модно, молодежно».
Как я выкрутился, а? :-)
Бонус :) (google drive)
(извините, по завершении получилось как индийское кино — с песнями)
Комментарии (25)

chegeras
15.03.2026 08:31в этом примере куда-то делись деньги, у меня ушло 50, а другу пришло 20
это налог на дефолтную имплементацию в интерфейсах?
me: 100; friend: -10me: 50; friend: 10
VadimLL Автор
15.03.2026 08:31Спасибо большое! Верно, в первой строчке опечатка, вы правы, конечно, должно быть:
me: 100; friend: -40Поправил.

iamkisly
15.03.2026 08:31Если честно, выглядит как оверинжинириг для сумрачного гения. Код должен быть интуитивно понятен, а тут интуитивностью не пахнет, и очень напоминает мне маниакальное обмазывание декораторами в java.

withkittens
15.03.2026 08:31Не знаю. Не хочу умалять ваш труд, но я тоже не вижу для себя применение этой штуки. Чем старше я становлюсь, тем больше ценю подходы потупее и попроще.
Надо подружить два класса - можно повесить
internalи положить в отдельную сборку (хотелось бы в дотнете модулей поменьше, как в Rust, ну да ладно). Либо через explicit interface implementation, чтобы методы или свойства не отсвечивали в intellisense. Либо скрыть черезEditorBrowsable(Never). Либо как в EF Core - положить вInternalnamespace и написать анализатор. Влезть можно - но на свой страх и риск. В конце концов, знак "Осторожно! Злая собака" не призван запретить вам перелезать через забор, только лишний раз подумать ;)Большой плюс подходов выше - они простые, используются много лет и всем знакомы. Если разработчик увидит
EditorBrowsable(Never)- он поймёт что это такое и для чего.Большой минус
OnlyYou<T>- сложность её понимания превосходит пользу, которую она приносит. Имхо, конечно.ПС. Поставил вам плюсик за готовность воспринимать критику. Эта статья читается намного проще.

VadimLL Автор
15.03.2026 08:31Withkittens Спасибо за профессиональный пост!
Вы правы, есть разные решения, которые не умоляю. Да, и в Rust есть, что хотелось бы иметь и в C#.
Предлагаемый вариант, имхо, все же гибче, селективнее описанных вами подходов. В примерах в «Приложении» попытался показать применения. Возможно дело привычки (всегда кажется лучше и проще то к чему привык). Наверное пристрастен, только, имхо, просто поставить на мембере или классе/интерфейсе атрибут с вполне очевидным смыслом (Только Ты<T>), не так уж напряженно. Вот вы, как понял, профи и в Rust, вот там, имхо, действительно сложные вещи (как минимум за себя скажу — ломал мозги когда изучал).

AlexM2001
15.03.2026 08:31На работе реорганизацию устроили ...
Собрали ВСЕХ с IT дипломами и озадачили развитием компании, в т.ч. направления программирования.
Также и тематикой озадачили указанной у данного автора.
Я пока только разбираюсь в вопросе, но полагаю методика интересная и заслуживает внимания.
Сразу скажу что публикации плюс поставил, т.к. заинтересован в развитии данной темы на Хабре.
Не так много публикаций подробных, подобной этой нашел. Этих данных в прямой выдаче в Интернете немного, IMHO
P.S. Озадачили на работе именно тематикой разбираемой (довольно подробно) в данном посте.
Стараюсь отслеживать, искать новые публикации/материалы.
Лично мне не хватает опыта, и данная статья очень кстати.

gBear
15.03.2026 08:31Доброе...
Начну с середины
Если мембер где-то используется, то эта связь так и так есть. Декларация разрешающая ее тут лишь «документирует» этот факт.
Это не так. И, имхо, это достаточно очевидно. Например, достаточно "представить", что "аттрактор" и "друг" это типы из разных сборок. "Лишняя" связь тут же станет очевидной. Нет?
Представить в кавычках, т.к. сильно не уверен, что "оно" умеет в "такое". Код не смотрел. Но без "такого" ценность (и так не очевидная) этой штуки прям сильно уменьшается, имхо.
Через интерфейс
Вот как раз "через интерфейс" исходная задача решается "с точностью до наоборот". В том смысле, что исходная "сущность" - мутабельная, а через реализацию интерфейса предоставляется readonly проекция. Нет? При таком подходе, "грануляция" доступа - пока отбросив её ценность - органично "схлопывается" до агрегатов. Оно может быть и выглядит громоздко на синтетике, но смотрится вполне себе нормально и логично (отбросив, опять же, всё остальное, что можно "предъявить" такому агрегату).
В C++ весь контроль над друзьями находится на стороне класса ..., что по идее более правильно.
Как бы это уже всё давно разжевано не один раз. "Это" - как раз - не правильно. Механизм "дружбы" в плюсах - это следствие, а не причина, так сказать. И если речь о "дружбе" на уровне классов, то реальной ценности там нет. Если мне не изменяет мой склероз, даже Страуструп предлагал смотреть на "дружбу" исключительно с позиции friend function (например, при реализации операторов).
В отличии от плюсов, не можем сделать другом только для конкретного метода другого класса.
Глубокое имхо... ценность такой "гранулярности" - есть следствие "кривоватого" дизайна. Насколько я понимаю, оно может быть хоть сколько-то ценно при работе с - скажем так - перегруженным агрегатом, в качестве аттрактора. Но даже при этом - мы живём в реальном мире, и не отрицаем этого - я бы сильно подумал над тем, нужно ли мне плодить доп. связность, практически, на ровном месте. Есть более "ровные" способы обеспечить такого рода разграничения. Особенно, если речь исключительно о конкретной сборке.
То что приватно и должно оставаться приватным. Тем более что всякого приватного у класса может быть очень много, и выставлять все это хозяйство на показ (как в плюсах) тот еще «эксгибиционизм».
?! Ну т.е. все "минусы" упомянутые вами ранее - это на самом деле не "минусы"? Так? И мы - внезапно - осознаем что из "C++ дружбы" хоть сколько-то ценно только friend function? Или как?
Пусть даже если так. Но в чём ценность такой "дружбы" для C#? "Дать доступ" же не является само целью? Надеюсь. В "плюсах" для этой "штуки" есть вполне конкретная область - операторы - в которой "оно" прям ценно. В том смысле, что другими механиками языка это "не лечится".
А в C# где вы видите эту "область ценности"? Вопрос вызван тем, что - выглядит так - начали мы с одного, а пришли к другому :-) Т.е. решали одну задачу, а решение (которое получилось) - это решение какой-то совсем другой задачи. Вот и интересно - какой? Там у вас есть некоторые "рассуждения на тему". Но это именно что "рассуждения не тему". Можете ли вы переформулировать их в терминах той задачи, которая сформулирована в начале статьи?
Следующий тип случаев с иерархией, реализован так ...
Хм... имхо, это "немножко" не логично. Логично, имхо, требовать для этого случая явной реализации интерфейса. Во избежание, так сказать. Ну и - если уж всё это навеяно "C++ дружбой" - не стоит забывать про "три не". Они не просто так были сформулированы. А из рассуждений про "как должно быть правильно", складывается стойкое ощущение, что про одно из не вы всё время забываете :-(
А предлагается инструмент лишенный их недостатков — новый модификатор доступа, дополняющий стандартную коллекцию модификаторов.
Ну это как сказать :-) Если оно - как я подозреваю (хочу ошибаться) - работает только в рамках конкретной сборки... лично мне вот вообще не понятна ценность этого "инструмента". И даже если представить, что это такой "заход" в т.н. sealed hierarchy - что могло бы иметь ценность, применительно к C#... оно - всё равно - выглядит сильно "странновато" и "не ровно".
Модификаторы позволяют делать так, что группа классов, не только семантически (в голове у разработчика), но теперь и синтаксически образует целостный концепт, без «торчащих ниток» за которые могут дергать кто угодно.
Когда вы говорите про "торчащие нитки", вы вообще о чём? Эти "нитки" они из сборки чтоль "торчат"? Или откуда? Складывается ощущение что вам кто-то строго запретил пользоваться internal (и его производными) уровнем доступа и использовать neasted иерархии. Это, кстати, относится и к приведенным вами "примерам".
Без более строго сформулированного "зачем" - лично у меня - статья оставляет сильное "хлебное" послевкусие :-)

VadimLL Автор
15.03.2026 08:31Спасибо!
Прежде всего отмечу, что, имхо, польза может быть особенно в проектах с большим, сложным внутреннем API, где важно строгое соблюдения контрактов/бизнес-логики.Сборок может быть сколько надо, нет ограничений, главное чтобы аналайзер был ко всем ним подключен, или подключен хотя бы пакет с атрибутам, если в какой-то сборке функции аналайзера не нужны, но атрибуты используются (если все в одном сольюшене, то предлагался
Directory.Build.props).Например, достаточно "представить", что "аттрактор" и "друг" это типы из разных сборок. "Лишняя" связь тут же станет очевидной. Нет?
В разных они сборках или нет, в любом случае, они знают друг о друге (иначе как декларацию декларировать?). Знают и используют (уже имеем связь), если же не используют, то писал о варининге и TODO (или, наоборот, можно сказать, UNDO индикаторе, если оказался просчет в начальном проектировании).
Вот как раз "через интерфейс" исходная задача решается "с точностью до наоборот". В том смысле, что исходная "сущность" - мутабельная, а через реализацию интерфейса предоставляется readonly проекция. Нет? При таком подходе, "грануляция" доступа - пока отбросив её ценность - органично "схлопывается" до агрегатов. Оно может быть и выглядит громоздко на синтетике, но смотрится вполне себе нормально и логично (отбросив, опять же, всё остальное, что можно "предъявить" такому агрегату).
Исходная сущность (класс
Me) внешне как раз не мутабельная (readonly), а
"через интерфейс" предоставляется строго избирательная "проекция" с доступом к внешне скрытым или/и readonly мемберам.
Простите, про агрегацию, плохо вас понял. Имеете ввиду inner/nested классы (когда inner класс может получать доступ к закрытым мемберам outer класса)?Про френды в C++ уже который раз пожалел, что вообще упомянул, они лишь начальная ассоциация, и, смотрю, каждый раз, «вызывают на себя огонь». Вот их можем отбросить, они лишь путают в понимании.
(а правило "три не" в плюсах, хорошее правило, если бы еще все его знали и использовали, это ведь только благое пожелание, а не запреты на уровне синтаксиса языка).Помимо основного тела статьи, в «Приложении» сделал много примеров задач с возможным применением, обоснованием зачем. Думал достаточно (можно попробовать их решать другими методами, не спорю, и тогда уж сравнить что будет проще/лаконичнее...).
Когда вы говорите про "торчащие нитки", вы вообще о чём?
Про «торчащие нитки» (образное выражение), за которые могут дергать (т.е. использовать эти нитки-мемберы/классы: вызывать методы, сеттить свойства...) все (ну почти все с учетом более глобальных модификаторов) в том числе и те кому не желательно согласно контракту (это иллюстрировал в примерах).
Складывается ощущение что вам кто-то строго запретил пользоваться internal (и его производными) уровнем доступа и использовать neasted иерархии.
internal и nested не дает той гибкости и селективности, как данный модификатор.
internal работает «по площадям».
nested не всегда возможен (а если нужен как nested сразу в нескольких классах? а если нужен «взаимный» nested?) и удобен, и он получает доступ сразу ко всем приватным мемберам outer класса (фактически как в плюсах в варианте не function friend), что не есть хорошо.Про оценку ценности тут, конечно, пусть каждый сам приценяется, насколько ему интересен данный функционал (сугубо лично мне интересен).

gBear
15.03.2026 08:31Прежде всего отмечу, что, имхо, польза может быть особенно в проектах с большим, сложным внутреннем API, где важно строгое соблюдения контрактов/бизнес-логики.
Я это уже читал, но так и не понял в чём конкретно эта "польза". Другими словами, я не понимаю, каким образом дополнительная связность (хоть её наличие вы и отрицаете) может быть полезна. Особенно, в "проектах с большим, сложным внутреннем API".
Сборок может быть сколько надо, нет ограничений ...
Ну я таки глянул код. Позвольте усомниться в этом вашем утверждении. Если аттрактор и друг это типы из разных сборок, то возникает циклическая зависимость между сборками. Причина - та самая "лишняя" связность, наличие которой вы отрицаете, но которой, тем не менее, нет в канонических вариантах решения.
Собственно, это (наличие циклической зависимости) было очевидно и без кода... но я надеялся, что у вас там она разрешается, например, через генерацию отдельной сборки. Я ошибался.
В разных они сборках или нет, в любом случае, они знают друг о друге ...
При традиционных подходах к - та часть, что в вашем варианте становится аттрактором ничего не знает о "дружественной" части.
Исходная сущность (класс Me) внешне как раз не мутабельная (readonly), а
"через интерфейс" предоставляется строго избирательная "проекция" с доступом к внешне скрытым или/и readonly мемберам.Я это понял :-) Моя реплика была про то, что каноническое решение выглядит с точностью до наоборот. Если хотите, то вопрос в том, почему у вас не так? Почему за основу взят - так сказать - "вывернутый" вариант?
... а правило "три не" в плюсах, хорошее правило ...
Как я понимаю, мы о разных "трех не" говорим. Речь была о том, что "дружба" не наследуется. Не просто так. Очевидно, что - как минимум - в отношении к интерфейсам у вас "всё не так". Я, собственно, поэтому и упомянул явную реализацию.
Помимо основного тела статьи, в «Приложении» сделал много примеров задач с возможным применением, обоснованием зачем.
К сожалению, там нет "обоснования зачем". Там есть "смотри как могу". Любой из рассмотренных примеров замечательно решается без "этого". Собственно, в большинстве случаев, вы сами же об этом и пишите. Поэтому и был мой вопрос про "область ценности".
Про «торчащие нитки» ...
Опять же... если кто-то, что-то "видит" - он имеет полное право за это "дергать". И - скажем так - "прикладная задача проектирования" сводится к "надо сделать так, чтобы каждый видел только то, что ему можно дергать". А вы - буквально - говорите "а давайте на это всё забьем" :-)
internal и nested не дает той гибкости и селективности, как данный модификатор.
Вот ваш пример:
class Me { [OnlyYou<IFriend>(..)] public void SomeMethod() { } } interface IFriend { void UseMe(Me me); }Объясните мне, пожалуйста, чем "это" - в принципе - отличается от?
Мой пример:
class Me { private void SomeMethod() { } interface IFriend { void UseMe(Me me) => me.SomeMethod(); } }Мне вот видится, что - принципиально - ничем. И, имхо, второй вариант "чище", как минимум, как раз из-за того, что ничего не "торчит". Т.е. с мой точки зрения, всё что вы пытаетесь делать в такого рода вариантах - это заменить вполне традиционную технику т.н. mixin типов, на непонятно что :-(
internal работает «по площадям»
Зато для него есть, например, InternalsVisibleToAttribute, который позволяет вытащить "потроха" в отдельную сборку, и уже там сделать "торчать красиво" :-) В смысле, чтоб "торчало" только то, что нужно. И не приводит, при этом, к циклическим зависимостям.
nested не всегда возможен (а если нужен как nested сразу в нескольких классах? а если нужен «взаимный» nested?) и удобен, и он получает доступ сразу ко всем приватным мемберам outer класса (фактически как в плюсах в варианте не function friend), что не есть хорошо.
Ну вы чего?! Через neasted объявляются агрегаты (сильно упрощая - интерфейсы), которые потом используются для "сборки" того, что вам нужно где-то "во вне". Т.е. если вам прям так нужен "слуга двух господ" - вы вполне можете его получить без особых приседаний. И даже в другой сборке, если надо.
Если говорить за "вообще", то приемов работы с tight coupling - мы знаем не мало. В том смысле, что мы знаем "как есть этих слонов". Проблема - ту которую я вижу - заключается в том, что "дружба" - это как раз то, что к этой самой tight coupling приводит.
Про оценку ценности тут, конечно, пусть каждый сам приценяется ...
dixi :-)

VadimLL Автор
15.03.2026 08:31Если аттрактор и друг это типы из разных сборок, то возникает циклическая зависимость между сборками.
Понял вас. В принципе вы правы.
А чтобы не было циклической зависимостей (но не только), такие зависимости обычно принято выносить в отдельную сборку (какую-нибудь «Contracts») в виде интерфейсов, даже рекомендуется как часть SOLID принципов — DIP. Тогда можно будет без проблем определять правила на интерфейсах.Но спасибо вам, что подсказали вариант доработки аналайзера, для случаев если не хотим это делать.
Решается просто: например, добавить дляOnlyYouатрибута еще один конструктор с таким использованием:
[OnlyYou("T:MyFriend",...)]
Ибо аналайзеру, в принципе, достаточно лишь имени типа.Как я понимаю, мы о разных "трех не" говорим.
Пожалуй, оставим это правило в плюсах, чтобы не уводить дискуссию в сторону. Это своя тема.
И ассоциация с плюсовыми френдами только путает.
Обсуждаемый псевдо-модификатор это не френды (плюсовые френды критикую в статье), а это коротко говоря:publicс правилами.И следующая ваша цитата, мне кажется, продиктована этой путаницей с плюсами:
Опять же... если кто-то, что-то "видит" - он имеет полное право за это "дергать". И - скажем так - "прикладная задача проектирования" сводится к "надо сделать так, чтобы каждый видел только то, что ему можно дергать". А вы - буквально - говорите "а давайте на это всё забьем" :-)
По примерам с nested классами и вообще о nested решениях:
Во первых попадаем под то проклятие — «эксбиционизм» плюсовых френдов (если используем именно inner класс как есть).
Не можем сделать один и тот же класс как nested сразу 2-х классов
Менее селективно.
Не всегда правильно загонять класс в nested (по моему опыту необходимости в элвис-модификаторе возникала в таких классах, которые ну совсем не годились чтобы из них делать nested пары).Приведу такой сравнительный пример:
Скрытый текст
namespace NestedTest { class Me { private string _veryConfidentialValue = "top secret"; private void Method1() { } public void Method2() { var friend = new Friend(); // ok friend.SomeMethod(); // ok } public class Friend { public Friend() { } public void UseMe1(Me me) { me.Method1(); var getAnySecret = me._veryConfidentialValue; // ok me._veryConfidentialValue = "hack any secret"; // ok } public void UseMe2(Me2 me2) { me2.Method1(); // err } public void SomeMethod() { var me = new Me(); } } } class Me2 { private void Method1() { } } class Other { public void UseMe1(Me me) { var friend = new Me.Friend(); // ok friend.SomeMethod(); // ok } } } namespace EATest { class Me { private string _verySecureValue = "top secret"; public Me() { } [OnlyYou<Friend>(nameof(Friend.UseMe1))] public void Method1() { } public void Method2() { var friend = new Friend(); // ok friend.SomeMethod(); // err } } class Me2 { public void Method1() { } } class Friend { [OnlyYou<Me>] public Friend() { } public void UseMe1(Me me) { me.Method1(); // ok var getAnySecret = me._veryConfidentialValue; // err me._veryConfidentialValue = "hack any secret"; // err } public void UseMe2(Me2 me2) { me2.Method1(); // ok } [OnlyYou<Me>(nameof(Me.Method2))] public void SomeMethod() { } } class Other { public void UseMe1(Me me) { var friend = new Friend(); // err friend.SomeMethod(); // err } } }Ну вы чего?! Через neasted объявляются агрегаты (сильно упрощая - интерфейсы), которые потом используются для "сборки" того, что вам нужно где-то "во вне".
Насколько вас понял, под агрегатами (интерфейсы) имеете ввиду определять интерфейсы (или (абстрактные) классы) как nested с реализацией доступа с приватному функционалу внешнего, а потом где нужно наследоваться от этих интерфейсов.
Т.е. ровно то, что я описывал в разделе статьи «Через интерфейс» (очевидно, интерфейсов может быть сколько угодно). Ну а далее предложил и другой подход в виде псевдо-модификатора.
usrsse2
Обычно под "Elvis operator" имеют в виду "?:", иногда "??" или "?.", поэтому по названию кажется, что будет что-нибудь типа "protected?"
VadimLL Автор
В каком-то смысле модификатор и защищает, только не от NullReferenceException, а от нарушения бизнес-логики/контракта.
Если бы не было «Elvis оператора», то наверное и не подумал так назвать модификатор, а так дорога уже проложена, считайте традиция, да и для «маркетинга», имхо, хорошее название (в кавычках, потому как какой маркетинг при MIT лицензии).
VadimLL Автор
На счет "?:" не встречал чтобы называли его "Elvis operator". Классический Элвис-олератор, как верно далее пишете, это "??" (также как и "?.", тоже относится к Элвис семейству операторов).
iamkisly
https://kotlinlang.org/docs/null-safety.html#elvis-operator
VadimLL Автор
Это в Kotlin. В C# же (наш контекст) "?:" все же принято называть тернарным оператором (очень старый оператор еще до Элвис оператора):
https://www.google.com/search?client=opera&q=what+is+"%3F%3A"+operator+in+c%23%3F&sourceid=opera&ie=UTF-8&oe=UTF-8
и, как минимум, гугл не согласен называть его Элвис оператором в C#:
https://www.google.com/search?client=opera&q="%3F%3A"+operator+in+c%23+is+Elvis+operator%3F&sourceid=opera&ie=UTF-8&oe=UTF-8
iamkisly
Тернарный оператор и элвис это для широкой аудитории одно и тоже. Потому что выглядит и ведет себя одинаково. А вы хотите присвоить устоявшуюся терминологию, и от того лезете сейчас в бутылку.
VadimLL Автор
Пожалуйста, приведите мне к-л авторитетное руководство по C#, где тернарный оператор называли бы элвис оператором. может плохо ищу:
https://www.google.com/search?q=Тернарный+оператор+и+Элвис+оператор+это+одно+и+тоже+в+C%23%3F&newwindow=1&client=opera&hs=Xco&sca_esv=2da575c93e33c7c8&sxsrf=ANbL-n7Jsk_eBKYYWIR76J2l42pvuAoU1Q%3A1773574077816&ei=vZe2aZrGMd39wPAP0tuvuQI&biw=912&bih=501&ved=0ahUKEwjamJCn5qGTAxXdPhAIHdLtKycQ4dUDCBE&uact=5&oq=Тернарный+оператор+и+Элвис+оператор+это+одно+и+тоже+в+C%23%3F
И, поверьте, не «лезу в бутылку», готов признавать свои ошибки (буду только благодарен если укажите на них, ставлю за них лайки, как тут ниже для chegeras и вам готов поставить), готов узнавать что-то новое. Просто пока не понимаю вас.
И опять замечу, что речь в статье о C#, не о Kotlin. В C# именно вот так
?:одной строкой вообще нет такого оператора.iamkisly
Ваша позиция понятна, но ошибочна. То что в C# и dotnet оператор называется тернарным, вообще не исключает того что он же много лет неформально называется элвисом. Нельзя просто взять слово из устоявшейся лексикона и использовать в новом смысле, явно создавая путаницу.. просто потому что вам нравится Пресли.
VadimLL Автор
Увы, авторитетных доказательств что именно в C# тернарный оператор называется еще и Элвис оператором вы не привели. Уважаю ваше мнение и право называть что-либо как вы хотите, но пока тот же гугл не разделяет его. Даже не минусую ваши посты (в отличии от вас).
В статье же речь не об операторе, а о модификаторе, так что какая путаница?
iamkisly
Если вы уж так топите за авторитетные руководства, то почему пытаетесь назвать предмет статьи модификатором. Модификаторов доступа в C# ровно 5 (7 если считать C#11 file и CLR's family and assembly), а сабж - это атрибуция.
VadimLL Автор
Потому как этот атрибут фактически и выполняет функцию модификатора доступа, конечно, нового модификатора не входящего в стандарт. Поэтому счел возможным назвать как модификатор.
У нас с вами дискуссии по названиям. Имхо, названия не столь важны как суть, предлагаю не продолжать их.
iamkisly
Ваше предложение отвергнуто.
В C# модификаторы доступа прибиты гвоздями к CLR, и как синтаксис доступа к классам их свойствам не отделимы от него.
Как удобно у вас получается, тут значит затребую доказательства, а тут "счел возможным". Вы "либо крестик снимите, либо трусы наденьте", а то как-то очень избирательно у вас всё.
VadimLL Автор
Ну почему "прибиты гвоздями", вот новые появляются как тот же file модификатор. И, конечно, в CLR не лезу, кто же мне позволит? Ну назвал модификатором инструмент, который фактически выполняет функцию модификатора. Вы не согласны с названием, понял вас, уважаю ваше мнение (говорю, даже ни одного минуса не поставил вам в отличии от вас, после моих ответов). Смысл дальше спорить? (и не я его начинал, лишь вежливо отвечаю вам)
withkittens
Странный спор у вас тут происходит, но ваша собственная ссылка различает elvis (
a ?: b) и тернарный (a ? b : c) операторы. У них же даже арность разная, как они могут быть одним и тем же.А если говорить про C#, у нас под elvis'ом всегда понимался null-conditional operator (
?.), а тернарный всегда был тернарным.usrsse2
https://en.wikipedia.org/wiki/Elvis_operator
Я имел в виду именно
?:как бинарный оператор, а не тернарный, и не утверждал, что это в C#.