Чи певні петлі "while (true)" такі погані? [зачинено]


218

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

do{
     //get some input.
     //if the input meets my conditions, break;
     //Otherwise ask again.
} while(true)

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

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

Що не так do-while(true)?


23
Запитайте свого вчителя, що такий матеріал є досить суб'єктивним.
Кріс Еберл

18
Я вважаю цю статтю корисною для розуміння того, що шкідливо в гото . Порівняння з break, мабуть, добре означає, але насправді неправильно зрозуміло. Можливо, ви можете навчити свого професора з цього приводу;) З мого досвіду професори не знають багато про те, як вміти програмувати.
Магнус Хофф

100
Єдина справді, безперечно погана річ у цьому, - на мій погляд, той факт, що do {} while (true)рівнозначний, while(true) {}і остання є набагато більш звичайною формою і набагато зрозумілішою.
Voo

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

16
Я не згоден з тегом домашнього завдання.
aaaa bbbb

Відповіді:


220

Я б не сказав, що це погано - але однаково я б хоч шукав альтернативу.

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

Як приклад того, де зрозуміліше використовувати breakпрапор, розглянемо:

while (true)
{
    doStuffNeededAtStartOfLoop();
    int input = getSomeInput();
    if (testCondition(input))
    {
        break;
    }
    actOnInput(input);
}

Тепер давайте змусимо його використовувати прапор:

boolean running = true;
while (running)
{
    doStuffNeededAtStartOfLoop();
    int input = getSomeInput();
    if (testCondition(input))
    {
        running = false;
    }
    else
    {
        actOnInput(input);
    }
}

Я вважаю останній більш складним для читання: у нього є додатковий elseблок, він actOnInputє більш відступним, і якщо ви намагаєтеся розробити те, що відбувається при testConditionповерненні true, вам потрібно уважно переглянути решту блоку, щоб перевірити, чи є це не те , що після того, як в elseблоці , який відбудеться , чи буде runningвстановлено значення falseчи ні.

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

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


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

3
@ X-Zero: Так, іноді. Якщо ви можете перетворити "перерву" на "повернення", це часто є добротою ... хоча ви все ще можете закінчитисьwhile (true)
Джон Скіт

21
@trutheality: Я вважаю за краще, щоб більшість мого коду була якомога більше без обмежень. І стан зі складеним завданням мені здається складнішим.
Джон Скіт

3
@Vince: Так, і це чудово, якщо ви зможете легко висловити умову на початку циклу . Але іноді ти не можеш - і ось про яку ситуацію ми говоримо. Зверніть увагу на перші пару речень моєї відповіді.
Джон Скіт

6
@Ismail: Я сподіваюся, що ні. Дописи слід судити виключно за їх змістом, а не за автором. Чорт забираю, я б навіть відмовився від посади Еріка Ліпперта, якби я відчував це неточним / недоброзичливим. Це ще не сталося, хоча :)
Джон Скіт

100

AFAIK нічого, насправді. Вчителі просто алергічні goto, тому що десь почули, що це справді погано. Інакше ви просто напишете:

bool guard = true;
do
{
   getInput();
   if (something)
     guard = false;
} while (guard)

Що майже те саме.

Можливо, це чистіше (адже вся інформація про циклічне вміщення міститься у верхній частині блоку):

for (bool endLoop = false; !endLoop;)
{

}

4
Мені подобаються ваші пропозиції, оскільки умова припинення набагато помітніша. Це означає, що читаючи код, ви швидше зрозумієте, що він намагається зробити. Я б ніколи не використовував нескінченний цикл, але часто використовую ваші дві версії.
Sled

4
Консенсус полягає в тому, що прапор краще, ніж зламати, багато в чому він виражає наміри? Я бачу, що це вигідно. Усі тверді відповіді, але я зазначу це як прийняте.
JHarnach

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

8
Мені більше подобається відповідь Джона Скіта. Крім того, в той час, як (правда) {} набагато краще, ніж робити {} в той час, як (правда)
aaaa bbbb

1
Я ненавиджу, що Dijkstra коли-небудь писав цю статтю GoTo. Хоча GoTo, безумовно, можна було часто зловживати в минулому, не означає, що ви ніколи не повинні ним користуватися. Також Exit For, Break, Exit while, Try / Catch - це лише спеціалізовані форми Goto. Готос часто може зробити код більш читабельним. І так, я знаю, що без Гото все можна зробити. Це не означає, що воно повинно.
Kibbee

39

Дуглас Крокфорд мав зауваження про те, як він хотів, щоб JavaScript містив loopструктуру:

loop
{
  ...code...
}

