Pre Scriptum (добавленный Post Factum)
Внимание! Опасность! Статья с элементами юмора. Пусть, шуток совсем немного (в общей доле всего материала, пожалуй, где-то сотые доли). Но из-за этого статью заминусовали. По делу же критики не увидел. Забавно, что после публикации и 2-х минут (реально, без шуток) не прошло, а уже сразу 2 минуса. «Не читал, но осуждаю»?
Не понимаю. Имхо, всегда приветствовалась живая подача материала, с юмором, который расслабляет, настраивает на игривый лад (а известно, что в не напряженной, игровой форме лучше всего идет усвоение). Совсем сухо - это справочник читать. В конце-концов ~ «вам без шушочек или ехать»?
Специально старался сделать теплую, остроумною статью. Первую статью. Не говоря уже о том, что все это оформлять - реально великий труд, подготовка материалов, публикация nuget пакетов, расширений,... да хотя бы картинку оформить...
Сделал для вас, имхо, весьма полезный функционал («красота для тех кто пониматЪ», конечно) и выложил в открытый доступ.
Склоняюсь к тому, что больше нет смысла писать статьи (хотя идей много) — первая и последняя. Просто людям не нужно, так зачем такие усилия?
Надеялся, еще поднять карму (ибо ранее она была начальная, читал, не писал). Ага, Спасибо Большое! Спасибо за заботу, за доброту, за ласку...
Мечты сбываются, поднял — отрицательный рост кармы. Сам, кстати, не минусую, только плюсы ставлю. Даже если нахамят. Если есть что сказать — просто говорю, как и сейчас.
+ еще ссылка на мой коммент-разъяснение по теме (открывать в новой вкладке - ибо, похоже, недоработака хабра в работе со ссылками в markdown)
Чаль путхактыримнида ▶ 잘 부탁드립니다
(кор.) ~ Пожалуйста, позаботьтесь обо мне.
Так принято говорить в Корее, впервые вливаясь в какое-то сообщество (новая школа, работа и другие похожие поводы... в моем случае — первая здесь статья). И делать поклон.
(может и вы читали цикл «Косплей Сергея Юркина»?)
«На наши щи» (vs кимчи): Бью челом бояре! Паки, паки...!
И надеюсь найти друзей (дружескую поддержку) не только в C#, но и здесь.
Как адепт функционального программирования (ФП), всегда стараюсь выстроить так систему классов приложения, чтобы классы в идеальном случаем имели только readonly публичные свойства (публичные это как минимум, а так — все). Братья адепты (и сестры, короче — сиблинги) тут меня хорошо поймут.
Не знаю нужно ли объяснять насколько это проще, понятнее, когда имеешь дело с такими классами? Наверное нет, ведь тут собрались те еще «тертые калачи» в программировании (хотя, не исключено, что они еще покажут мне, где кузькина мать зимует и докажут, что, наоборот, неправ во всем и в этом вопросе в частности).
Ну а для менее искушенного читателя лишь замечу — гляньте на тот же string C# класс. Как же проще понимать логику программы при работе со string экземплярами, когда знаешь, что их невозможно изменить.
А беззаботность в многопоточности? Недаром ФП ныне один из основных трендов в развитии языков.
А что делать? Кремневая микроэлектроника уже фактически уперлась в физический предел и дальнейший прогресс идет по пути наращивания числа ядер, многопоточности. Благодаря же неизменяемости в ФП, хороший компилятор с функционального языка умеет сам автоматически распараллеливать, создавать на выходе многопоточное приложение из кода, где вручную этого даже не было предусмотрено.
Как бывший активный плюсовик, а сейчас разве что пассивный (только не подумайте чего) и уже «давненько не брал в руки шашки». Как то в процессе разработке одного приложения на C# столкнулся с тем, что возжелал странного — friend-ов аки в плюсах, ибо уж больно они, если бы были в C#, вписались бы, решая архитектурную проблемку в том приложении.
Вы спросите, причем тут ФП? И будете правы. Просто тут чистый авторский произвол, волюнтаристский, авторитарный стиль написания статьи (понять и простить). Однако, все же поясню эту свою «колокольню».
Friend-ы, конечно, не входит в инструментарий академического ФП, но эта фича позволяет минимизировать «ущерб», когда пытаемся подражать стилю ФП в не ФП языках. Точнее в нечистых (свят, свят, свят... сгинь нечистый) ФП языках, ибо функциональные фичи сейчас есть везде.
Пусть некое свойство все же изменяемое, но мы строго локализуем случаи, когда такие изменения допустимы, не допуская до «комиссарского тела» кого попало. Может трогать лишь очень-очень узкий круг («член ЦК» там), для всех остальных же, все приблизительно выглядит как в чистом/непорочном ФП.
И тут может было бы более корректно говорить в терминах инкапсуляции, открытости/закрытости... Но влюбленные все видят через призму объекта своего обожания, так что вот так вот вывернулось в мозгах (еще раз понять и простить, даже бороду готов отрастить).
Вот, учитесь натягивать совушку на глобус.
Так или иначе, но именно ФП было побудительным мотивом, вдохновило на сей труд.
Ладно, оставим «хвилософию».
От абстрактной философии к суровой правде жизни
Вот вполне себе жизненная ситуация:
Есть условный «я» (class Me) и у меня есть друг (class MyFriend). Причем бедный друг (почти «бедный Йорик», но «еще жив курилка!»).
И пусть, с одной стороны, у меня есть 100 рублей (св-во Rubles).
А с другой, ведь «не имей сто рублей, а имей сто друзей», а также у друзей ведь все должно быть «на лапопам», не так ли? Так что готов разделить с ним рубли. Хочу дать другу «ключи от квартиры где деньги лежат» — приходи дорогой, бери половину рублей из тумбочки.
class Me {
public decimal Rubles { get; set; } = 100;
}
class MyFriend
{
public void AcceptMoney(in Me me)
{
decimal half = me.Rubles / 2;
me.Rubles = half;
Rubles += half;
}
public decimal Rubles { get; private set; } = -40;
}
Однако, очевидно, в такой реализации мои рубли открыты для всех, а не только для друга (так что вместо класса Me тут более подошло бы название Loh).
В плюсах тут можно было бы объявить класс MyFriend как friend для класса Me, где Me.Rubles было бы свойством открытым только для чтения, а вот к закрытому полю под этим свойством класс MyFriend как раз и имел бы доступ.
Прежде чем представить C# решение, приблизим ситуацию к еще более реальной: Пусть также у меня есть еще и юани (Yuans). А вот это уже святое (чай не уходящие в анналы истории всякие баксы с еврами, если верить пропаганде). К тому же, про юани народная мудрость ничего не говорит. Так что ни-ни, никому, приватные мои юанчики — другая (бронированная) тумбочка должна быть под надежным шифром.
Не правда ли хорошо вам всем знакомая жизненная ситуация?
Вот как можно реализовать аналог плюсовых friend-ов:
///////// Реализиция friend-ов:
class Me
{
public interface IFriend
{
// Setter
static protected void setMoney(in Me self, in decimal value)
=> self.Rubles = value;
}
public decimal Rubles { get; private set; } = 100;
public decimal Yuans { get; private set; } = 100_000;
}
class MyFriend : Me.IFriend
{
public void AcceptMoney(in Me me)
{
decimal half = me.Rubles / 2;
Me.IFriend.setMoney(me, half);
Rubles += half;
}
public decimal Rubles { 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.Rubles}₽; friend: {friend.Rubles}₽");
}
выполнив тест, ожидаемо получим:
me: 100₽; friend: -10₽
me: 50₽; friend: 10₽
Красота — и други сыты и юани целы.
Очевидно, здесь доступ будет разрешен только и только для друзей (кто является наследником от Me.IFriend интерфейса).
А посмотрев на Me класс в IDE, например, в Visual Studio:

