Почему закрытые поля закрыты для типа, а не для экземпляра?

153

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

public class Foo
{
    private bool aBool;

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

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

Лучший аргумент, который мы могли бы выдвинуть, - это проверки на равенство, когда класс может захотеть получить доступ к закрытым полям, чтобы определить равенство с другим экземпляром. Есть ли другие причины? Или какая-то золотая причина, которая абсолютно означает, что она должна работать таким образом, или что-то совершенно невозможное?

RichK
источник
18
Каковы преимущества для альтернативы?
Джон Скит
19
Это правда, что он не очень хорошо моделирует реальный мир. В конце концов, только то, что моя машина может сообщить, сколько бензина осталось, не означает, что моя машина должна быть в состоянии определить, сколько газа осталось в КАЖДОМ автомобиле. Так что, да, я теоретически могу видеть, что у меня есть доступ "частного экземпляра". Я чувствую, что действительно никогда не буду использовать это, хотя, потому что я был бы обеспокоен тем, что в 99% случаев я действительно хотел бы иметь другой экземпляр доступа к этой переменной.
Аквинский
2
@Jon Позиция, которая была принята за так называемые «преимущества», заключается в том, что классы не должны знать внутреннее состояние другого экземпляра - независимо от того, что это тот же тип. Не одно преимущество, скажем так, но, вероятно, есть довольно веская причина выбирать одно из другого, и это то, что мы пытались выяснить
RichK
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? Не во всех случаях. Можно утверждать, что это просто не должно компилироваться, но это означает, что у нас есть путь к коду, где частный экземпляр экземпляра правильного экземпляра недоступен, что, я думаю, еще хуже.

Только требование уровня типа, а не видимости уровня объекта гарантирует, что проблема поддается решению, а также создает ситуацию, которая, как кажется, должна работать, на самом деле работает.

РЕДАКТИРОВАТЬ : точка зрения Данил Хилгарт, что это рассуждение назад, имеет свои достоинства. Разработчики языка могут создать язык, который они хотят, и авторы компилятора должны соответствовать ему. При этом у языковых дизайнеров есть некоторый стимул, чтобы облегчить работу компиляторов. (Хотя в этом случае достаточно легко утверждать, что к закрытым членам можно было получить доступ только через this(неявно или явно)).

Тем не менее, я считаю, что это делает проблему более запутанной, чем она должна быть. Большинство пользователей (включая меня) считают это ненужным ограничением, если вышеуказанный код не работает: в конце концов, это мои данные, к которым я пытаюсь получить доступ! Почему я должен пройти this?

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

dlev
источник
33
ИМХО, это рассуждение неправильное. Компилятор обеспечивает соблюдение правил языка. Это не делает их. Другими словами, если бы закрытые члены были «частным экземпляром», а не «закрытым типом», компилятор был бы реализован другими способами, например, только разрешающим, this.bar = 2но не other.bar = 2потому, что это otherмог бы быть другой экземпляр.
Даниэль Хилгарт
@Daniel Это хороший момент, но, как я уже говорил, я думаю, что это ограничение будет хуже, чем видимость на уровне класса.
dlev
3
@dlev: Я ничего не сказал о том, считаю ли я, что «частные экземпляры» - это хорошая вещь. :-) Я просто хотел отметить, что вы утверждаете, что техническая реализация диктует правила языка и что, на мой взгляд, эта аргументация не верна.
Даниэль Хилгарт
2
@ Даниэль Точка занята. Я все еще думаю, что это обоснованное соображение, хотя вы, конечно, правы, что в конечном итоге разработчики языка выясняют, чего они хотят, и авторы компиляторов соответствуют этому.
dlev
2
Я не думаю, что аргумент компилятора верен. Вокруг есть языки, которые допускают только частный экземпляр (например, Ruby) и которые по своему выбору разрешают частный экземпляр и частный класс (например, Eiffel). Генерация компилятора не была необходима сложнее или проще для этих языков. Для получения дополнительной информации см. Также мой ответ вниз по ветке.
Авель
61

Поскольку цель типа инкапсуляции, используемой в C # и подобных языках *, состоит в том, чтобы уменьшить взаимную зависимость различных частей кода (классов в C # и Java), а не различных объектов в памяти.

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

Однако вся эта теория об инкапсуляции не работает, как только кто-то создает свойства (или получает / устанавливает пары в Java) и выставляет все поля напрямую, что делает классы такими же связанными, как если бы они в любом случае обращались к полям.

* Для уточнения видов инкапсуляции см. Отличный ответ Авеля.