І я не думаю, що Java може бути гіршим і для того, щоб мати loopструктуру.

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

Але те, що вони рідко згадують, - це те, що всі циклічні механізми можна повторити за допомогою while(true)циклів.

while( a() )
{
  fn();
}

те саме, що

loop
{
  if ( !a() ) break;
  fn();
}

і

do
{
  fn();
} while( a() );

те саме, що:

loop
{
  fn();
  if ( !a() ) break;
}

і

for ( a(); b(); c() )
{
  fn();
}

те саме, що:

a();
loop
{
  if ( !b() ) break;
  fn();
  c();
}

Поки ви можете налаштувати свої петлі таким чином, що працює конструкція, яку ви вирішили використовувати, є неважливою. Якщо трапляється вміщення в forцикл, використовуйте afor петлю.

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


2
+1: Є додаткові складності з forциклами, коли continueзадіяні оператори, але вони не є основним розширенням.
стипендіати доналу

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

4
Ніхто більше не пише for (;;) {? (Вимовлено «назавжди»). Це раніше було дуже популярним.
Dawood ibn Kareem

16

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

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

Очевидно, це не єдина парадигма програмування, але часто її можна легко застосувати до інших парадигм, таких як об'єктно-орієнтоване програмування (ala Java).

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

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

За іронією долі, обробка винятків - це область, де відхилення від структурованого програмування неодмінно з’являться і очікуються, коли ви продовжите програмування на Java.

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


2
Ось документ Едсгера Дікстра . Це добре читати.
Рой Прінс

14

Прийнятною умовою Java для введення читання є:

import java.io.*;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String strLine;

while ((strLine = br.readLine()) != null) {
  // do something with the line
}

А звичайна умова C ++ для введення читання:

#include <iostream>
#include <string>
std::string data;
while(std::readline(std::cin, data)) {
  // do something with the line
}

А в С це

#include <stdio.h>
char* buffer = NULL;
size_t buffer_size;
size_t size_read;
while( (size_read = getline(&buffer, &buffer_size, stdin)) != -1 ){
  // do something with the line
}
free(buffer);

або якщо ви впевнені, що знаєте, який довгий рядок тексту у вашому файлі, ви можете зробити

#include <stdio.h>
char buffer[BUF_SIZE];
while (fgets(buffer, BUF_SIZE, stdin)) {
  //do something with the line
}

Якщо ви перевіряєте, чи ввів ваш користувач quitкоманду, легко розширити будь-яку з цих 3 циклівних структур. Я зроблю це на Java для вас:

import java.io.*;
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String line;

while ((line = br.readLine()) != null  && !line.equals("quit") ) {
  // do something with the line
}

Отже, хоча, безумовно, є випадки, коли breakабоgoto це виправдано, якщо все, що ви робите, - це читання з файлу або консолі, рядка за рядком, то вам не потрібна while (true)петля для його виконання - ваша мова програмування вам вже надала з відповідною ідіомою для використання команди введення як умови циклу.


Насправді, якщо ви використовуєте while (true)цикл замість одного із цих звичайних вхідних циклів, можливо, ви забудете перевірити кінець файлу.
Кен Блум

3
Ви досягаєте цього ефективно, вставляючи першу частину циклу в whileумовну. Зрештою, умова Java вже стає досить здоровою, і якщо вам довелося зробити обширну маніпуляцію, щоб вирішити, продовжувати чи ні, це може зайняти досить довго. Ви можете розділити його на окрему функцію, і це може бути найкраще. Але якщо ви хочете, щоб це виконувались в одній функції, і перед тим, як вирішити, чи продовжувати роботу, слід зробити нетривіальну роботу, while(true)можливо, найкраще.
poolie

@poolie: Тоді, мабуть, найкраще використовувати команду read як умову циклу (яка перевіряє наявність EOF) та перевіряти інші умови в циклі як заяви про перерву.
Кен Блум

1
Що варто, gets()не є загальною умовою. Це дуже сприятливо для переповнення буфера. fgets(buffer, BUFFER_SIZE, file)набагато більше схожа на стандартну практику.
Дейв

@Dave: я тепер відредагував відповідь, яку потрібно використовувати fgets.
Кен Блум

12

Це не така страшна річ, але потрібно враховувати інших розробників при кодуванні. Навіть у школі.

Ваші колеги-розробники повинні мати змогу бачити вихідний пункт для вашого циклу під час оголошення циклу. Ти цього не робив. Ви приховали пункт виходу посеред циклу, зробивши більше роботи для когось іншого, хто підійде і намагається зрозуміти ваш код. Це та сама причина, що уникнути таких речей, як «зламати».

Попри це, ви все одно побачите подібні речі в МІНІ коді там, у реальному світі.


4
Якщо цикл починається з while (true)цілком очевидно, що там буде breakабо returnвсередині нього, або що він буде працювати назавжди. Бути прямолінійним важливо, але while(true)не особливо погано, саме по собі. Змінні, які мають складні інваріанти через ітерації циклу, будуть прикладом того, що викликає набагато більший стурбованість.
poolie

4
Спробуйте пройти три рівні вглиб пошуку, а потім якось вирватися. Код стане набагато гіршим, якщо ви просто не повернетеся з цього пункту.
дасканді

11

Це ваш пістолет, ваша куля і ваша стопа ...

Це погано, бо ти просиш клопоту. Ви не будете ні у вас, ні в іншому з цих плакатів на прикладі коротких / простих циклів.

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

Чому? Мені довелося з’ясувати, чому додаток LOC 700K поступово починає спалювати 100% часу процесора, поки кожен процесор не насититься. Це був дивовижний цикл (справжній). Це було велике і бридке, але воно зводилося до:

x = read_value_from_database()
while (true) 
 if (x == 1)
  ...
  break;
 else if (x ==2)
  ...
  break;
and lots more else if conditions
}

