Почему закрытые поля закрыты для типа, а не для экземпляра? - PullRequest
149 голосов
/ 08 августа 2011

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

public class Foo
{
    private bool aBool;

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

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

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

Ответы [ 10 ]

72 голосов
/ 08 августа 2011

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

public class Foo
{
    private int bar;

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

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

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

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

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

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

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

59 голосов
/ 08 августа 2011

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

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

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

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

49 голосов
/ 10 августа 2011

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

Назад в дни

Где-то между 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 # .

18 голосов
/ 08 августа 2011

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

13 голосов
/ 08 августа 2011

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

9 голосов
/ 08 августа 2011

Прежде всего, что будет с приватными статическими членами?Доступ к ним возможен только статическими методами?Вы, конечно, не захотите этого, потому что тогда вы не сможете получить доступ к вашим 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, создав строку правильной длины, а затем выполнив небезопасную копию каждого блока в соответствующем месте вstring:

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

3 голосов
/ 08 августа 2011

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

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

Подобное обсуждение здесь

1 голос
/ 08 августа 2011

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

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

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

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

0 голосов
/ 31 августа 2017

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

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

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

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

0 голосов
/ 08 августа 2011

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

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

...