Чому приватні поля є приватними для цього типу, а не екземплярами?


153

У C # (та багатьох інших мовах) абсолютно законно отримати доступ до приватних полів інших екземплярів того ж типу. Наприклад:

public class Foo
{
    private bool aBool;

    public void DoBar(Foo anotherFoo)
    {
        if (anotherFoo.aBool) ...
    }
}

Оскільки специфікація C # (розділи 3.5.1, 3.5.2) констатує, що доступ до приватних полів має тип, а не екземпляр. Я обговорював це з колегою, і ми намагаємось знайти причину, чому це працює так (а не обмежувати доступ до того ж примірника).

Найкращий аргумент, який ми могли б придумати, - це перевірка рівності, де клас може захотіти отримати доступ до приватних полів для визначення рівності з іншим екземпляром. Чи є інші причини? Або якась золота причина, яка абсолютно означає, що вона повинна працювати так, або щось було б абсолютно неможливо?


18
Які переваги для альтернативи?
Джон Скіт

19
Це правда, що він не дуже моделює реальний світ. Зрештою, тільки те, що мій автомобіль може повідомити, скільки газу залишилось, не означає, що мій автомобіль повинен мати можливість сказати, скільки газу КОЖНЕ автомобіля залишило. Отже, так, я теоретично можу побачити доступ до "приватного примірника". Я відчуваю, що я б ніколи цього не використовував, хоча тому, що переживаю, що в 99% випадків я дійсно хочу отримати інший екземпляр доступу до цієї змінної.
Аквінські

2
@Jon Позиція, яку прийняли за так звані "переваги", полягає в тому, що класи не повинні знати внутрішній стан іншого екземпляра - незалежно від того, що це той самий тип. Не користь за скажімо, але, мабуть, є досить
вагомий

4
Ви завжди можете перетворити змінну в пару зачинень getter / setter, ініціалізованих у конструкторі, які вийдуть з ладу, якщо це не буде таким, як це було під час ініціалізації ...
Martin Sojka

8
Scala надає приватні члени екземплярів - поряд з багатьма іншими рівнями доступу, які не зустрічаються на Java, C # та багатьох інших мовах. Це ідеально законний питання.
Бруно Рейс

Відповіді:


73

Я думаю, що одна з причин, що це працює саме так, полягає в тому, що модифікатори доступу працюють під час компіляції . Отже, визначення того, чи є даний об’єкт також поточним об'єктом, зробити непросто. Наприклад, врахуйте цей код:

public class Foo
{
    private int bar;

    public void Baz(Foo other)
    {
        other.bar = 2;
    }

    public void Boo()
    {
        Baz(this);
    }
}

Чи може компілятор обов'язково з'ясувати, що otherце насправді this? Не у всіх випадках. Можна стверджувати, що це просто не слід компілювати, але це означає, що у нас є кодовий шлях, коли член приватного примірника правильного екземпляра недоступний, що, я думаю, ще гірше.

Лише необхідність видимості на рівні типу, а не видимості на рівні об'єкта гарантує, що проблема відслідковується, а також створює ситуацію, яка здається, що вона має працювати справді .

EDIT : точка Danilel Hilgarth, що це міркування в зворотному напрямку має сенс. Мовні дизайнери можуть створити потрібну мову, і автори-компілятори повинні відповідати їй. Незважаючи на це, мовні дизайнери мають певний стимул, щоб полегшити авторам-упорядникам виконувати свою роботу. (Хоча в цьому випадку досить просто стверджувати, що приватні члени можуть бути доступними лише через this(неявно або явно)).

Однак я вважаю, що це робить проблему більш заплутаною, ніж повинна бути. Більшість користувачів (включаючи мене) вважають це необмеженим обмеженням, якщо вищезгаданий код не працював: зрештою, це мої дані, до яких я намагаюся отримати доступ! Чому я повинен пройти this?

Коротше кажучи, я думаю, що, можливо, я завищив випадок, оскільки це було "складно" для компілятора. Те, що я насправді мав намір перетнути, - це те, що вище ситуація здається такою, над якою дизайнери хотіли б працювати.


33
ІМХО, це міркування - це неправильний шлях. Компілятор виконує правила мови. Це не робить їх. Іншими словами, якби приватні члени були "приватними екземплярами", а не "типовими приватними", компілятор був би реалізований іншими способами, наприклад, лише дозволяючи, this.bar = 2але ні other.bar = 2, тому що це otherможе бути інший екземпляр.
Даніель Гілгарт