Остаточного відділення ще не було. Якщо значення не збігалося з умовою if, цикл продовжував працювати до кінця часу.

Звичайно, програміст звинувачував кінцевих користувачів у тому, що вони не вибрали значення, яке очікував програміст. (Тоді я усунув усі випадки часу (true) у коді.)

ІМХО - це не дуже добре оборонним програмуванням використовувати конструкції типу while (true). Це повернеться, щоб переслідувати вас.

(Але я пам'ятаю професорів, які оцінювали вниз, якщо ми не коментували кожен рядок, навіть за i ++;)


3
+1 за коментар щодо оборонного програмування.
JHarnach

3
У вашому прикладі код нерозумний не через вибір часу (правда), а через ідею поставити цикл навколо коду.
Флоріан Ф

Справді, в чому полягала ця петля? :)
джузлін

6

Це погано в тому сенсі, що структуровані програми програмування вважають за краще (дещо неструктуровані) розбивати і продовжувати оператори. Вони, для порівняння, віддають перевагу "гото" за цим принципом.

Я завжди рекомендую зробити свій код максимально структурованим ... хоча, як зазначає Джон Скіт, не робіть його більш структурованим!


5

Згідно з моїм досвідом, у більшості випадків петлі мають "головну" умову для продовження. Це умова, яку слід записати в оператор while (). Усі інші умови, які можуть порушити цикл, є вторинними, не настільки важливими тощо. Вони можуть бути записані як додаткові if() {break}заяви.

while(true) часто заплутаний і менш читабельний.

Я думаю, що ці правила не охоплюють 100% випадків, але, ймовірно, лише 98%.


Добре сказано. Це як би використовувати цикл "робити" для циклу: while (true) {i ++; ...}. Ви переховуєте стан циклу всередині циклу, а не в 'підписі'.
LegendLength

3

Хоча це не обов'язково відповідь на те, чому не використовувати while (true), я завжди вважав це комічне та супроводжуюче висловлювання автора стислим поясненням, чому потрібно робити, а не робити час.

Що стосується вашого питання: Немає властивої проблеми

while(true) {
   do_stuff();
   if(exit_time) {
      break;
   }
}

... якщо ви знаєте, що робите, і переконайтеся, що exit_timeв якийсь момент оцінять true.

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


Я думаю, що налаштування exit_time = false; while(!exit_time) { execute_stuff(); }і do { execute_stuff(); } while(! exit_time );обидва набагато чіткіше, ніж наявність if( condition ) { break; }у кінці циклу з a while(true). Перерви - це коротке замикання до циклів - ідеально добре, коли використовується як коротке замикання в середині циклу, але вам слід просто оцінити умову в операторі while проти перерви в кінці циклу.
dr jimbob

Що стосується цього комічного: в той час як робити час поки не може робити все, що в той час, коли це може зробити, в той час як іноді краще робити те, що робити, в той час як це може зробити, навіть тоді, коли це може зробити.
Тераот

3

Ви можете просто використовувати булевий прапор, щоб вказати, коли закінчити цикл while. Breakі go toбули причиною того, що програмне забезпечення важко підтримувати - криза програмного забезпечення (tm) - і цього слід уникати, і легко може бути занадто.

Це питання, прагматичний ти чи ні. Прагматичні кодери можуть просто використати перерву в цій простій ситуації.