Сразу видим кто допущен.
Более того, ведь не обязательно тут должен быть именно сеттер (setMoney), это избыточно «щедро». Здесь расчет на то, что друг, имеет строгий моральный кодекс и не возьмет больше половины.
А если нет? И если тумбочка подключена к кредитно-банковской линии? Тогда ведь он может снять и 100_000 ₽, загнав меня в минус. Простим, понять его можно — на 10 ₽ нынче особо сыт не будешь (тут ранее погорячился заявляя такое).
Чтобы не вводить друга в искушение (ибо сказано «да не введи нас во искушение»), можно так сделать:
class Me
{
public interface IFriend
{
static protected decimal TakeMyHalfMoney(Me self)
{
decimal half = self.Rubles / 2;
self.Rubles -= half;
return half;
}
}
public decimal Rubles { get; private set; } = 100;
public decimal Yuans { get; private set; } = 100_000;
}
class MyFriend : Me.IFriend
{
public void AcceptMoney(in Me me)
=> Rubles += Me.IFriend.TakeMyHalfMoney(me);
public decimal Rubles { get; private set; } = -40;
}
Здесь уже дали более тонкий доступ, разрешив другу делать только и только то, что хотели,
Анализ, сравнение с C++
Буду сравнивать с теми плюсами, которые сам юзал в оное время. Что там наворотили в последних редакциях не проверял — вдруг что-то и дополнилось в плане friend-ов.
Но поскольку эта глава определенный вбоквел к статье, и интересна разве только тем кто «и вышивать могет, и на машинке тоже...» и знает C# и C++ как кот Матроскин, то даже не стал проверять — на тему статьи это совершенно не влияет, чтобы этим озабочиваться. Но если что, буду благодарен активным плюсовикам за активные комменты.
• Большой плюс: IFriend дает доступ не ко всем закрытым членам, а позволяет тонкую настройку, где можно обернуть это логикой, защитить от неправомерных операций (vs: вспомним как для случая доступа к непосредственному сеттеру можно загнать тумбочку в кредитную кабалу). Кроме того, в плюсах для friend открылся бы доступ и к «никому не дам» юанями (к приватному полю под этим свойством) так что прощай юанчики (цзай цзянь юанчики 再见 元).
• Минус: В C++ весь контроль над друзьями находится на стороне класса (в отличии от наследования MyFriend : Me.IFriend), что по идее более правильно.
С другой стороны, интерфейс может иногда быть и плюсиком, если класс находится в другой библиотеке, а мы хотим воспользоваться его дружелюбными фичами, которые он предоставляет.
И вам не докучают с просьбами добавить в друзья (или лезут в код, редактируя ваш C++ класс) — один раз написали C# класс, определив границы его дружелюбия, и отдыхаете.
Есть и определенный контроль. IDE в помощь — она всегда покажет разработчику Me.IFriend интерфейса кто подключился в друзья. Как уже отмечали, всегда можно посмотреть список друзей:

• Большой минус: в отличии от плюсов, не можем сделать другом только для конкретного метода другого класса.
В общем несколько разные подходы с определенным балансом своих плюсов и минусов.
Но это еще не конец. Попробуем еще один, имхо, красивый подход, не только наведя порядок аки в плюсах, но и более того.
Порядок (красота для тех кто пониматЪ)
Для того чтобы сделать так же как в плюсах доступ только для конкретного метода, а также контроль друзей на стороне класса, можно воспользоваться такими Roslyn технологиями как Analyzers или/и Incremental Source Generators.
Что же, «пацан сказал — пацан сделал» (хотя некоторые говорят, что лучше звучит: «пацан сказал — девочка сделала», но я олдскульный человек).
Ссылка на проект
Описывать внутреннюю реализацию этого Roslyn проекта здесь нет смысла — обычный проект такого рода. Речь в статье совсем не о техниках Roslyn.
А о том какой инструментарий предоставляется для удовлетворения наших хотелок и как его использовать. Каков предлагаемый «интерфейс», а не второстепенное дело деталей его реализации (по ним см. код на гитхабе).
Вот на этом далее и сосредоточимся.
Элвис атрибут.
Имеется [OnlyYou] атрибут (можно было назвать и OnlyFor, но просто в честь известной песни ▶, так что можете считать что перешли от темы friend к теме girlfriend):
[AttributeUsage(AttributeTargets.Class
| AttributeTargets.Method
| AttributeTargets.Property,
AllowMultiple = true)]
class OnlyYouAttribute : Attribute
{
public OnlyYouAttribute(Type type,
params string[] members) { }
}
Как видим из определения, атрибут можно применять для любого метода или свойства (мембера), класса или интерфейса, статического или не статического.
Теперь можем следующим образом избавится от того недостатка, когда не было возможности как в плюсах сделать другом только и только конкретный метод другого класса:
class Me
{
public interface IFriend
{
[OnlyYou(typeof(MyFriend),
nameof(MyFriend.AcceptMoney))]
static protected decimal TakeMyHalfMoney(Me self) {...}
}
...
}
class MyFriend : Me.IFriend
{
public void AcceptMoney(in Me me)
=> Rubles += Me.IFriend.TakeMyHalfMoney(me); // ok
public void CantAcceptMoney(in Me me)
=> Rubles += Me.IFriend.TakeMyHalfMoney(me); // err
...
}
Здесь комментарии err и ok показывают, где будет ошибка компиляции, а где она пройдет успешно.
Только этот пример избыточный — теперь интерфейс по сути не нужен (см. следующий пример).
Analyzer проверяет вызовы (использование) мемберов, которые отмечены данными атрибутами. И только в тех местах вызовы этих мемберов будут разрешены, которые описаны в параметрах [OnlyYou] атрибутов. Для всех же остальных вызовов (в местах, которые не описаны в параметрах атрибутов) будет ошибка компиляции.
Для одного мембера, как видно из определения атрибута, можно задать несколько таких атрибутов.
Вот более чистый (без Me.IFriend интерфейса) пример использования этого атрибута:
class Me
{
public decimal Rubles { get; private set; } = 100;
[OnlyYou(typeof(MyFriend),
nameof(MyFriend.AcceptMoney))]
public decimal TakeMyHalfMoney() {
decimal half = Rubles / 2;
Rubles -= half;
return half;
}
}
class MyFriend
{
public void AcceptMoney(in Me me)
=> Rubles += me.TakeMyHalfMoney(); // ok
public void CantAcceptMoney(in Me me)
=> Rubles += me.TakeMyHalfMoney(); // err
public void AcceptMoneyFromLoh(in NotMe me)
=> Rubles += me.TakeMyHalfMoney(); // ok
public decimal Rubles { get; private set; } = -40;
}
class NotMe // Loh :)
{
public decimal TakeMyHalfMoney() => ... 1000;
...
}
В Visual Studio это так выглядит:

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