Горан Йович
источник
3
Я не думаю, что это терпит неудачу, как только get/setметоды предоставляются. Методы доступа по-прежнему поддерживают абстракцию (пользователю не нужно знать, что за ним есть поле или какие поля могут быть за свойством). Например, если мы пишем Complexкласс, мы можем выставить свойства, чтобы получить / установить полярные координаты, а другой получить / установить для декартовых координат. Какой класс использует класс, неизвестно пользователю (это может быть что-то совершенно другое).
Кен Уэйн ВандерЛинде
5
@Ken: Это не удастся, если вы предоставите свойство для каждого поля, которое у вас есть, даже не задумываясь о том, должно ли оно быть открыто.
Горан Йович
@ Goran Хотя я согласен, что это не было бы идеально, он все еще не полностью потерпел неудачу, поскольку методы доступа get / set позволяют добавить дополнительную логику, если, например, изменяются базовые поля.
ForbesLindesay
1
«Потому что целью инкапсуляции является снижение взаимозависимости разных частей кода» >> нет. Инкапсуляция также может означать инкапсуляцию экземпляра, это зависит от языка или школы мысли. Бертран Мейер, один из наиболее авторитетных представителей ОО, считает, что это даже по умолчанию private. Вы можете проверить мой ответ на мой взгляд на вещи, если хотите;).
Авель
@Abel: мой ответ основывался на языках с инкапсуляцией классов, потому что OP спросил об одном из них, и, честно говоря, потому что я их знаю. Спасибо за исправление и за новую интересную информацию!
Горан Йович
49

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

В те дни

Где-то между Smalltalk в 80-х и Java в середине 90-х созрела концепция объектной ориентации. Сокрытие информации, изначально не рассматриваемое как концепция, доступная только для ОО (впервые упомянутое в 1978 г.), было введено в Smalltalk, поскольку все данные (поля) класса являются частными, все методы являются открытыми. Во время многих новых разработок ОО в 90-х годах Бертран Мейер попытался формализовать большую часть концепций ОО в своей знаковой книге Построение объектно-ориентированного программного обеспечения (OOSC), которая с тех пор считается (почти) окончательной ссылкой на концепции ОО и проектирование языка. ,

В случае частной видимости

Согласно Мейеру, метод должен быть доступен для определенного набора классов (стр. 192-193). Это, очевидно, дает очень высокую степень детализации сокрытия информации, следующая функция доступна для classA и classB и всех их потомков:

feature {classA, classB}
   methodName

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

Другими словами: чтобы разрешить доступ экземпляру того же класса, вы должны явно разрешить доступ к методу этого класса. Это иногда называют частным экземпляром против частного класса.

Частный экземпляр в языках программирования

Я знаю по крайней мере два языка, которые в настоящее время используются, которые используют скрытие частной информации экземпляра, а не скрытие частной информации класса. Одним из них является Eiffel, язык, разработанный Мейером, который доводит ОО до предела. Другой - Ruby, гораздо более распространенный язык в наши дни. В Ruby privateозначает «личное для данного экземпляра» .

Выбор для языкового дизайна

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

С технической точки зрения, нет причин выбирать тот или иной путь (особенно если учесть, что Eiffel.NET может делать это с IL, даже с множественным наследованием, нет внутренней причины не предоставлять эту функцию).

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

Почему C # допускает только инкапсуляцию классов, а не инкапсуляцию экземпляров

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

Тем не менее, C #, по общему признанию, больше всего смотрел на C ++ и Java из-за своего языкового дизайна. Хотя Eiffel и Modula-3 также были в картине, учитывая, что многие функции Eiffel отсутствуют (множественное наследование), я считаю, что они выбрали тот же маршрут, что и Java и C ++, когда дело дошло до модификатора частного доступа.

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

Абель
источник
1
Это прекрасный ответ с большим количеством предыстории, очень интересный, спасибо, что
нашли
1
@RichK: пожалуйста, только что поймал вопрос слишком поздно, я думаю, но нашел тему достаточно интригующей, чтобы ответить с некоторой глубиной :)
Авель
Под COM «частный» означает «частный экземпляр». Я думаю, что это было в некоторой степени, потому что определение класса Fooэффективно представляло интерфейс и реализующий класс; поскольку в COM не было понятия ссылки на объект класса - просто ссылка на интерфейс - класс не мог содержать ссылку на что-то, что гарантированно является другим экземпляром этого класса. Этот дизайн на основе интерфейса сделал некоторые вещи громоздкими, но это означало, что можно было разработать класс, который мог бы заменить другой, без необходимости иметь какие-либо одинаковые внутренние компоненты.
Суперкат
2
Отличный ответ. ОП должен рассмотреть вопрос об обозначении этого вопроса как ответа.
Гэвин Осборн
18

Это только мое мнение, но прагматично, я думаю, что если у программиста есть доступ к источнику класса, вы можете разумно доверять им доступ к закрытым членам экземпляра класса. Зачем связывать программистов правой рукой, когда слева от них вы уже дали им ключи от королевства?