@Daniel Це хороший момент, але, як я вже згадував, я думаю, що обмеження було б гірше, ніж видимість на рівні класу.
день

3
@dlev: Я нічого не говорив про те, чи я вважаю, що "члени приватного екземпляра" - це добре. :-) Я просто хотів зазначити, що ви стверджуєте, що технічна реалізація диктує правила мови і що, на мою думку, ця аргументація не є правильною.
Даніель Гілгарт

2
@Daniel Point взятий. Я все ще думаю, що це є вагомим питанням, хоча ви, звичайно, правильні, що в кінцевому підсумку дизайнери мови з'ясовують, чого хочуть, і автори-компілятори відповідають цьому.
двр

2
Я не думаю, що аргумент компілятора справедливий. Навколо них існують мови, які дозволяють використовувати лише приватні екземпляри (наприклад, Ruby), які дозволяють використовувати приватні та приватні екземпляри на вибір (наприклад, Ейфель). Генерація компіляторів не була необхідною для цих мов важче чи простіше. Для отримання додаткової інформації дивіться також мою відповідь внизу.
Авель

61

Тому що мета інкапсуляції, що використовується в C # і подібних мовах *, полягає в зниженні взаємної залежності різних фрагментів коду (класи в C # і Java), а не різних об'єктів в пам'яті.

Наприклад, якщо ви пишете код в одному класі, який використовує деякі поля в іншому класі, то ці класи дуже щільно з'єднані. Однак якщо ви маєте справу з кодом, у якому у вас два об'єкти одного класу, додаткової залежності немає. Клас завжди залежить від себе.

Однак уся ця теорія про інкапсуляцію провалюється, як тільки хтось створює властивості (або отримує / встановлює пари в Java) і відкриває всі поля безпосередньо, що робить класи сполученими, як ніби вони отримують доступ до полів.

* Для роз'яснення видів інкапсуляції див відмінну відповідь Авеля.


3
Я не думаю, що це не вдається, коли get/setметоди будуть надані. Методи аксесуара все ще підтримують абстракцію (користувачеві не потрібно знати, що за ним знаходиться поле, або які поля можуть стояти за властивістю). Наприклад, якщо ми пишемо Complexклас, ми можемо розкрити властивості отримати / встановити полярні координати, а інший get / set для декартових координат. Який клас використовується під ним, користувачеві невідомо (це може бути зовсім інше).
Кен Уейн ВандерЛінде

5
@Ken: Це не вдається, якщо ви надаєте властивість для кожного вашого поля, навіть не враховуючи, чи слід його виставляти.
Горан Йович

@Goran Хоча я погоджуюся, що це не було б ідеально, воно все одно не виходить з ладу, оскільки аксесуари get / set дозволяють вам додати додаткову логіку, якщо, наприклад, змінюються основні поля.
ForbesLindesay

1
"Тому що мета інкапсуляції - зменшити взаємну залежність різних фрагментів коду" >> ні. Інкапсуляція також може означати інкапсуляцію екземплярів, це залежить від мови або школи думки. Один з найбільш остаточних голосів OO, Бертран Мейєр, вважає це навіть дефолтом private. Ви можете перевірити мою відповідь на мій погляд на речі, якщо вам подобається;).
Авель

@Abel: Свою відповідь я базував на мовах з класом інкапсуляції, тому що ОП запитав про одну з них, і відверто кажучи, тому що я їх знаю. Дякуємо за виправлення та нову цікаву інформацію!
Горан Йович

49

До цієї цікавої нитки вже додано кілька відповідей, однак я не зовсім знайшов справжню причину, чому така поведінка є такою, якою вона є. Дозвольте спробувати:

Ще в дні

Десь між Smalltalk в 80-х і Java в середині 90-х років визріла концепція орієнтації на об'єкти. Інформація про приховування, спочатку не розглядалася як концепція, доступна лише для OO (згадана спочатку в 1978 р.), Була введена в Smalltalk, оскільки всі дані (поля) класу є приватними, усі методи є загальнодоступними. Під час багатьох нових розробок ОО в 90-х Бертран Мейєр намагався формалізувати значну частину концепцій ОО у своїй знаковій книзі " Об'єктно-орієнтована побудова програмного забезпечення" (OOSC), яка з тих пір вважається (майже) остаточним посиланням на концепції OO та мовний дизайн .

