Почему языки программирования позволяют скрывать /скрывать переменные и функции?

Многие из самых популярных программных languges (таких как C ++, Java, Python и т. д.) имеют концепцию скрытия / теневого копирования переменных или функций. Когда я столкнулся с укрытием или затенением, они стали причиной трудностей поиска ошибок, и я никогда не видел случая, когда я счел необходимым использовать эти возможности языков.

Мне казалось бы лучше запретить скрывать и затенять.

Кто-нибудь знает о хорошем использовании этих понятий?

Update:
Я не имею в виду инкапсуляцию членов класса (частные /защищенные члены).

31 голос | спросил Simon 21 +04002013-10-21T16:04:21+04:00312013bEurope/MoscowMon, 21 Oct 2013 16:04:21 +0400 2013, 16:04:21

6 ответов


26

Если вы запретите скрывать и затенять, то у вас есть язык, на котором все переменные глобальны.

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

Если вы запретили скрывать и скрывать, AND вы пытаетесь «защитить» определенные глобальные переменные, вы создаете ситуацию, когда компилятор сообщает программисту: «Прости, Дэйв, но ты можешь», t используйте это имя, оно уже используется ». Опыт работы с COBOL показывает, что программисты почти сразу прибегают к ненормативной лексике в этой ситуации.

Основная проблема заключается не в скрытии /тенировании, а в глобальных переменных.

ответил John R. Strohm 21 +04002013-10-21T17:11:05+04:00312013bEurope/MoscowMon, 21 Oct 2013 17:11:05 +0400 2013, 17:11:05
14
  

Кто-нибудь знает о хорошем использовании этих понятий?

Использование точных описательных идентификаторов всегда полезно.

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

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

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

ответил Telastyn 21 +04002013-10-21T17:18:13+04:00312013bEurope/MoscowMon, 21 Oct 2013 17:18:13 +0400 2013, 17:18:13
7

Чтобы убедиться, что мы находимся на одной странице, метод «hiding» - это когда производный класс определяет член с тем же именем, что и в базовом классе (который, если это метод /свойство, не отмечен virtual /overridable), а при вызове из экземпляра производного класса в «производном контексте» используется производный член, а если он используется одним и тем же экземпляром в контексте его базового класса, используется элемент базового класса. Это отличается от абстракции /переопределения элементов, где член базового класса ожидает, что производный класс определит замену, и из модификаторов видимости видимости /видимости, которые «скрывают» участника от потребителей за пределами требуемой области.

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

Вот длинный ответ; во-первых, рассмотрим следующую структуру классов в альтернативном юниверсе, где C # не допускает скрытие элемента:

public interface IFoo
{
   string MyFooString {get;}
   int FooMethod();
}

public class Foo:IFoo
{
   public string MyFooString {get{return "Foo";}}
   public int FooMethod() {//incredibly useful code here};
}

public class Bar:Foo
{
   //public new string MyFooString {get{return "Bar";}}
}

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

Bar myBar = new Bar();
Foo myFoo = myBar;
IFoo myIFoo = myFoo;

Console.WriteLine(myFoo.MyFooString);
Console.WriteLine(myBar.MyFooString);
Console.WriteLine(myIFoo.MyFooString);

Сверху моей головы, я на самом деле не уверен, что вы получите «Foo» или «Bar» на этой последней строке. Вы наверняка получите «Foo» для первой строки и «Bar» для второго, хотя все три переменные ссылаются на один и тот же экземпляр с точно таким же состоянием.

Итак, дизайнеры языка в нашей альтернативной вселенной препятствуют этому явно плохому коду, предотвращая скрытие свойств. Теперь у вас, как у кодера, есть настоящая необходимость делать именно это. Как вы обойдете ограничения? Ну, один из способов - назвать свойство Бар по-разному:

public class Bar:Foo
{
   public string MyBarString {get{return "Bar";}}       
}

Совершенно легально, но это не то поведение, которое мы хотим. Экземпляр Bar всегда будет создавать «Foo» для свойства MyFooString, когда мы хотим, чтобы он создавал «Bar». Мы не только должны знать, что наш IFoo - это, в частности, Bar, мы также должны знать, как использовать другой аксессор.

Мы также вполне могли забыть отношения родитель-потомок и реализовать интерфейс напрямую:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   public int FooMethod() {...}
}

