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

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

public class Foo
{
    private bool aBool;

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

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

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

Ответ 1

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

public class Foo
{
    private int bar;

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

Назад в дни

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

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

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

feature {classA, classB}
   methodName

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

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

Экземпляр-частный язык программирования

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

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

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

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

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

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

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

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

Что касается вашего явного вопроса, рассмотрим случай 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), что делает ее очень быстрой и возможно только в том случае, если у вас есть доступ к закрытым членам других экземпляров вашего класса.

Ответ 7

Это совершенно законно на многих языках (С++ для одного). Модификаторы доступа исходят из принципа инкапсуляции в ООП. Идея состоит в том, чтобы ограничить доступ к вне, в этом случае внешний класс - это другие классы. Любой вложенный класс в С#, например, может также получить к нему доступ к родителям.

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

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

Ответ 8

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

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

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

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

Ответ 9

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

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

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

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

Ответ 10

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

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