У випадку приватної видимості

За Мейєром, метод повинен бути доступний для визначеного набору класів (стор. 192-193). Це, очевидно, дає дуже високу деталізацію прихованості інформації, наступна особливість доступна класамA та classB та всім їх нащадкам:

feature {classA, classB}
   methodName

У разі privateвін говорить наступне: без явного оголошення типу видимим для власного класу, ви не можете отримати доступ до цієї функції (методу / поля) в кваліфікованому виклику. Тобто, якщо xце змінна, x.doSomething()не дозволена. Безумовно, доступ дозволений, звичайно, всередині самого класу.

Іншими словами: щоб дозволити доступ екземпляру того ж класу, ви повинні дозволити методу доступ до цього класу явно. Іноді це називається екземпляр-приватний проти клас-приватний.

Примірник-приватний у мовах програмування

Я знаю щонайменше дві мови, які в даний час використовуються, які використовують приховування приватної інформації, на відміну від приховування приватно-класової інформації. Один - Ейфелева, мова, розроблена Мейєром, яка підводить ОО до найбільшої межі. Інший - Рубі, набагато більш поширена мова в наш час. У Ruby privateозначає: "приватний для цього екземпляра" .

Вибір мовного дизайну

Було висловлено припущення, що дозвіл екземпляра-приватного буде важким для компілятора. Я не думаю, що так просто, як просто дозволити або заборонити кваліфіковані дзвінки до методів. Якщо для приватного методу doSomething()дозволено, а x.doSomething()це не так, мовний конструктор ефективно визначив доступність для приватних методів та полів, доступних лише для примірників.

З технічної точки зору, немає жодної причини вибирати той чи інший спосіб (наприклад, якщо врахувати, що Eiffel.NET може це зробити з IL, навіть при багатократному успадкуванні, немає властивої причини не надавати цю функцію).

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

Чому C # дозволяє лише інкапсуляцію класу, а не інкапсуляцію примірника

Якщо ви дивитесь на Інтернет-потоки щодо інкапсуляції екземплярів (термін, який іноді використовується для позначення того, що мова визначає модифікатори доступу на рівні екземпляра, на відміну від рівня класу), ця концепція часто насувається. Однак, враховуючи, що деякі сучасні мови використовують інкапсуляцію екземплярів, принаймні для модифікатора приватного доступу, змушують вас думати, що це може бути і є корисним у сучасному світі програмування.

Однак C #, правда кажучи, найважче виглядав на C ++ та Java за його мовний дизайн. У той час як Ейфель і Модула-3 також були на знімку, враховуючи безліч особливостей відсутності Ейфеля (багаторазове успадкування), я вважаю, що вони вибрали той самий маршрут, що і Java та C ++, коли мова зайшла про модифікатор приватного доступу.

Якщо ви дійсно хочете дізнатись, чому вам слід спробувати домогтися Еріка Ліпперта, Кшиштофа Кваліна, Андерса Хейльсберга чи будь-кого іншого, хто працював за стандартом C #. На жаль, я не зміг знайти остаточну замітку в анотованій мові програмування C # .


1
Це прекрасна відповідь з великою кількістю досвіду, дуже цікава, дякую, що
знайшли

1
@RichK: ласкаво просимо, я просто спіймав питання занадто пізно, але я вважаю, що тема досить інтригуюча, щоб відповісти з деякою глибиною :)
Abel

Під COM, "приватний" означав "екземпляр-приватний". Я думаю, це було певним чином, тому що визначення класу Fooефективно представляло інтерфейс та клас реалізації; оскільки COM не мала поняття посилання на об'єкт класу - просто посилання на інтерфейс - не було можливості класу містити посилання на щось, що гарантовано було б іншим екземпляром цього класу. Цей інтерфейс на основі інтерфейсу зробив деякі речі громіздкими, але це означало, що можна створити клас, який можна замінити іншим, без того, щоб мати спільний доступ до будь-якого з тих же внутрішніх.
supercat

2
Чудова відповідь. ОП має розглянути питання про позначення цього питання як відповідь.
Гевін Осборн

18

