Имеет ли смысл создавать блоки только для уменьшения области видимости переменной?


38

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

//Some code
....

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

...
//Some more code

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

Тем не менее, мне было любопытно, был ли случай, когда это было не так глупо. Единственное, о чем я могу подумать, это если вы хотите повторно использовать общие имена переменных, такие как count, или temp, но хорошие соглашения об именах и короткие методы делают маловероятным, что это будет полезно.

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


13
@gnat .... что? Я даже не знаю, как отредактировать мой вопрос, чтобы он был менее похож на эту двойную цель. Какая часть этого заставила вас поверить, что эти вопросы совпадают?
Лорд Фаркваад

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

4
Есть пара похожих вопросов, которые задают это для C ++: это плохая практика для создания блоков кода? и структурный код с фигурными скобками . Возможно, один из них содержит ответ на этот вопрос, хотя C ++ и Java сильно отличаются в этом отношении.
Амон


4
@gnat Почему этот вопрос вообще открыт? Это одна из самых широких вещей, которые я когда-либо видел. Это попытка канонического вопроса?
jpmc26

Ответы:


34

Сначала поговорим о базовой механике:

В области видимости C ++ == время жизни b / c деструкторы вызываются при выходе из области видимости. Далее, важное различие в C / C ++ мы можем объявить локальными объектами. Во время выполнения для C / C ++ компилятор обычно выделяет кадр стека для метода, который настолько велик, насколько это необходимо, заранее, а не выделяет больше места в стеке при входе в каждую область (которая объявляет переменные). Таким образом, компилятор разрушает или выравнивает области видимости.

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

Я упоминаю, что синтаксис C / C ++ b / c Java, то есть фигурные скобки и область видимости, по крайней мере частично получены из этого семейства. А также потому, что C ++ возник в вопросе комментариев.

В отличие от этого, в Java невозможно иметь локальные объекты: все объекты являются объектами кучи, а все локальные / формальные объекты являются либо ссылочными переменными, либо примитивными типами (кстати, то же самое верно и для статики).

Кроме того, в Java область видимости и время жизни точно не приравниваются: находиться в области видимости и вне ее - это в основном концепция времени компиляции, идущая к доступности и конфликтам имен; ничего не происходит в Java при выходе из области видимости при очистке переменных. Сборка мусора в Java определяет (конечную точку) время жизни объектов.

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

(Тем не менее, чтобы быть уверенным, что базовый JIT-компилятор во время выполнения может повторно использовать одну и ту же область памяти стека для двух разных типов (и разных слотов) локальных систем, с достаточным анализом байт-кода.)

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

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

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

(Обратите внимание, что как код в блоках, отдельные случаи имеют доступ к операторам "break;" и "continue;", относящимся к замкнутому циклу, тогда как в качестве методов потребуется возвращать логические значения и использовать условные выражения вызывающей стороны для получения доступа к этому потоку управления заявления.)


Очень интересный ответ (+1)! Однако одно дополнение к части C ++. Если пароль является строкой в ​​блоке, строковый объект будет выделять место где-то в свободном хранилище для части переменного размера. При выходе из блока строка будет уничтожена, что приведет к освобождению переменной части. Но так как его нет в стеке, ничто не гарантирует, что он скоро будет перезаписан. Так что лучше соблюдайте конфиденциальность, перезаписывая каждого своего персонажа до того, как строка будет уничтожена ;-)
Кристоф

1
@ErikEidt Я пытался привлечь внимание к проблеме разговора о «C / C ++», как будто это на самом деле «вещь», но это не так: «Во время выполнения для C / C ++ компилятор обычно выделяет кадр стека для метода, который настолько велик, насколько это необходимо ", но C не имеет методов вообще. У него есть функции. Они разные, особенно в C ++.
17

7
« Напротив, в Java невозможно иметь локальные объекты: все объекты являются объектами кучи, а все локальные / формальные объекты являются либо ссылочными переменными, либо типами примитивов (кстати, то же самое относится и к статике)». - Обратите внимание, что это не совсем верно, особенно для локальных переменных метода. Анализ побега может выделить объекты в стек.
Борис Паук

1
« В лучшем случае компилятор может повторно использовать слоты локальных переменных, но (в отличие от C / C ++), только если их тип одинаков. Это просто неправильно. На уровне байт-кода локальные переменные могут быть назначены и переназначены с любым, что вы хотите, единственным ограничением является то, что последующее использование совместимо с последним типом назначенного элемента. Повторное использование слотов локальной переменной является нормой, например { int x = 42; String s="blah"; } { long y = 0; }, с longпеременной будет повторно использоваться слоты обеих переменных xи sвсех широко используемых компиляторов ( javacи ecj).
Хольгер

1
@Boris the Spider: это часто повторяемое разговорное утверждение, которое на самом деле не соответствует тому, что происходит. Escape-анализ может включать скаляризацию, которая представляет собой разложение полей объекта на переменные, которые затем подлежат дальнейшей оптимизации. Некоторые из них могут быть исключены как неиспользуемые, другие могут быть свернуты с исходными переменными, используемыми для их инициализации, другие сопоставлены с регистрами ЦП. Результат редко совпадает с тем, что люди могут себе представить, говоря «распределены в стеке».
Хольгер

27

На мой взгляд, было бы более понятно, чтобы вытащить блок в свой собственный метод. Если вы оставите этот блок внутри, я надеюсь увидеть комментарий, разъясняющий, почему вы ставите этот блок в первую очередь. В этот момент становится понятнее, что вызов метода, в initializeKeyManager()котором переменная пароля ограничена областью действия метода, показывает ваше намерение с помощью блока кода.


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