FishBasketGordo
источник
13
Я не понимаю этот аргумент. Допустим, вы работаете над программой, в которой вы являетесь единственным разработчиком, и вы единственный, кто когда-либо будет использовать ваши библиотеки. Вы говорите, что в этом случае вы КАЖДЫЙ участник становитесь публичным, потому что у вас есть доступ к исходному коду?
Аквинский
@aquinas: это совсем другое. Обнародование всего приведет к ужасным практикам программирования. Разрешение типу видеть закрытые поля других экземпляров самого себя - очень узкий случай.
Игби Крупный человек
2
Нет, я не защищаю публичность каждого члена. Небеса нет! Я утверждаю, что если вы уже находитесь в источнике, можете видеть приватных участников, как они используются, используемые соглашения и т. Д., Почему бы не быть в состоянии использовать эти знания?
FishBasketGordo
7
Я думаю, что способ мышления все еще в терминах типа. Если типу нельзя доверять для правильного обращения с членами типа, что может? ( Обычно это был бы программист, фактически контролирующий взаимодействия, но в наш век инструментов генерации кода это не всегда так.)
Энтони Пеграм,
3
Мой вопрос не о доверии, а о необходимости. Доверие совершенно не имеет значения, когда вы можете использовать отражение для отвратительных вещей для рядовых. Мне интересно, почему, концептуально, вы можете получить доступ к рядовым. Я предположил, что была логическая причина, а не ситуация лучших практик.
RichK
13

Причина действительно в проверке равенства, сравнении, клонировании, перегрузке операторов ... Было бы очень сложно реализовать оператор +, например, на комплексных числах.

Лоренцо Дематте
источник
3
Но все, что вы упоминаете, требует одного типа ПОЛУЧЕНИЕ значения другого типа. Я нахожусь на борту с высказыванием: вы хотите проверить мое личное состояние? Хорошо, давай. Вы хотите установить мое личное состояние? Ни за что, приятель, не меняй свое собственное состояние. :)
Аквинский
2
@aquinas Так не работает. Поля есть поля. Они доступны только для чтения, если объявлены как readonlyи тогда, что также относится и к объявленному экземпляру. Очевидно, вы утверждаете, что должно быть специальное правило компилятора, чтобы запретить это, но каждое такое правило - это функция, которую команда C # должна определять, документировать, локализовывать, тестировать, поддерживать и поддерживать. Если нет убедительных преимуществ, то зачем им это делать?
Ааронаут
Я согласен с @Aaronaught: с реализацией каждой новой спецификации и изменением компилятора связаны затраты. Для заданного сценария рассмотрим метод клонирования. Конечно, вы можете захотеть реализовать это с помощью частного конструктора, но, возможно, в некоторых случаях это невозможно / не рекомендуется.
Лоренцо Дематте
1
C # команда? Я просто обсуждаю возможные теоретические изменения языка на ЛЮБОМ языке. «Если нет убедительной выгоды ...» Я думаю, что может быть и польза. Как и с любой гарантией компилятора, кто-то может счесть полезным гарантировать, что класс изменяет только свое собственное состояние.
Аквинский
@aquinas: Особенности будут реализованы , потому что они являются полезными для многих или большинства пользователей - не потому , что они могли бы быть полезны в некоторых изолированных случаях.
Аарона
9

Прежде всего, что будет с частными статическими членами? Доступ к ним возможен только статическими методами? Вы, конечно, не захотите этого, потому что тогда вы не сможете получить доступ к своим const.

Что касается вашего явного вопроса, рассмотрим случай a 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 ++ для одного). Модификаторы доступа происходят из принципа инкапсуляции в ООП. Идея состоит в том, чтобы ограничить доступ извне , в этом случае извне есть другие классы. Например, любой вложенный класс в C # может получить доступ к частным членам своих родителей.

Пока это выбор дизайна для языкового дизайнера. Ограничение этого доступа может чрезвычайно усложнить некоторые очень распространенные сценарии, не внося большой вклад в изоляцию объектов.

Существует аналогичное обсуждение здесь

Mehran
источник
1

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

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

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

Конечно, вы всегда можете написать свой код, как если бы он был закрытым для экземпляров. Используйте обычные get/setметоды для доступа / изменения данных. Это, вероятно, сделает код более управляемым, если класс будет подвержен внутренним изменениям.

Кен Уэйн ВандерЛинде
источник
0

Отличные ответы, данные выше. Я хотел бы добавить, что частью этой проблемы является тот факт, что создание экземпляра класса внутри себя даже разрешено в первую очередь. Например, в рекурсивной логике цикла «for» он использует трюк такого типа, если у вас есть логика для завершения рекурсии. Но создание или передача одного и того же класса внутри себя без создания таких циклов логически создает свои опасности, хотя это широко распространенная парадигма программирования. Например, класс C # может создавать свою копию в конструкторе по умолчанию, но это не нарушает никаких правил и не создает причинно-следственные связи. Зачем?

Кстати ... эта же проблема относится и к "защищенным" членам. :(

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

Этот «странный и дурацкий» аспект C # - еще одна причина, почему хорошее программирование не имеет ничего общего с опытом и навыками, а просто знает хитрости и ловушки… как работа на автомобиле. Это аргумент, что правила должны были быть нарушены, что является очень плохой моделью для любого вычислительного языка.

Stokely
источник
-1

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

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

С Джонсон
источник