Свойства можно читать, но изменять их могут только друзья.
Попробую ввести такую терминологию: про термин «друг» (класс MyFriend в примерах) вопросов нет, а вот того с кем хотят все дружить, кто всех привлекает (класс Me в примерах), назову «аттрактором» (придумайте лучше? социофил? другофил(фу!)?).
Собственно это альтернативный, уже со своей спецификой и возможностями, способ задания друзей.
Основная специфика, что в отличии от плюсов не позволяем залезать в приватную часть класса. Считаю это большим благом — то что приватно и должно оставаться приватным. Тем более что всякого приватного у класса может быть очень много, и выставлять все это хозяйство на показ (как в плюсах) тот еще «эксгибиционизм». Да еще и все это (надо - не надо) друзья могут курочить как угодно, внося хаос, так что может так случится, что с такими «друзьями» и врагов не понадобится.
Здесь же точечная, ювелирная работа — определяем публичный мембер, где разрешаем друзьям действовать только в тех рамках которые сами определили как безопасные. Для остальных же (недружественных) классов этот мембер становится фактически как если бы он был определен приватным в классе аттракторе.
Написал как Analyzer (FriendAnalyzer) так и Incremental Source Generators (FriendGenerator).
Это предварительные демонстрационные проекты.
Где FriendGenerator сделан только для методов. Его просто попробовал на случай если вдруг генератор будет быстрее, благодаря хваленым кэшам в новых инкрементальных генераторах. И для генератора не нужно добавлять код класса атрибута, он сам его добавляет в компиляцию.
Но что быстрее — не стал устраивать сравнительные «забеги», решив все же остановится на Analyzer варианте, который себя и так хорошо показал. Сгенерировал для него 1000 cs-файлов с простыми классами друзей, добавил в проект и не заметил каких-либо тормозов при редактировании кода в Visual Studio (для определенности: Visual Studio 2022, 17.14.16).
Было беспокойство, что при редактировании кода будет пере-анализироваться весь проект. Ибо как в этом плане ведут себя аналайзеры, как-то не встречал чтобы про это писали в доках по ним. Добавил в аналайзер короткий (в 100ms) консольный бип, фактически трещетку. И убедился, что аналайзер действует очень экономно — анализирует только тот файл на который смотрим. И даже похоже учитывает прокрутку, т.е. только тот кусок что видим. При открытии файла выдает очень короткую дробь, а при первой (только первой) прокрутке можно услышать одиночные щелчки. При редактировании — та же очень короткая дробь, несмотря на > 1000 зависимых файлов.
У [OnlyYou(type, method1, prop1..)] вторым аргументом идет список имен методов/свойств. Для методов же, как известно, возможна перегрузка. Однако в этом атрибуте такие перегруженные методы не различаются — атрибут принимает только имена. Так что если method1 перегружен, то все его варианты будут френдами.
И здесь можно попробовать реализовать более тонкий подход - например, передавать не просто имена методов, а сигнатуры, например: "method1(string, int)".
Однако, такой подход со строкой сигнатуры не очень нравятся. Ибо здесь нужно и не ошибиться в ее составлении, и в процессе разработки следить за тем, чтобы в этой строке всегда была именно актуальная сигнатура, и следить за регистром букв...
Поэтому пошел другим путем. Были определены еще 2 атрибута:
[AttributeUsage(AttributeTargets.Method
| AttributeTargets.Property,
AllowMultiple = true)]
class OnlyAliasAttribute : Attribute
{
public OnlyAliasAttribute(Type type,
params string[] aliases) { }
}
[AttributeUsage(AttributeTargets.Method,
AllowMultiple = true)]
class FriendAliasAttribute : Attribute
{
public FriendAliasAttribute(string alias) { }
}
Атрибут [OnlyAlias] играет ту же роль что и [OnlyYou], только вторым аргументом теперь передаем не имена методов, а их алиасы. Алиас же для метода можно назначить с помощью атрибута [FriendAlias].
Пример:
class Me
{
public const string AcceptMul = nameof(AcceptMul);
public decimal Money { get; private set; } = 100;
[OnlyAlias(typeof(MyFriend), AcceptMul)]
public decimal TakeMyHalfMoney() {
decimal half = Money / 2;
Money -= half;
return half;
}
}
class MyFriend
{
public void AcceptMoney(in Me me)
=> Money += me.TakeMyHalfMoney(); // err
[FriendAlias(Me.AcceptMul)]
public void AcceptMoney(in Me me, int mul)
=> Money += mul * me.TakeMyHalfMoney(); // ok
public decimal Money { get; private set; } = -40;
}
Так что для случаев перегрузки методов, можно использовать этот инструментарий.
Рекомендую именно так определять алиасы — константами в классе аттрактора. Такое единственное место определения убережет от возможной ошибки в значениях алиаса в первом и втором атрибуте (в принципе можно даже добавить специальное правило в аналайзер, которое будет заставлять именно так делать).
Рассмотрим такой случай:
Пусть для одного и того же MyFriend класса на один и тот же перегруженный AcceptMoney метод (для не перегруженного вопросов нет) из нашего примера выше, навесили сразу и [OnlyAlias] и [OnlyYou] атрибуты:
class Me
{
public const string AcceptMul = nameof(AcceptMul);
public decimal Money { get; private set; } = 100;
[OnlyAlias(typeof(MyFriend), AcceptMul)]
[OnlyYou (typeof(MyFriend), AcceptMoney)]
public decimal TakeMyHalfMoney() {
decimal half = Money / 2;
Money -= half;
return half;
}
}
Тогда имеем дилемму:
[OnlyYou] разрешает оба варианта AcceptMoney,
[OnlyAlias] же, со своей стороны, разрешает только метод под AcceptMul алиасом.
Кто будет прав?
Сделал по принципу «или» — объединение множеств методов первого и второго атрибутов, а не пересечение. Т.е. в нашем примере оба перегруженных метода будут разрешены.
Так, думаю, более логично.
Но в принципе возможен выбор и в пользу «и» (пересечения множеств методов). А то и вовсе, определить в аналайзере специальное правило, которое бы запрещало такие случаи.
Как видно из определения [OnlyYou] атрибута его можно применять и к классам. С очевидным смыслом — разрешать доступ к мемберам аттрактора только избранным типам (далее еще немного «порастекаюсь по древу», а потом будет сразу общий пример кода).
Прим.:
кстати, опять отметим здесь такое же (как и для friend-методов) благое расхождение уже с friend-классами в плюсах.
[OnlyYou] на аттракторе «бьет» [OnlyYou]/[OnlyAlias] на принадлежащих ему мемберах. Т.е. если у аттрактора запрещены все типы кроме некоторых привилегированных, то на его мемберах уже никто не сможет пролезть, пытаясь определить на них атрибуты разрешающие доступ запрещенным типам.
Как если у короля, «нашего королька — как это я называю» (класса аттрактора) есть враги (недружественные типы), то если его подданные (его мемберы) вдруг попытаются с ними дружить, то это будет расценено как предательство, и СБ королевства (аналайзер) это будет пресекать («расстрелы, только расстрелы...!»). В принципе можно даже добавить в аналайзер правило, согласно которому, запрещено использовать [OnlyYou] на мемберах, если параметр типа применяемого атрибута не один из тех, что разрешены в атрибутах на классе аттракторе.
Пример:
[OnlyYou(typeof(MyFriend))]
class Me
{
public decimal Money { get; set; } = 100;
public void Method1() { }
[OnlyYou(typeof(MyFriend),
nameof(MyFriend.CanInvoke1))]
[OnlyYou(typeof(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
}
Хотя тут можно было реализовать и другой принцип, типа: «вассал моего вассал не мой вассал».
[OnlyYou] на классе — почти как новый модификатор доступа. Очень нравится не так давно введенный file модификатор в C#, не редко применяю его. Здесь же получается еще более селективно — фактически разрешаем доступ только для избранных классов, только тем классам, кому это реально нужно, отсекая прочих праздно-досужих, а то и «шпионов» :)
Хотя в текущей реализации это не полноценный модификатор доступа, ибо, например, читать те же открытые свойства аттрактора по прежнему можно. Но, думаю, лучше бы и это все запретить, вернее сделать опциональным, задавая параметром в атрибуте.
В C# как-то якобы похоже регулировать доступ можно через специальный импорт, например:
using SpecificClassAlias = Utilities.SpecificUtilityClass;
Однако, в [OnlyYou] варианте доступа мы, во-первых, задаем это на стороне аттрактора.
Во-вторых, если в неймспейсе Utilities куча других классов которые нам нужны, то перечислять их все, да еще назначать алиасы (что мне не нравится — даже если делать их совпадающими с именами классов, то при переименовании классов будут несоответствия).
А если попытаться сделать:
using SpecificClassAlias = Utilities.SpecificUtilityClass;
using Utilities;
то второй using просто обесценит первый.
И в-третьих, с [OnlyYou] глянув на аттрактор — сразу видим, кому и только кому и только что разрешено. Имхо, это ценная информация для понимания кода программы (как другими, так и самому автору год спустя). Больше порядка, «пуговицы в ряд». В отличии от юзингов, где все на совести пользователей, и ваши классы-«кровиночки» все так же могут «полоскать» кто только пожелает, «в любых позах».
В общем, регулировка доступа через юзинги — «это другое» (c).
Вот и описан базовый функционал.
Только в аналайзере все же больше всяких нюансов, особенно с наследованием классов. Множество примеров ситуаций использования можно найти в github репозитории — см. там TestAnalyzerLib тестовую либу. Есть открытые темы — их примеры можно найти поиском по !!! строке в этой либе (а то и добавьте своих открытых тем, уверен их можно найти). Также пока не делалась реализация для полей (да и нужно ли?) ...
Хоть велико-скромно (ибо самый скромный человек в мире) и назвал это демонстрационным проектом, но его вполне можно использовать. В отличии от к-л сторонней либы, которая при ее использовании может подвести вас, если она «сырая». FriendAnalyzer ничего в принципе не может нехорошего привнести в ваш прекрасный код, а может лишь выдавать ошибки компиляции, чем попросит вас сделать ваш код еще прекрасней, понятней, яснее.
Feel free, если кто заинтересовался этой темой (особенно если кто хорошо шарит в анализаторах/генераторах), и желает подключится к развитию этого проекта (стать контрибьютером).
Весь код в репозитории. При компиляции аналайзера создаются Friend.Analyzer nuget пакет и крошечный FriendLib пакетик с атрибутами. Они опубликованы, так что welcome для подключения к вашим проектам.
Единственное, несмотря на то, что FriendLib пакетик определен как зависимый пакет, который используется Friend.Analyzer пакетом, он почему-то автоматические не подключается, когда аналайзер добавляется в пользовательский проект.
Возможно потому, что аналайзер это аналайзер, т.е. он не линкуется в пользовательский проект, и поэтому возможно и FriendLib не добавляется за компанию. Так что сейчас нужно добавлять FriendLib самим. Но если это не фича и все же возможно как-то автоматизировать это добавление, то буду благодарен за мудрое наставление.
(Как вариант такой автоматизации, так теоретизировал: определить в проекте аналайзера еще и генератор (Incremental Source Generator), единственной функцией которого будет добавлять код атрибутов при его инициализации. Ну если «прокатит» аналайзер и генератор «в одном флаконе».).
Или, вообще, код этих атрибутов можно просто вручную добавить в код вашего проекта, благо они тривиальны.
А возможно эти фичи будут вам столь угодны, что захотите установить расширение в студию, и больше не заморачиваться с установкой пакетов аналайзера — тогда, как воскликнул бы Якубович, «расширение в студию!»
Еще, можно сказать, эти правила-ограничения вводят более строгий, тонко-настраиваемый вариант инкапсуляции. Даже хотелось бы, чтобы в языке ввели для этого какую-то удобную синтаксическую конструкцию, нежели вот так на атрибутах и аналайзере (в плюсах же озаботились включить в язык подобное).
Заключение (или программист в законе)
Ограничения важны. Они суть законы. Как в физике, где формула-закон жестко связывая разные величины тем самым накладывая ограничение, и пущенный камень уже летит не рамдомно, а с ограничением — по параболе.
В некотором смысле, законы-ограничения и определяют тот или иной язык программирования. Одно из сильнейших ограничений — неизменяемость, а какая красота порождается в ФП. Или в ООП та же инкапсуляция и др....
Хотите полной свободы — программируйте в машинных кодах. Неудобно? Кто бы спорил.
Законы из хаоса наводят порядок и красоту, везде: в физике (простите, опять первой ее упоминаю, пристрастен — как физик по базовому образованию), в других науках... в человеческом обществе и, конечно, в языках программирования.
Что было бы, если бы камень который вы уронили не упал на землю, а ударил по голове? Пол в квартире внезапно проваливался?... Каждый хочет контролировать свою жизнь, жить в среде, которую он может предсказывать. Законы именно это и позволяют. Так же и в программной среде. И, надеюсь, эти Элвис/friend законы, сделают эту среду чуточку более предсказуемой, внесут, в натуре, больше управляемости в этот важный аспект вашей жизни, дорогие коллеги-кореша :)
Понравился стих (по памяти, кто написал гугл точно не смог определить):
Не терпит красота канона,
Но и без формы гибнет красота,
А форма требует закона.
Заранее спасибо за ваши лайки и комменты — они «греют».
Ой, а могут и огреть (зачем сказал? зачем навожу людей на неправильные мысли? хорошо же выше строчкой закончил статью... ой, дурааак)
P.S. Поделюсь опытом написания статей на хабре (скажете: вот нахал — только-только первую статью написал, а уже опытом делится!):
Фух!
(вот и весь пока опыт — оказывается тот еще труд; или трудны только первые 100 статей?)
P.P.S. «выпить йаду» или «пиши есчо»?
Ссылки:
код на github
Friend.Analyzer и FriendLib nuget пакеты
vsix расширение
Комментарии (29)

Cheater
03.11.2025 14:11От стиля статьи прямо как в ламповые 00-е кинуло если не раньше :)
Про плюсы не понял откуда там гранулярный доступ к членам класса. Такого в C++ нет, ни в современном, ни в старом. Есть множество способов указывать друзей (конкретная ф-я, конкретный класс, конкретный метод чужого класса, шаблон...), но любой друг всегда видит всё. Гранулярный доступ к методам в C++ для друзей осуществляется через дополнительный набор ограничивающих интерфейсов поверх данного класса, одна из известных реализаций это идиома Attorney-Client https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Friendship_and_the_Attorney-Client.

VadimLL Автор
03.11.2025 14:11В C++ можно определять friend-ы только для конкретных функций/методов (не целиком для классов):
https://www.w3schools.com/cpp/cpp_friend_function.asp(почему-то в конце ссылки символ  добавляет, удалите его после прохода по ссылки и перейдите заново)
Или что имеете ввиду?

VadimLL Автор
03.11.2025 14:11Спасибо! Хоть кто-то оценил стиль. Как раз старался чтобы был ламповым. Но видимо не попал в тренд, видимо сейчас в тренде только жесть, только хардкор :)