Для этого простого примера это отличный ответ, пока вам все равно, что Foo и Bar являются IFUOS. Код использования пары примеров не смог бы скомпилироваться, потому что панель не является Foo и не может быть назначена как таковая. Однако, если у Foo был полезный метод «FooMethod», который нужен Bar, теперь вы не можете наследовать этот метод; вам придется либо клонировать его код в баре, либо получить объявление:

public class Bar:IFoo
{
   public string MyFooString {get{return "Bar";}}
   private readonly theFoo = new Foo();

   public int FooMethod(){return theFoo.FooMethod();}
}

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

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

public class Foo:IFoo
{
   public virtual string MyFooString {get{return "Foo";}}
   //...
}

public class Bar:Foo
{
   public override string MyFooString {get{return "Bar";}}
}

Проблема с этим заключается в том, что доступ к виртуальному члену под капотом относительно дороже для выполнения, и поэтому вы, как правило, только хотите сделать это, когда вам нужно. Однако отсутствие скрытия заставляет вас пессимистично относиться к членам, которые другой кодер, который не контролирует ваш исходный код, может захотеть переопределить; «наилучшей практикой» для любого непечатаемого класса было бы сделать все виртуальным, если вы специально этого не хотели. Он также еще не дает вам точного поведения скрытия; Струнавсегда будет «Bar», если экземпляр представляет собой панель. Иногда действительно полезно использовать уровни данных скрытого состояния, основанные на уровне наследования, с которым вы работаете.

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

ответил KeithS 22 +04002013-10-22T01:00:12+04:00312013bEurope/MoscowTue, 22 Oct 2013 01:00:12 +0400 2013, 01:00:12
2

Честно говоря, Эрик Липперт, главный разработчик команды компилятора C #, объясняет это довольно хорошо (спасибо Lescai Ionel за ссылку). Интерфейсы .NET IEnumerable и IEnumerable<T> хороши примеры того, когда сокрытие элемента полезно.

В первые дни .NET у нас не было дженериков. Таким образом, интерфейс IEnumerable выглядел следующим образом:

public interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Этот интерфейс позволяет нам foreach по совокупности объектов, однако мы должны были набросать все эти объекты, чтобы использовать их правильно.

Затем появились дженерики. Когда мы получили дженерики, мы также получили новый интерфейс:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

Теперь нам не нужно бросать объекты, пока мы ищем их через них! Woot! Теперь, если сокрытие элемента не было разрешено, интерфейс должен выглядеть примерно так:

public interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> GetEnumeratorGeneric();
}

Это было бы глупо, потому что GetEnumerator() и GetEnumeratorGeneric() в обоих случаях делают в точности именно те же , но имеют несколько разные значения возврата. На самом деле они настолько похожи, что вы в основном always хотите по умолчанию использовать общую форму GetEnumerator , если вы не работаете с устаревшим кодом, который был написан до того, как дженерики были внедрены в .NET.

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

ответил Phil 22 +04002013-10-22T02:52:51+04:00312013bEurope/MoscowTue, 22 Oct 2013 02:52:51 +0400 2013, 02:52:51
2

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

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

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

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

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

Предложение Бертрана Мейера состояло бы в том, чтобы использовать такой язык, как Eiffel, который не допускает подобных конфликтов имен и заставляет программиста переименовать один или оба, тем самым полностью избегая проблемы. Мое предложение состояло бы в том, чтобы избежать использования наследования целиком, а также полностью избежать проблемы. Если вы не можете или не хотите делать что-либо из этого, есть все, что вы можете сделать, чтобы уменьшить вероятность наличия проблемы с наследованием: следовать LSP (Принцип замещения Лискова), предпочесть состав над наследованием, сохранить ваши иерархии наследования неглубокие и сохраняйте классы в иерархии наследования маленькими. Кроме того, некоторые языки могут выдавать предупреждение, даже если они не выдавали ошибку, так как язык, подобный Эйфелю.