16
@RubberDuck правда, но в этом случае остальные из нас никогда не должны видеть это, поэтому не имеет значения, что мы думаем. То, чем занимаются взрослый кодер и согласный компилятор в частной жизни своей собственной кабинки, больше никого не волнует.
Candied_Orange

@candied_orange Боюсь, что я не понимаю :(
Хотите

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

@candied_orange О, подумал, что вы спорите, а не расширяете аргумент RubberDuck.
DoubleOrt

17

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

Есть один случай, когда я нахожу это полезным - в switchзаявлении! Иногда вы хотите объявить переменную вcase :

switch (op) {
    case ADD:
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
        break;
    case SUB:
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
        break;
}

Это, однако, не удастся:

error: variable result is already defined in method main(String[])
            int result = a - b;

switch операторы странные - они контрольные операторы, но из-за их падения они не вводят области видимости.

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

switch (op) {
    case ADD: {
        int result = a + b;
        System.out.printf("%s + %s = %s\n", a, b, result);
    } break;
    case SUB: {
        int result = a - b;
        System.out.printf("%s - %s = %s\n", a, b, result);
    } break;
}

6
  • Общий случай:

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

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

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

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

Это может иметь смысл, но это очень редко.

  • Ваш вариант использования:

Вот ваш код:

KeyManagerFactory keyManager = KeyManagerFactory.getInstance("SunX509");
Keystore keyStore = KeyStore.getInstance("JKS");

{
    char[] password = getPassword();
    keyStore.load(new FileInputStream(keyStoreLocation), password);
    keyManager.init(keyStore, password);
}

Вы создаете FileInputStream , но никогда не закрываете его.

Одним из способов решения этой проблемы является использование оператора try-with-resources . Он ограничивает область действия InputStream, и вы также можете использовать этот блок для уменьшения области действия других переменных, если хотите (в пределах разумного, поскольку эти строки связаны):

KeyManagerFactory keyManager = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
Keystore keyStore = KeyStore.getInstance("JKS");

try (InputStream fis = new FileInputStream(keyStoreLocation)) {
    char[] password = getPassword();
    keyStore.load(fis, password);
    keyManager.init(keyStore, password);
}

(Кстати, часто лучше использовать, getDefaultAlgorithm()а не SunX509.)

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


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

2

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

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

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

Не предложение для лишних методов

Это не означает, что вы должны принудительно создавать методы, просто чтобы уменьшить область. Возможно, это так же плохо или хуже, и то, что я предлагаю, не должно вызывать потребность в неуклюжих частных «вспомогательных» методах больше, чем потребность в анонимных областях. Мы слишком много думаем о коде, как он есть сейчас, и о том, как уменьшить область видимости переменных, а не о том, как концептуально решить проблему на уровне интерфейса способами, которые естественным образом обеспечивают чистую, короткую видимость состояний локальных функций без глубокого вложения блоков. и 6+ уровней отступа. Я согласен с Бруно в том, что вы можете помешать удобочитаемости кода, принудительно вставляя 3 строки кода в функцию, но это начинается с предположения о том, что вы развиваете функции, которые вы создаете на основе существующей реализации, вместо того, чтобы проектировать функции без запутывания в реализациях. Если вы сделаете это последним способом, я обнаружил небольшую потребность в анонимных блоках, которые не преследуют никакой цели, кроме сокращения области видимости переменной в данном методе, если только вы не ревностно пытаетесь уменьшить область действия переменной всего на несколько строк безвредного кода где экзотическое введение этих анонимных блоков, возможно, вносит столько интеллектуальных издержек, сколько и устраняет.

Попытка уменьшить минимальные области еще больше

Если целесообразно сократить область видимости локальных переменных до абсолютного минимума, тогда должно быть широкое признание такого кода:

ImageIO.write(new Robot("borg").createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())), "png", new File(Db.getUserId(User.handle()).toString()));

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

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

Практическая полезность уменьшения объема

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


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

2

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

Я думаю, что мотивация является достаточным основанием для введения блока, да.

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

Джон Кармарк написал памятку о встроенном коде в 2007 году. Его заключительное резюме

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

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


1

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

Я в первую очередь программист на C #, но я слышал, что в Java возвращают пароль, char[]который позволяет сократить время пребывания пароля в памяти. В этом примере это не поможет, поскольку сборщику мусора разрешено собирать массив с того момента, когда он не используется, поэтому не имеет значения, выходит ли пароль из области действия. Я не спорю, является ли это жизнеспособным, чтобы очистить массив после того, как вы закончили с ним, но в этом случае, если вы хотите сделать это, определение области действия имеет смысл:

{
    char[] password = getPassword();
    try{
        keyStore.load(new FileInputStream(keyStoreLocation), password);
        keyManager.init(keyStore, password);
    }finally{
        Arrays.fill(password, '\0');
    }
}

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

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

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

{
    MethodBuilder m_ToString = tb.DefineMethod("ToString", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofString, Type.EmptyTypes);
    var il = m_ToString.GetILGenerator();
    il.Emit(OpCodes.Ldstr, templateType.ToString()+":"+staticType.ToString());
    il.Emit(OpCodes.Ret);
    tb.DefineMethodOverride(m_ToString, m_Object_ToString);
}
{
    PropertyBuilder p_Class = tb.DefineProperty("Class", PropertyAttributes.None, typeofType, Type.EmptyTypes);
    MethodBuilder m_get_Class = tb.DefineMethod("get_Class", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final, typeofType, Type.EmptyTypes);
    var il = m_get_Class.GetILGenerator();
    il.Emit(OpCodes.Ldtoken, staticType);
    il.Emit(OpCodes.Call, m_Type_GetTypeFromHandle);
    il.Emit(OpCodes.Ret);
    p_Class.SetGetMethod(m_get_Class);
    tb.DefineMethodOverride(m_get_Class, m_IPattern_get_Class);
}

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

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.