SWATOPLUS
03.11.2025 14:11А можно выжать воду и написать техническую статью? А то мне понадобились значительные усилия, что бы понять о чем статья, несмотря на то, что я знаком и с с# и с с++.
Суть статьи это создание friend-классов (в терминологии c++), возможность определенного класса иметь доступ к не публичным членам другого класса. Но зачем, если в c# есть модификатор доступа internal? Есть ли кейсы где internal не достаточно?

VadimLL Автор
03.11.2025 14:11internal дает доступ всем классам в сборке. Пример же (кейс) c# друзей как раз сквозным образом проходит через всю статью.

SWATOPLUS
03.11.2025 14:11Я делаю сборку библиотеки и только мои классы могут лезть в internal, пользователи библиотеки не могут трогать internal.
Это расширяет возможности разработчикам библиотеки, но снижает пользователям библиотеки (что б не сломали). А для friend классов какой кейс?

VadimLL Автор
03.11.2025 14:11Если в вашей библиотеке пара классов, то может и не вопрос (и то с оговорками). Только разве в реальных проектах часто так? А по кейсу уже писал — пример, сквозным образом проходит через всю статью.
Как знаете, помимо internal есть разные модификаторы доступа. В статье, можно сказать, предлагается еще один, так сказать, elvis-модификатор. Если не видите его специфику, разницу с internal, то, честно говоря, не знаю, что тут еще добавить, кроме того, что уже подробно описано в статье.
Wolfdp
03.11.2025 14:11Да хоть миллион классов, насколько часто нужно именно запрещать доступ всем остальным, кроме конкретного класса? Мне кажется это очень специфическая задача и проще (возможно даже и лучше) прокинуть доступ через интерфейс/статику/вложенность.
Хотя строго говоря это попахивает нарушением инкапсуляции. Если изменение извне запретили для всех, значит это может нарушить логику работы класса. Если даем разрешение внешнему коду -- есть смысл тогда просто "распаковать" доступ полностью, и делегировать управление внешнему коду.

