Многие из самых популярных языков программирования (такие как C ++, Java, Python и т. Д.) Имеют концепцию скрытия / теневого копирования переменных или функций. Когда я сталкивался с сокрытием или затенением, они стали причиной трудностей при поиске ошибок, и я никогда не видел случая, когда я считал необходимым использовать эти возможности языков.
Мне было бы лучше запретить прятаться и скрывать.
Кто-нибудь знает о хорошем использовании этих понятий?
Обновление:
я не ссылаюсь на инкапсуляцию членов класса (частных / защищенных членов).
Ответы:
Если вы не разрешаете скрывать и скрывать, то у вас есть язык, на котором все переменные являются глобальными.
Это явно хуже, чем разрешать локальные переменные или функции, которые могут скрывать глобальные переменные или функции.
Если вы не разрешаете скрывать и скрывать, И вы пытаетесь «защитить» определенные глобальные переменные, вы создаете ситуацию, когда компилятор говорит программисту: «Извините, Дейв, но вы не можете использовать это имя, оно уже используется «. Опыт работы с COBOL показывает, что программисты практически сразу прибегают к ненормативной лексике в этой ситуации.
Фундаментальная проблема не в сокрытии / теневом копировании, а в глобальных переменных.
источник
Использование точных описательных идентификаторов всегда полезно.
Я могу утверждать, что скрытие переменных не вызывает много ошибок, так как наличие двух очень похожих имен переменных одного и того же / похожих типов (что бы вы сделали, если бы скрытие переменных было запрещено) может вызвать столько же ошибок и / или серьезные ошибки Я не знаю, верен ли этот аргумент , но он, по крайней мере, правдоподобен.
Использование некоторой венгерской нотации для дифференциации полей по сравнению с локальными переменными позволяет обойти это, но оказывает свое влияние на обслуживание (и здравомыслие программиста).
И (возможно, наиболее вероятно, причина, по которой концепция известна в первую очередь), для языков гораздо проще реализовать скрытие / теневое копирование, чем запретить его. Более простая реализация означает, что компиляторы с меньшей вероятностью будут иметь ошибки. Более простая реализация означает, что компиляторам требуется меньше времени для написания, что приводит к более раннему и широкому внедрению платформы.
источник
Просто чтобы убедиться, что мы находимся на той же странице, метод «скрывается» - это когда производный класс определяет элемент с тем же именем, что и в базовом классе (который, если это метод / свойство, не помечается как виртуальный / переопределяемый ), и когда вызывается из экземпляра производного класса в «производном контексте», используется производный член, в то время как при вызове тем же экземпляром в контексте его базового класса используется член базового класса. Это отличается от абстракции / переопределения членов, когда член базового класса ожидает, что производный класс определит замену, и модификаторов области видимости, которые «скрывают» элемент от потребителей за пределами желаемой области.
Короткий ответ на вопрос, почему это разрешено, заключается в том, что если этого не сделать, разработчики нарушат несколько ключевых принципов объектно-ориентированного проектирования.
Вот более длинный ответ; во-первых, рассмотрим следующую структуру классов в альтернативном юниверсе, где C # не позволяет скрывать элементы:
Мы хотим раскомментировать участника в Bar и тем самым разрешить Bar предоставить другую MyFooString. Однако мы не можем этого сделать, потому что это нарушило бы запрет альтернативной реальности на скрытие членов. Этот конкретный пример будет распространен на ошибки и является ярким примером того, почему вы можете захотеть его запретить; например, какую консольную информацию вы бы получили, если бы сделали следующее?
Вдобавок ко всему, я на самом деле не уверен, что в последней строке вы увидите «Foo» или «Bar». Вы определенно получите «Foo» для первой строки и «Bar» для второй, хотя все три переменные ссылаются на один и тот же экземпляр с абсолютно одинаковым состоянием.
Таким образом, разработчики языка в нашей альтернативной вселенной препятствуют этому явно плохому коду, предотвращая скрытие свойств. Теперь вам, как программисту, действительно необходимо сделать именно это. Как вы обходите ограничения? Ну, один из способов - назвать собственность Бара по-другому:
Совершенно законно, но это не то поведение, которое мы хотим. Экземпляр Bar всегда будет выдавать «Foo» для свойства MyFooString, когда мы хотели, чтобы он производил «Bar». Мы должны не только знать, что наш IFoo - это конкретно Bar, мы также должны знать, как использовать другой метод доступа.
Мы также могли бы, совершенно правдоподобно, забыть отношения родитель-потомок и напрямую реализовать интерфейс:
Для этого простого примера это идеальный ответ, если вам важно, чтобы Foo и Bar были IFoo. Код использования, приведенный в нескольких примерах, не скомпилируется, потому что Bar не является Foo и не может быть назначен как таковой. Однако, если у Foo есть какой-то полезный метод "FooMethod", который нужен Bar, теперь вы не можете наследовать этот метод; вам придется либо клонировать его код в Bar, либо проявить творческий подход:
Это очевидный взлом, и хотя некоторые реализации языковых спецификаций ОО составляют чуть больше этого, концептуально это неправильно; если потребители Bar должны раскрыть функциональность Foo, Bar должен быть Foo, а не Foo.
Очевидно, что если мы контролируем Foo, мы можем сделать его виртуальным, а затем переопределить его. Это концептуальная лучшая практика в нашем текущем юниверсе, когда ожидается, что член будет переопределен и будет применяться в любом альтернативном юниверсе, который не позволяет скрывать:
Проблема заключается в том, что доступ к виртуальному члену изнутри является относительно более дорогостоящим, и поэтому обычно вы хотите делать это только тогда, когда это необходимо. Однако отсутствие скрытия заставляет вас пессимистично относиться к членам, которые другой кодер, который не контролирует ваш исходный код, может захотеть переопределить; «Лучшая практика» для любого незапечатанного класса - делать все виртуальным, если только вы сами этого не хотели. Это также все еще не дает вам точное поведение сокрытия; строка всегда будет "Bar", если экземпляр является Bar. Иногда действительно полезно использовать слои скрытых данных о состоянии, основываясь на уровне наследования, на котором вы работаете.
Таким образом, разрешение скрывать членов является меньшим из этих зол. Отсутствие этого, как правило, ведет к худшим злодеяниям, совершаемым против объектно-ориентированных принципов, чем допускает это.
источник
IEnumerable
andIEnumerable<T>
, описанный в блоге Эрика Либберта на эту тему.Честно говоря, Эрик Липперт, главный разработчик команды компиляторов C #, объясняет это довольно хорошо (спасибо Lescai Ionel за ссылку). .NET
IEnumerable
иIEnumerable<T>
интерфейсы являются хорошими примерами того, когда скрытие членов полезно.В первые дни .NET у нас не было дженериков. Итак,
IEnumerable
интерфейс выглядел так:Этот интерфейс позволил нам
foreach
перебрать коллекцию объектов, однако нам пришлось привести все эти объекты, чтобы правильно их использовать.Затем появились дженерики. Когда мы получили дженерики, мы также получили новый интерфейс:
Теперь нам не нужно создавать объекты, пока мы их перебираем! Woot! Теперь, если скрытие членов запрещено, интерфейс должен выглядеть примерно так:
Это было бы глупо, потому что
GetEnumerator()
иGetEnumeratorGeneric()
в обоих случаях делать почти точно в одно и то же , но они имеют немного разные возвращаемые значения. На самом деле они настолько похожи, что вы почти всегда хотите использовать стандартную форму по умолчаниюGetEnumerator
, если только вы не работаете с устаревшим кодом, который был написан до того , как обобщенные версии были введены в .NET.Иногда член скрытие делает , чтобы больше места для противного кода и труднодоступных найти ошибки. Однако иногда это полезно, например, когда вы хотите изменить тип возврата, не нарушая устаревший код. Это лишь одно из тех решений, которые должны принять разработчики языка: доставляем ли мы неудобство разработчикам, которые законно нуждаются в этой функции, и оставляем ее без внимания, или мы включаем эту функцию в язык и привлекаем внимание тех, кто стал жертвой ее неправильного использования?
источник
IEnumerable<T>.GetEnumerator()
скрываетIEnumerable.GetEnumerator()
, это только потому, что C # не имеет ковариантных возвращаемых типов при переопределении. По логике это переопределение, полностью соответствующее LSP. Скрытие - это когдаmap
в функции есть локальная переменная в файлеusing namespace std
(в C ++).Ваш вопрос может быть прочитан двумя способами: либо вы спрашиваете об области действия переменной / функции в целом, либо задаете более конкретный вопрос о области действия в иерархии наследования. Вы не упомянули о наследовании конкретно, но вы упомянули, что трудно найти ошибки, которые больше похожи на область действия в контексте наследования, чем на простую область видимости, поэтому я отвечу на оба вопроса.
Область применения в целом является хорошей идеей, поскольку она позволяет нам сосредоточить наше внимание на одной конкретной (мы надеемся, небольшой) части программы. Поскольку это позволяет локальным именам всегда побеждать, если вы читаете только ту часть программы, которая находится в заданной области, то вы точно знаете, какие части были определены локально, а какие были определены в другом месте. Либо имя относится к чему-то локальному, в этом случае код, который определяет его, находится прямо перед вами, или это ссылка на что-то за пределами локальной области видимости. Если нет никаких нелокальных ссылок, которые могли бы измениться из-под нас (особенно глобальные переменные, которые могли бы быть изменены из любого места), тогда мы можем оценить, является ли часть программы в локальной области действия правильной или нет, не обращаясь к ней. к любой части остальной программы вообще .
Это может иногда приводить к нескольким ошибкам, но это более чем компенсирует, предотвращая огромное количество возможных ошибок. Кроме локального определения с тем же именем, что и у библиотечной функции (не делайте этого), я не вижу простого способа введения ошибок с локальной областью, но локальная область - это то, что позволяет многим частям одной и той же программы использовать Я в качестве счетчика индекса для цикла, не забивая друг друга, и позволяет Фреду в зале написать функцию, которая использует строку с именем str, которая не будет загромождать вашу строку с тем же именем.
Я нашел интересную статью Бертрана Мейера, в которой обсуждается перегрузка в контексте наследования. Он приводит интересное различие между тем, что он называет синтаксической перегрузкой (имеется в виду, что есть две разные вещи с одинаковым именем), и семантической перегрузкой (что означает, что есть две разные реализации одной и той же абстрактной идеи). Семантическая перегрузка была бы хороша, поскольку вы хотели реализовать ее в подклассе иначе; Синтаксическая перегрузка будет случайным столкновением имен, которое вызвало ошибку.
Разница между перегрузкой в ситуации наследования, которая предназначена и которая является ошибкой, заключается в семантике (смысле), поэтому у компилятора нет возможности узнать, правильно ли вы сделали или нет. В простой ситуации правильный ответ всегда локальный, поэтому компилятор может выяснить, что является правильным.
Бертран Мейер предложил бы использовать такой язык, как Eiffel, который не допускает столкновения имен, подобные этому, и вынуждает программиста переименовывать одно или оба, тем самым полностью избегая проблемы. Мое предложение состоит в том, чтобы избегать использования наследования полностью, а также полностью избегать проблемы. Если вы не можете или не хотите делать что-либо из этого, есть еще способы уменьшить вероятность возникновения проблемы с наследованием: следуйте LSP (принцип подстановки Лискова), предпочитайте композицию над наследованием, сохраняйте ваша иерархия наследования мала, и классы в иерархии наследования остаются небольшими. Кроме того, некоторые языки могут выдавать предупреждение, даже если они не будут выдавать ошибку, как это сделал бы язык, подобный Eiffel.
источник
Вот мои два цента.
Программы могут быть структурированы в блоки (функции, процедуры), которые являются самостоятельными единицами программной логики. Каждый блок может ссылаться на «вещи» (переменные, функции, процедуры), используя имена / идентификаторы. Это отображение имен в вещи называется связыванием .
Имена, используемые блоком, делятся на три категории:
Рассмотрим, например, следующую программу на C
Функция
print_double_int
имеет локальное имя (локальная переменная)d
и аргументn
и использует внешнее глобальное имяprintf
, которое находится в области видимости, но не определяется локально.Обратите внимание, что это
printf
также может быть передано в качестве аргумента:Обычно аргумент используется для указания параметров ввода / вывода функции (процедуры, блока), тогда как глобальные имена используются для ссылки на такие вещи, как библиотечные функции, которые «существуют в среде», и поэтому удобнее упоминать их только когда они нужны. Использование аргументов вместо глобальных имен является основной идеей внедрения зависимостей , которая используется, когда зависимости должны быть явными, а не разрешаться путем просмотра контекста.
Другое аналогичное использование внешне определенных имен можно найти в замыканиях. В этом случае имя, определенное в лексическом контексте блока, может использоваться внутри блока, и значение, связанное с этим именем, будет (обычно) продолжать существовать до тех пор, пока блок ссылается на него.
Взять, к примеру, этот код Scala:
Возвращаемым значением функции
createMultiplier
является замыкание(m: Int) => m * n
, которое содержит аргументm
и внешнее имяn
. Имяn
определяется путем просмотра контекста, в котором определяется замыкание: имя связано с аргументомn
функцииcreateMultiplier
. Обратите внимание, что эта привязка создается при создании замыкания, т.createMultiplier
Е. Когда вызывается. Таким образом, имяn
привязано к фактическому значению аргумента для конкретного вызова функции. Сравните это со случаем подобной библиотечной функцииprintf
, которая разрешается компоновщиком при сборке исполняемого файла программы.Подводя итог, может быть полезно ссылаться на внешние имена внутри локального блока кода, чтобы вы
Затенение приходит, когда вы считаете, что в блоке вас интересуют только соответствующие имена, которые определены в среде, например, в
printf
функции, которую вы хотите использовать. Если случайно вы хотите использовать локальное имя (getc
,putc
,scanf
...) , который уже используется в среде, вы просто хотите игнорировать (тень) глобальное имя. Поэтому, думая локально, вы не хотите рассматривать весь (возможно, очень большой) контекст.В другом направлении, думая глобально, вы хотите игнорировать внутренние детали локального контекста (инкапсуляция). Поэтому вам нужно затенение, иначе добавление глобального имени может сломать каждый локальный блок, который уже использовал это имя.
В итоге, если вы хотите, чтобы блок кода ссылался на внешние привязки, вам нужно затенение, чтобы защитить локальные имена от глобальных.
источник