Це лише моя думка, але прагматично я вважаю, що якщо програміст має доступ до джерела класу, ви можете з розумом довіряти їм доступ до приватних членів екземпляра класу. Навіщо зв’язувати програмістів правою рукою, коли в лівій частині ви вже дали їм ключі від королівства?


13
Я не розумію цього аргументу. Скажімо, ви працюєте над програмою, в якій ви єдиний розробник, і ви єдиний, хто коли-небудь використовуватиме ваші бібліотеки, чи говорите ви, що в такому випадку ви робите КОЖНИЙ член публічним, оскільки у вас є доступ до вихідного коду?
Аквінський

@aquinas: це зовсім інакше. Оголошення всього публічного призведе до жахливої ​​практики програмування. Дозвіл типу бачити приватні поля інших примірників себе - це дуже вузький випадок.
Ігбі Ларгеман

2
Ні, я не виступаю за оприлюднення кожного члена. Небеса ні! Мій аргумент полягає в тому, що якщо ви вже в джерелі, можете бачити приватних членів, як вони використовуються, використовуються конвенції тощо, то чому б вам не вдатися використовувати ці знання?
FishBasketGordo

7
Я думаю, що спосіб мислення все ще знаходиться в плані типу. Якщо типу не можна довіряти належним чином поводитися з членами типу, що робити? ( Зазвичай це був програміст, який фактично контролює взаємодію, але в цей вік інструментів для генерації коду це може бути не завжди.)
Ентоні Пеграм

3
Моє запитання насправді не довіра, а більше необхідність. Довіра абсолютно не має значення, коли ти можеш використовувати рефлексію до грізних речей приватним особам. Мені цікаво, чому, концептуально, ви можете отримати доступ до приватних осіб. Я припускав, що ситуація є кращою, а не найкращою практикою.
RichK

13

Причина справді - перевірка рівності, порівняння, клонування, перевантаження оператора ... Було б дуже складно реалізувати, наприклад, оператор + на складних числах.


3
Але все, що ви згадуєте, вимагає одного типу Отримати значення іншого типу. Я на борту кажу, що ви хочете оглянути мою приватну державу? Добре, іди вперед. Ви хочете встановити мою приватну державу? Ні в якому разі, друже, змінити свій власний проклятий стан. :)
aquinas

2
@aquinas Це не працює так. Поля - поля. Вони readonlyдоступні лише для читання, якщо вони оголошені як і тоді, що стосується і декларуючого екземпляра. Очевидно, ви стверджуєте, що має бути певне правило компілятора, яке забороняє це, але кожне таке правило є особливістю, яку команда C # повинна проаналізувати, документувати, локалізувати, протестувати, підтримувати та підтримувати. Якщо немає переконливої вигоди, то чому б вони це робили?
Aaronaught

Я погоджуюся з @Aaronaught: є вартість, пов’язана з впровадженням кожної нової специфікації та зміною компілятора. Для заданого сценарію розглянемо метод Clone. Звичайно, ви можете реалізувати це з приватним конструктором, але, можливо, у деяких сценаріях це неможливо / доцільно.
Лоренцо Дематте

1
C # команда? Я просто обговорюю можливі теоретичні зміни мови в будь-якій мові. "Якщо немає переконливої ​​вигоди ..." Я думаю, що може бути користь. Як і будь-яка гарантія компілятора, комусь може здатися корисним гарантувати, що клас змінює лише власний стан.
aquinas

@aquinas: Особливості будуть реалізовані , тому що вони є корисними для багатьох або більшості користувачів - не тому , що вони могли б бути корисні в деяких ізольованих випадках.
Aaronaught

9

Перш за все, що буде з приватними статичними членами? До них можна отримати доступ лише статичними методами? Ви, звичайно, не хотіли б цього, тому що тоді ви не зможете отримати доступ до своїх consts.

Що стосується вашого явного питання, розгляньте випадок StringBuilder, який реалізується як пов'язаний список самих примірників:

public class StringBuilder
{
    private string chunk;
    private StringBuilder nextChunk;
}

Якщо ви не можете отримати доступ до приватних членів інших примірників вашого власного класу, вам доведеться реалізувати ToStringтак:

public override string ToString()
{
    return chunk + nextChunk.ToString();
}