a-tk
03.11.2025 14:11Как же тяжело пробиваться через обилие словоблудия на квадратный пиксель

VadimLL Автор
03.11.2025 14:11Хорошо, что не так?
Пробую понять...
Первый с хвостиком абзац — приветствие человека первый раз написавшего здесь статью. Дань вежливости.
Далее немного славословий любимому ФП. Ведь тем более далее будут ссылки на ФП. Совсем-совсем немного! В сентябре вот решил отрефрешить свои знания по Haskell выбрал самую краткую книжку по нему, так там только первые 4 главы (немаленькая часть книги) «словоблудили» в вашей терминологии. И нормально, те кому не нужно (как мне), просматривали по диагонали, кивая головой. Здесь что-ли «пробивались»? Это же как раз легкая часть.
Кстати, после этой статьи была мысль (напрасная? опыт не удался) как раз написать статью по Haskell.Кроме того, показалось, что читающим будет интересен «линк» между friend и ФП (может и баян, но сам лично не встречал). Такие семантические/ассоциативные связи весьма приветствуются — чем их больше, тем лучше, авторитетно считается, что они активируют креативность у людей. Может и здесь кого-то на что-то, уже свое, по цепочке ассоциаций сподвигло бы.
Кроме того, как писал, это было моей мотивацией. И также известно, что мотивация, мысль-исток из которой развилась к-л теория, важна для лучшего, более глубокого понимания.
Поэтому и есть вводная часть.
Потом идет львиная, центральная часть статьи. Тут уже чисто техническая информация (как вы любите), чуть-чуть скрашенная юмором (простите). Через эти крошки юмора что-ли «пробивались»? Имхо, обычно это как раз самые легкие фрагментики. Ну, если не брать героев анекдотов определенной национальности (все нации уважаю — не я такой, анекдоты такие), до которых долго доходит юмор и они «подвисают».
Есть образы для лучшего восприятия. Вот у вас тоже хороший образ, да еще с юмором: «словоблудия на квадратный пиксель». Так и хорошо выражаетесь. Или только вам можно?И в конце небольшое заключение. Имхо, интересная (мне точно) мысль о важности законов-ограничений. Есть поверье, что чем больше дается свободы, тем лучше. Здесь же «встречная» мысль. Те же стихи — ограничение на рифму, дает красоту, углубляет смыслы (еще: «тесные врата и узкий путь ведут в жизнь вечную» — вот, еще за цитату заминусуйте). Считал важным это высказать, ибо полагаю важным философским принципом, важным обобщением, взяв за базу которое, кому-то может прийти идея как придумать свои-другие ограничения (так что а, вдруг, вообще придумаете что-то по-круче принципов-ограничений ФП).
Эта часть можно сказать мета-выход за рамки friend-ов. Как отчасти и другие части (каламбурчик) — не только о friend-ах речь (о них и вовсе можно не упоминать, а говорить о новом elvis-модификаторе доступа), но в заголовки же все содержание не выскажешь, а тут приходят и критикуют, мол сбился с темы заголовка (а там, между прочим, есть уточнение: «однако, более того»).Поэтому и есть заключительная («словоблудная») маленькая часть.
Может тут просто какое-то взаимное недопонимание?