ответил Michael Shaw 22 +04002013-10-22T13:36:47+04:00312013bEurope/MoscowTue, 22 Oct 2013 13:36:47 +0400 2013, 13:36:47
2

Вот мои два цента.

Программы могут быть структурированы в блоки (функции, процедуры), которые являются автономными единицами программной логики. Каждый блок может ссылаться на «вещи» (переменные, функции, процедуры), используя имена /идентификаторы. Это сопоставление от имен к вещам называется привязкой .

Имена, используемые блоком, делятся на три категории:

  1. Локально определенные имена, например. локальные переменные, которые известны только внутри блока.
  2. Аргументы, которые привязаны к значениям при вызове блока и могут использоваться вызывающим пользователем для указания параметра ввода /вывода блока.
  3. Внешние имена /привязки, которые определены в среде, в которой содержится блок, и находятся в области действия внутри блока.

Рассмотрим, например, следующую C-программу

#include<stdio.h>

void print_double_int(int n)
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4);
}

Функция print_double_int имеет локальное имя (локальная переменная) d и аргумент n и использует внешнее глобальное имя printf, который находится в области видимости, но не определен локально.

Обратите внимание, что printf также может быть передан как аргумент:

#include<stdio.h>

void print_double_int(int n, int printf(const char *, ...))
{
  int d = n * 2;

  printf("%d\n", d);
}

int main(int argc, char *argv[])
{
  print_double_int(4, printf);
}

Обычно аргумент используется для указания параметров ввода /вывода функции (процедуры, блока), тогда как глобальные имена используются для обозначения таких функций, как библиотечные функции, которые «существуют в среде», и, следовательно, это более удобно упомянуть их только тогда, когда они необходимы. Использование аргументов вместо глобальных имен является основной идеей инъекции зависимостей , которая используется, когда зависимости должны быть сделаны явными, а не разрешены путем просмотра контекста.

Другое подобное использование имен, определяемых извне, можно найти в закрытии. В этом случае в блоке может использоваться имя, определенное в лексическом контексте блока, и значение, связанное с этим именем, будет (как правило) продолжать существовать до тех пор, пока блок ссылается на него.

Возьмем, к примеру, этот код Scala:

object ClosureExample
{
  def createMultiplier(n: Int) = (m: Int) => m * n

  def main(args: Array[String])
  {
    val multiplier3 = createMultiplier(3)
    val multiplier5 = createMultiplier(5)

    // Prints 6.
    println(multiplier3(2))

    // Prints 10.
    println(multiplier5(2))
  }
}

Возвращаемое значение функции createMultiplier - это замыкание (m: Int) => m * n, который содержит аргумент m и внешнее имя n. Имя n разрешено путем просмотра контекста, в котором определено замыкание: имя привязано к аргументу n функции createMultiplier. Обратите внимание, что это связывание создается при создании замыкания, то есть при вызове createMultiplier. Таким образом, имя n привязано к фактическому значению аргумента для конкретного вызова функции. Сравните это со случаем библиотечной функции, такой как printf, которая разрешается компоновщиком при создании исполняемого файла программы.

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

  • не нужно /хотеть передавать внешние имена явно в качестве аргументов, а
  • вы можете заморозить привязки во время выполнения при создании блока, а затем получить доступ к нему позже при вызове блока.

Затенение приходит, когда вы считаете, что в блоке вас интересуют только релевантные имена, определенные в среде, например. в функции printf, которую вы хотите использовать. Если случайно вы хотите использовать локальное имя (getc, putc, scanf, ...), который уже использовался в среде, вы просто хотите игнорировать (тень ) глобальное имя. Итак, при локальном мышлении вы не хотите рассматривать весь (возможно, очень большой) контекст.

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

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

ответил Giorgio 23 +04002013-10-23T00:02:20+04:00312013bEurope/MoscowWed, 23 Oct 2013 00:02:20 +0400 2013, 00:02:20

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132