Чи має сенс створювати блоки просто для зменшення обсягу змінної?


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 не можуть бути локальні об’єкти: усі об'єкти - це купі об'єкти, а всі локальні / формальні форми - це або еталонні змінні, або примітивні типи (btw, те ж саме стосується і статики).

Крім того, сфера та термін служби Java точно не прирівнюються: використання в / з сфери застосування - це переважно концепція часу компіляції, що стосується доступності та конфліктів імен; в Java нічого не відбувається при виході із застосуванням області щодо очищення змінних. Збір сміття Java визначає (кінцеву точку) терміну експлуатації об’єктів.

Також механізм байт-коду Java (вихід компілятора Java) прагне просувати користувацькі змінні, оголошені в межах обмеженого діапазону, до найвищого рівня методу, оскільки на рівні байт-коду немає функції масштабу (це аналогічно обробці C / C ++ стек). У кращому випадку компілятор може повторно використовувати локальні слоти змінних, але (на відміну від C / C ++), лише якщо їх тип однаковий.

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

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

І все-таки немає нічого поганого в тому, щоб використовувати його для деконфліктування імен.

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

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


Дуже цікава відповідь (+1)! Однак одне доповнення до частини С ++. Якщо пароль - це рядок у блоці, об’єкт рядка виділить простір десь у вільному сховищі для для частини змінного розміру. Виходячи з блоку, рядок буде зруйнований, що призведе до розміщення змінної частини. Але оскільки його немає на стеці, ніщо не гарантує, що воно скоро буде перезаписано. Тож краще керуйте конфіденційністю, перезаписавши кожен її символ до руйнування рядка ;-)
Крістоф

1
@ErikEidt Я намагався звернути увагу на проблему говорити про "C / C ++", ніби це насправді "річ", але це не так: "Під час виконання для C / C ++ компілятор зазвичай виділяє рамку стека для методу, який є настільки великим, як може знадобитися ", але у C методів взагалі немає. Він має функції. Вони різні, особливо в С ++.
tchrist

7
" Навпаки, на Java не можуть бути локальні об'єкти: усі об'єкти - це купи" об'єктів ", і всі локальні / формальні форми є або еталонними змінними, або примітивними типами (btw, те ж саме стосується статики). " - Зауважте, що це не зовсім вірно, особливо не для локальних змінних методів. Аналіз втечі може виділити об'єкти в стек.
Борис Павук

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

1
@Boris the Spider: це часто повторюване розмовне твердження, яке насправді не відповідає тому, що відбувається. Аналіз втечі може ввімкнути масштабування, тобто розкладання полів об'єкта на змінні, які потім підлягають подальшій оптимізації. Деякі з них можуть бути усунені як невикористані, інші можуть скластись із вихідними змінними, які використовуються для їх ініціалізації, інші отримають відображення в регістри процесора. Результат рідко збігається з тим, що люди могли б уявити, говорячи "виділено на стек".
Холгер

27

На мою думку, було б зрозуміліше вивести блок у свій метод. Якщо ви залишили цей блок, я би сподівався побачити коментар, який роз'яснює, чому ви ставите цей блок в першу чергу. У цей момент просто почуватиметься чистішим виклик методу, initializeKeyManager()який обмежує парольну змінну обмеженою сферою застосування методу та показує ваш намір із блоком коду.


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

16
@RubberDuck правда, але в цьому випадку решта з нас ніколи його не бачать, тому не має значення, що ми думаємо. Те, що кодер для дорослих та упорядник, що погоджується, ставлять у конфіденційність власної кабіни, вже нікого не хвилює.
candied_orange

@candied_orange Я боюсь, я не зрозумію це :( Докладно розробити?
подвійне

@doubleOrt Я мав на увазі, що проміжні етапи рефакторингу є реальними та необхідними, але, безумовно, не потрібно їх закріплювати в контролі джерел, де кожен може на це подивитися. Просто закінчіть рефакторинг перед тим, як зареєструватися.
candied_orange

@candied_orange О, подумав, ти сперечаєшся і не поширюєш аргумент RubberDuck.
подвійнийОкт

17

Вони можуть бути корисними в Расті, якщо це суворі правила запозичення. І взагалі, у функціональних мовах, де цей блок повертає значення. Але на таких мовах, як 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, але ви ніколи не закриваєте його.

Одним із способів вирішення цього питання є використання оператора "спробувати ресурси" . Він охоплює область 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 рядки (якщо що, то це випадок, коли створення окремого методу перешкоджає читабельності).


«Зменшення обсягу деяких змінних може бути ознакою того, що ваш метод досить довго» - це може бути ознакою , але він також може і ІМХО повинно бути використано для того , щоб документ , як довго використовується значення місцевого змінної. Насправді, для мов, які не підтримують вкладені області застосування (наприклад, Python), я би вдячний, якщо відповідний коментар до коду зазначив "кінець життя" змінної. Якщо та ж змінна буде повторно використана пізніше, було б легко з'ясувати, чи повинна вона (або повинна) бути ініціалізована з новим значенням десь раніше (але після коментаря "кінець життя").
Джонні Ді

2

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

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

Наприклад, якщо ви намагаєтесь це зробити для повторного використання змінної, названої 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');
    }
}

Це дійсно схоже на заяву " спробу використання ресурсів" , оскільки воно охоплює ресурс і виконує його завершення після того, як ви закінчите. Знову зауважте, що я не сперечаюся таким чином обробляти паролі, лише якщо ви вирішите це зробити, цей стиль є розумним.

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

Ще один приклад, про який я можу придумати, - це коли у вас є дві змінні, які мають схожу назву та значення, але ви працюєте з однією, а потім з іншою, і ви хочете їх тримати окремо. Я написав цей код на 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.