a-tk
03.11.2025 14:11Сократим до поста:
Я захотел сломать объектную модель C# и сделать анализатор, который позволит мне это сделать. Я не знаю, что такое generic-атрибуты и зачем нужно ключевое слово in (но использую его), и сейчас нагорожу свой огород так, что теперь код должен знать своих клиентов, что порождает избыточную связность кода.

ValeriyPus
03.11.2025 14:11Суть в том, что теперь методы можно вызывать лишь из определенных методов.
Не хватает:
1) вызова только из определенных Namespace, из определенных классов\интерфейсов.
2) Вызывать отовсюду, кроме...
3) nuget-пакета с тестами
4) внятного стиля изложения

VadimLL Автор
03.11.2025 14:111) Да namespaces можно добавить для коллекции (также как и поля...). А "из определенных классов\интерфейсов" сделано (про это есть в статье).
2) В принципе можно добавить и инверсию, правда пока плохо представляю, где это может быть полезно.
3) Пакеты с тестами... Помилосердствуйте, это только первая версия. И так замаялся все это оформлять - реально труд великий с этой статьей, подготовкой материалов, публикаций nuget пакетов, расширений,... да хотя бы картинку оформить... И, как оказалось, заминусоваили. Склоняюсь к тому, что больше нет смысла писать статьи (хотя идей много), первая и последняя - людям не нужно.
4) Понять и простить.