Але добре отримати звичку не використовувати їх, інакше ви можете використовувати їх з привитку в невідповідних ситуаціях, як, наприклад, у складних вкладених циклах, де читабельність та ремонтопридатність вашого коду стає складніше за допомогою break.


8
Тому що зберігати булевий прапор, мабуть, не зрозуміліше, і , на мій досвід , це може бути менш зрозуміло?
Джон Скіт

3
@ Jon Skeet Ну, це питання про те, щоб поїхати з хорошим привитком або навчити себе поганому, використовуючи перерву. Бул під назвою "біг" вам незрозумілий? Його очевидний, простий у налагодженні, і, як я вже згадував раніше, хороший вміст зберігати. Де проблема?
Даніель Лешковський

4
Буль називається запущеним, що вимагає, щоб я мав if (running)всередині циклу, відступ у всьому іншому коду, коли все, що я хочу, - це вийти з циклу, для мене, безумовно, менш зрозумілий, ніж просте повідомлення про перерву, в якому зазначено саме те , що я хочу робити. Ви, здається, розглядаєте аксіоматичне використання перерви як шкідливої ​​звички - я не вважаю це таким.
Джон Скіт

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

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

3

Можливо, мені не пощастило. А може, мені просто бракує досвіду. Але кожен раз, коли я пригадую, що while(true)мав справу з breakвнутрішньою, можна було вдосконалити код, застосовуючи метод Extract до блоку while , який утримував, while(true)але (за збігом обставин?) Перетворив усі breaks на returns.

На мій досвід, while(true)без перерв (тобто з віддачею або кидками) досить зручно і легко зрозуміти.


  void handleInput() {
      while (true) {
          final Input input = getSomeInput();
          if (input == null) {
              throw new BadInputException("can't handle null input");
          }
          if (input.isPoisonPill()) {
              return;
          }
          doSomething(input);
      }
  }

3

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


2

Немає жодної великої проблеми while(true)зbreak заявами, однак деякі можуть подумати , його трохи знижує читаність коду. Постарайтеся надати змінним значущі імена, оцініть вирази в потрібному місці.

Для вашого прикладу здається набагато зрозумілішим зробити щось на кшталт:

do {
   input = get_input();
   valid = check_input_validity(input);    
} while(! valid)

Це особливо актуально, якщо цикл виконання, поки цикл стає довгим - ви точно знаєте, де перевіряється, чи є додаткова ітерація. Усі змінні / функції мають відповідні назви на рівні абстракції. Thewhile(true)Твердження не є вам сказати , що обробка не в тому місці , ви думали.

Можливо, ви хочете, щоб в інший раз виходили через цикл. Щось на зразок

input = get_input();
while(input_is_not_valid(input)) {
    disp_msg_invalid_input();
    input = get_input();
}

мені здається більш зрозумілим

do {
    input = get_input();
    if (input_is_valid(input)) {
        break;
    }
    disp_msg_invalid_input();
} while(true);

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


1

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

DWORD dwError = ERROR_SUCCESS;

do
{
    if ( (dwError = SomeFunction()) != ERROR_SUCCESS )
    {
         /* handle error */
         continue;
    }

    if ( (dwError = SomeOtherFunction()) != ERROR_SUCCESS )
    {
         /* handle error */
         continue;
    }
}
while ( 0 );

if ( dwError != ERROR_SUCCESS )
{
    /* resource cleanup */
}

1

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


1

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


1

Для мене проблема - читабельність.

Одночасне твердження з справжньою умовою нічого не говорить про цикл. Це значно ускладнює роботу з розумінням цього.

Що було б простіше зрозуміти з цих двох фрагментів?

do {
  // Imagine a nice chunk of code here
} while(true);

do {
  // Imagine a nice chunk of code here
} while(price < priceAllowedForDiscount);

0

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


0

1) Нічого поганого з a do -while(true)

2) Ваш вчитель помиляється.

NSFS !!:

3) Більшість викладачів - це викладачі, а не програмісти.


@Connell зачекайте, поки ваш вчитель почує це!
Pacerier

1
Мене навчають на фронті програмування. Все, що я повинен судити, - це мій вчитель ІТ в середній школі, який навчив нас робити веб-сайти, використовуючи Dreamweaver, таким чином, щоб ВСЕ було абсолютно розміщеним дівом ...
Connell

@Connell на моє повідомлення, щоб показати вашу згоду
Pacerier

2
«Ті, хто не вміє, навчають». - Це досить велике узагальнення, ви не думаєте? Чи повинні лікарі, інженери, пілоти тощо навчатись самому, оскільки їх інструктори некомпетентні?
filip-fku

@ filip-fku wow wow ~ cool it cool it!
Pacerier

0

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.