Це буде працювати, але це O (n ^ 2) - не дуже ефективно. Насправді це, мабуть, перемагає всю мету - мати StringBuilderклас в першу чергу. Якщо ви можете отримати доступ до приватних членів інших примірників вашого власного класу, ви можете реалізувати ToString, створивши рядок потрібної довжини, а потім зробивши небезпечну копію кожного фрагмента на відповідному місці в рядку:

public override string ToString()
{
    string ret = string.FastAllocateString(Length);
    StringBuilder next = this;

    unsafe
    {
        fixed (char *dest = ret)
            while (next != null)
            {
                fixed (char *src = next.chunk)
                    string.wstrcpy(dest, src, next.chunk.Length);
                next = next.nextChunk;
            }
    }
    return ret;
}

Ця реалізація є O (n), що робить її дуже швидкою і можливою, лише якщо у вас є доступ до приватних членів інших інстанцій вашого класу .


Так, але хіба немає інших способів навколо цього, наприклад, викриття деяких полів як внутрішніх?
MikeKulls

2
@Mike: Розкриваючи поле, оскільки internalвоно стає менш приватним! Ви, в кінцевому підсумку, розкриєте інтернати свого класу всьому, що є в його складі.
Гейб

3

Це цілком законно для багатьох мов (C ++ для однієї). Модифікатори доступу виходять з принципу інкапсуляції в OOP. Ідея полягає у обмеженні доступу назовні , в даному випадку зовні - це інші класи. Наприклад, будь-який вкладений клас у C # може отримати доступ до приватних членів батьків.

Хоча це вибір дизайнера для мовного дизайнера. Обмеження цього доступу може надзвичайно ускладнити деякі дуже поширені сценарії, не сприяючи сильній ізоляції суб'єктів.

Існує аналогічне обговорення тут


1

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

Але в реальній практиці я сумніваюся, що це дійсно було б корисно. Як ви зазначали, наша звичайна приватність є корисною для таких речей, як перевірка рівності, а також для більшості інших операцій, що включають кілька екземплярів типу. Хоча мені також подобається ваша думка щодо підтримки абстрагування даних, оскільки це є важливим моментом в ООП.

Я думаю, що все навколо, надання можливості обмежувати доступ таким чином, може стати гарною особливістю додати до OOP. Чи справді це корисно? Я б сказав, що ні, оскільки клас повинен мати можливість довіряти своєму коду. Оскільки цей клас - це єдине, що може отримати доступ до приватних членів, немає реальної причини потребувати абстрагування даних під час роботи з екземпляром іншого класу.

Звичайно, ви завжди можете писати свій код так, ніби приватний застосовується до екземплярів. Використовуйте звичайні get/setметоди для доступу / зміни даних. Це, ймовірно, зробить код більш керованим, якщо клас може зазнати внутрішніх змін.


0

Чудові відповіді, наведені вище. Я додам, що частиною цієї проблеми є той факт, що інстанціювати клас всередині себе навіть дозволено в першу чергу. Наприклад, у рекурсивній логіці циклу "for", наприклад, використовується такий тип трюку, доки у вас є логіка для припинення рекурсії. Але екземпляр або передача одного класу всередині себе, не створюючи таких циклів, логічно створює власні небезпеки, навіть незважаючи на те, що загальноприйнята парадигма програмування. Наприклад, клас C # може створювати копію себе в конструкторі за замовчуванням, але це не порушує жодних правил і не створює причинних циклів. Чому?

До речі .... ця сама проблема стосується і "захищених" членів. :(

Я ніколи не прийняв цю парадигму програмування повністю, тому що вона все ще постає з цілим набором питань та ризиків, які більшість програмістів не повністю розуміють, поки такі проблеми, як ця проблема, не зникнуть і не збивають з пантелику людей і не вирішать усіх причин наявності приватних членів.

Цей "дивний і химерний" аспект C # - це ще одна причина, чому хороше програмування не має нічого спільного з досвідом та вмінням, а просто знанням хитрощів та пасток ..... як робота над машиною. Це аргумент того, що правила мали бути порушені, що є дуже поганою моделлю для будь-якої мови обчислень.


-1

Мені здається, що якби дані були приватними для інших екземплярів одного типу, вони не обов'язково були б уже того самого типу. Здавалося б, поводитись чи діяти так само, як інші випадки. Поведінку можна легко змінити на основі приватних внутрішніх даних. Це просто поширило біду на мою думку.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.