Oceanshiver
03.11.2025 14:11На мой вкус вот эти плюсовые friends - это какой-то рудимент, который только запутывает логику приложения и делает зависимости менее прозрачными. Как хорошо, что его не стали тащить в C# разработчики языка.

VadimLL Автор
03.11.2025 14:11В C# много чего из плюсов не стали тащить. И, да, friend-ы в C++ тоже критикую в статье, если заметили. Но не только критикую ("критикуя - предлагай"), но и предлагаю, имхо, более адекватный вариант.

Oceanshiver
03.11.2025 14:11К сожалению, статью дочитать не смог - устал продираться через все эти шуточки-прибауточки

kemsky
03.11.2025 14:11Текст имеет все признаки гпт (да и код), читать невозможно.

VadimLL Автор
03.11.2025 14:11Можно конкретно написать, какие признаки GPT? Так же вместо того, чтобы приписывать невесть что, можно ведь просто спросить (именно это будет этично). И если бы меня спросили ответил: Текст писал ИСКЛЮЧИТЕЛЬНО сам. Разве не видно, что он имеет индивидуальные черты — мышление у меня поливалентное, не одномерное, со множеством ассоциаций, думал люди оценят, улыбнутся. Разве так пишет GPT? И опять, считаю что без конкретики — это как, вот вам ассоциация, если понравится: вброс сферического г..(галоши?) в вакуум (а раз вентилятор в вакууме не функционален, то точно галоши).
Так же как и код писал «вот этими вот руками». Заглядывал за некоторыми деталями в доки и в google конечно (а у него, конечно, gemini в первой строчке), ибо все детали того же Roslyn помнить невозможно (разве только на нем узко специализироваться 10 лет подряд). Никаких же промптов типа "напиши мне целиком такой-то ... проект",... готово, выкладываю — и в помине не было (хотя даже не знаю, хорошо это или плохо, скорее плохо).
И какой код? Примеры в статье? (вот их точно абсолютно никуда не глядя писал). В репозитории? Если там, то что конкретно там не так?Почему люди не пишут о самом материале статьи, вместо этого практически переходя на личности?
Посмотрел вашу ссылку.
Какой-то там куций функционал. Только для класса, только для internal, а может и только для static - непонятно из их единственного примера и описания:
https://github.com/piotrstenke/Durian?tab=readme-ov-file#friendclassВ то время как для классов — это как раз наименее интересный случай. Там имеем похожие недостатки что и в плюсах, о которых писал («эксгибиционизм» членов класса).
И главное — не музыкально. При написании кода Элвиса не напеть.
P.S. А у вас статьи, кстати, интересные.

posledam
03.11.2025 14:11Я честно пытался...

VadimLL Автор
03.11.2025 14:11Эх, может хоть кто скажет, где именно трудности?
Вот прям уже «Фигаро тут, Фигаро там» (или как сапер — «одна нога здесь, другая там»), разрываюсь, пытаюсь всем угодить, на каждый коммент максимально развернуто ответить, пытаюсь понять по очень скупым туманным фразам, что человек имеет ввиду. Вас, дорогие коллеги, похоже мне (конечно только мне) понять еще труднее.
Где? В вводной части? Обычно такие, лирико-хвилософские вводные части в книжках быстро «проглатываются».
Может в той, где сравнивается C# и C++? Ну она, как писал, вбоквел, только для тех Матроскиных, кто хорошо знает оба языка, ее можно без всякого ущерба пропустить.
Хотя, пожалуй, для понимания некоторых замечаний в других частях, все же надо представлять что такое friend-ы в плюсах. Может надо было дать вначале краткую справку? Если в этом дело, только скажите — добавлю.
В основной, технической части?
Не верится, что фрагменты с юмором мешают, их можно тоже пропускать без ущерба.
Сам пример, что сквозным образом проходит через всю статью? Разве он не элементарен?
Думаю, скорее всего что-то в самом техническом контенте.
Что именно? Какая фраза/абзац вызывают затруднения, требуют дополнительного пояснения? Можно несколько примеров?Когда оформлял статью, думал какой уровень сложность ей поставить, колебался между "Простой" и "Средний". Может тут ошибся и надо было ставить "Сложный"?

vladislaw2020
03.11.2025 14:11Классная статья! Честно, не понимаю, что людям не нравится, я хоть и никогда не работал с C#(плюсы only), но все отлично понял, подача материала годная, читать приятно. Респект автору в общем)
DanielKross
Тема интересная, но статья попахивает графоманством. Имхо поменьше "шуточек" только пошло бы на пользу. Ничего личного!
VadimLL Автор
Спасибо за ваше мнение! Имхо, шуточек совсем немного (в общей доле всего материала, пожалуй, где-то сотые доли или меньше). Также, имхо, всегда приветствовалась живая подача материала, с юмором, который расслабляет, настраивает на игривый лад (а известно, что в не напряженной, игровой форме лучше всего идет усвоение). Совсем сухо - это справочник читать.
В конечном счете, ~ «вам без шушочек или ехать» :)
ZSN_2
Усвоение пива с сухариками...
VadimLL Автор
:) Нормальная шутка. Только странные люди - сами шутят, а другим запрещают.