Чи потрібна коли-небудь петля “do {…} while ()”?


75

Б'ярн Страуструп (творець C ++) одного разу сказав, що уникає циклів "do / while" і вважає за краще писати код у вигляді циклу "while". [Див. Цитату нижче.]

Почувши це, я переконався, що це правда. Які ваші думки? Чи є приклад, коли "робити / в той час" набагато чистіше і легше зрозуміти, ніж якби замість цього ви використовували "в той час"?

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

Дозвольте запитати по-іншому: припустимо, вам заборонили використовувати "do / while" - чи є реалістичний приклад, коли це не дасть вам іншого вибору, як писати нечистий код, використовуючи "while"?

З "Мова програмування на C ++", 6.3.3:

На моєму досвіді, твердження do є джерелом помилок і плутанини. Причина полягає в тому, що його тіло завжди виконується один раз перед оцінкою стану. Однак, щоб організм працював правильно, щось дуже схоже на стан повинно відбуватися навіть з першого разу. Частіше, ніж я міг би здогадатися, я виявив, що умова не виконується, як очікувалося, ні коли програма була вперше написана і перевірена, ні пізніше після того, як код, що передував їй, був змінений. Я також віддаю перевагу умові "спереду, де я це бачу". Отже, я, як правило, уникаю тверджень. -Бьярн


3
@brandaemon Code Review ще не було в червні 2009 року. Що саме ви говорите?
Щогла

1
@brandaemon Це дійсно погана рекомендація ...
Ісмаель Мігель

3
@brandaemon StackOverflow або Programmers.SE. Перегляд коду призначений лише для повністю робочого коду, а також повинен містити код . Це питання не має коду. Що робить його невідповідним на Code Review.
Ісмаель Мігель


1
@brandaemon Я б витратив трохи часу, щоб прочитати цю мета публікацію, яка досить обширна на цю тему. Він надає велике розуміння того, які питання є на тему в Programmers.StackExchange. Сподіваюся, це теж допоможе!
enderland

Відповіді:


96

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

#include <stdio.h>

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

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


1
Ось версія "while": char c = -1; while (c <'0' || c> '9') {printf (...); scanf (...); }
Дастін Босуелл

46
@Dustin, IMO приклад лічильника менш читабельний, оскільки ти використовуєш магічне число (-1). Тож з точки зору "кращого" коду (що, звичайно, суб'єктивно), я думаю, що оригінальний код @ Rekreativc кращий, оскільки він читабельніший для людських очей.
Рон Кляйн,

8
Я не впевнений, що назвав би це "магічним" числом, але в будь-якому випадку ви можете ініціалізувати його як "char c = '0' - 1;" якщо це зрозуміліше. Можливо, те, що ви справді чините опір, це той факт, що "с" взагалі має початкове значення? Я згоден, що це трохи дивно, але, з іншого боку, "час" - це приємно, тому що він вказує умову продовження спереду, тому читач одразу розуміє життєвий цикл циклу.
Дастін Босуелл,

6
Я заперечую так, в принципі, що "c" не повинно мати спостережуваного початкового значення взагалі. Ось чому я хотів би, щоб версія під час роботи була кращою. (Хоча, я думаю, у вас є питання щодо реальної читабельності.)
mqp

8
@Dustin: Важливим мінусом вашого перезапису є те, що вам потрібно зарезервувати один елемент (тут -1) з домену charяк спеціальний маркер. Хоча це не впливає на логіку тут, загалом це означає, що ви більше не можете обробляти потік довільних символів, оскільки він може включати символи, що мають значення -1.
j_random_hacker

39

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

Інший приклад - коли ви вже маєте дійсний об’єкт, коли має бути запущена перша ітерація, тому ви не хочете виконувати нічого (включаючи оцінку стану циклу) до початку першої ітерації. Прикладом може служити функція FindFirstFile / FindNextFile Win32: ви викликаєте FindFirstFile, який або повертає помилку, або дескриптор пошуку до першого файлу, а потім викликає FindNextFile, доки не поверне помилку.

Псевдокод:

Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

8
передумова? якщо стан перевіряється після одного виконання тіла, чи не є це післяумовою? :)

8
Ось перезапис, який, на мою думку, є більш читабельним (і має менше вкладеності) Handle handle = FindFirstFile (params); while (handle! = Помилка) {process (params); handle = FindNextFile (params); }
Дастін Босуелл

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

Чи не можете ви уникнути if altoghether, використовуючи цикл while тут? while ((handle = FindNextFile (params))! = Помилка)) {process (params); } Я маю на увазі, що умова в реченні if точно така ж, як умова у циклі.
Хуан Пабло Каліфано,

2
@Dustin: це також можна зробити так (і, мабуть, так і повинно бути) for(handle = FindFirstFile(params); handle != Error; handle = FindNextFile(params)) {}.
falstro

18

do { ... } while (0) є важливою конструкцією для забезпечення належної поведінки макросів.

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

Редагувати: Я зіткнувся з ситуацією, коли do / while сьогодні був набагато чистішим у власному коді. Я робив міжплатформену абстракцію спарених інструкцій LL / SC . Вони повинні використовуватися в циклі, наприклад:

do
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
} while (!SC (address, newvalue, oldvalue));

(Експерти можуть зрозуміти, що oldvalue не використовується в реалізації SC, але він включений, щоб цю абстракцію можна було емулювати з CAS.)

LL і SC є прекрасним прикладом ситуації, коли do / while значно чистіший за еквівалент, поки форма:

oldvalue = LL (address);
newvalue = oldvalue + 1;
while (!SC (address, newvalue, oldvalue))
{
  oldvalue = LL (address);
  newvalue = oldvalue + 1;
}

З цієї причини я надзвичайно розчарований тим фактом, що Google Go вирішив видалити конструкцію do-while .


Навіщо це потрібно для макросів? Можете навести приклад?
Дастін Босуелл,

5
Погляньте на < c2.com/cgi/wiki?TrivialDoWhileLoop >. Препроцесорні макроси не "мають справу з" синтаксисом C, тому розумні хакі, подібні до цього, необхідні для запобігання макрокоманді, наприклад, у ifвиписці без фігурних дужок.
Уеслі

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

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

1
Вам слід пояснити Pod, вам просто не подобається це естетично, не подобається додаткова безпека з якихось причин або не згодні з тим, що це потрібно? Допоможіть мені зрозуміти.
Ден Олсон,

7

Це корисно, коли ви хочете "зробити" щось ", поки" умова не буде виконана.

Це можна обдурити за певний цикл таким чином:

while(true)
{

    // .... code .....

    if(condition_satisfied) 
        break;
}

1
Насправді, це НЕ еквівалентно "do {... code ...}, тоді як (умова)", якщо "... code ..." містить оператор "continue". Правило "продовжити" полягає в тому, щоб пройти до кінця, щоб зробити / деякий час.
Дастін Босуелл

хороший момент, однак, я сказав "дурня", що означає, що це
якось

6

(Припускаючи, що ви знаєте різницю між ними)

Do / While добре підходить для завантаження / попередньої ініціалізації коду до перевірки вашого стану та запуску циклу while.


6

У наших правилах кодування

  • якщо / while / ... умови не мають побічних ефектів і
  • змінні повинні бути ініціалізовані.

Отже, ми майже ніколи не маємо do {} while(xx) Тому що:

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

переписано в:

int main() {
    char c(0);
    while (c < '0' ||  c > '9');  {
        printf("enter a number");
        scanf("%c", &c);
    } 
}

і

Handle handle;
Params params;
if( ( handle = FindFirstFile( params ) ) != Error ) {
   do {
      process( params ); //process found file
   } while( ( handle = FindNextFile( params ) ) != Error ) );
}

переписано в:

Params params(xxx);
Handle handle = FindFirstFile( params );
while( handle!=Error ) {
    process( params ); //process found file
    handle = FindNextFile( params );
}

6
Побачили помилку у вашому першому прикладі? Можливо, цикл do-while дозволив би вам цього уникнути. :-)
Jouni K. Seppänen

3
Я ненавиджу читання коду з циклами do ... while. Це наче хтось розповідає вам деталі про те, що вас взагалі не турбує, а потім через 15 хвилин лайки кажуть вам, чому ви повинні про це дбати в першу чергу. Зробіть код читабельним. Скажіть мені наперед, для чого потрібна циклічність, і не змушуйте мене прокручувати без причини.
Kieveli

@Kieveli - мої думки також. Я думаю, програмісти звикли мати умови вгорі, як це стосується "за" та "якщо". do / while це дивно, насправді.
Дастін Босуелл,

3
У більшості програмістів, здається, немає проблем із умовами посередині ( if (...) break;), часто навіть у поєднанні з while(true)відн. for(;;). То що робить do ... while, де ви хоча б знаєте, де шукати стан, гіршим?
celtschk

@celtschk Усі мої колишні робочі місця вважали if (bCondition) break;поганий дизайн і заохочували використовувати належні умови циклу . Звичайно , є кілька випадків , в яких не можна уникнути , але в більшості випадків breakі continueє ознаками ледачим кодування.
mg30rg

6

Наступна загальна ідіома видається мені дуже прямолінійною:

do {
    preliminary_work();
    value = get_value();
} while (not_valid(value));

Перезапис, якого слід уникати, doздається:

value = make_invalid_value();
while (not_valid(value)) {
    preliminary_work();
    value = get_value();
}

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

У подібних випадках doконструкція є дуже корисним варіантом.


5

Вся справа в читабельності .
Більш читабельний код призводить до зменшення головного болю в обслуговуванні коду та кращої співпраці.
Інші міркування (наприклад, оптимізація), безумовно, менш важливі в більшості випадків.
Я розробити, так як я отримав коментар тут:
Якщо у вас є фрагмент коду A , який використовує do { ... } while(), і це більш читабельним , ніж while() {...}еквівалентний B , то я б голосувати за A . Якщо ви віддаєте перевагу B , так як ви бачите умова циклу «фронт», і ви думаєте , що це більш зручним для читання (і , таким чином , ремонтопрігодни і т.д.) - то йдіть прямо вперед, використання B .
Я хочу сказати: використовуйте код, який є більш зрозумілим для ваших очей (і для ваших колег). Звичайно, вибір суб’єктивний.


1
в цьому ніхто не сумнівався, особливо не в ОП. Це жодним чином не відповідає на питання.
codymanix

4

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

Одне застереження полягає в тому, що ви не можете мати continuein, <setup code>оскільки це перестрибне умову розриву, але жоден із прикладів, що демонструють переваги виконуваної роботи, не потребує продовження перед умовою.

while (true) {
       <setup code>
       if (!condition) break;
       <loop body>
}

Тут він застосований до деяких найкращих прикладів циклів виконуваних дій вище.

while (true);  {
    printf("enter a number");
    scanf("%c", &c);
    if (!(c < '0' ||  c > '9')) break;
} 

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

while (true);  {
    // get data 
    if (data == null) break;
    // process data
    // process it some more
    // have a lot of cases etc.
    // wow, we're almost done.
    // oops, just one more thing.
} 

Чудовий контратак; використання break!
Містер Полівірл,

3

На мій погляд, це лише особистий вибір.

Найчастіше ви можете знайти спосіб переписати цикл do ... while на цикл while; але не обов’язково завжди. Крім того, може бути більш логічним сенс писати цикл do while, який іноді відповідає контексту, в якому ви перебуваєте.

Якщо подивитися вище, відповідь TimW, це говорить сам за себе. Другий із Handle, особливо, на мій погляд, більш брудний.


3

Прочитайте теорему про структуровану програму . Виконання {} while () завжди можна переписати на while () do {}. Послідовність, вибір та ітерація - все, що коли-небудь потрібно.

Оскільки все, що міститься в тілі циклу, завжди може бути інкапсульовано в рутину, бруд від необхідності використовувати while () {} ніколи не погіршує, ніж

LoopBody()
while(cond) {
    LoopBody()
}

Питання в тому, що відбувається, коли "LoopBody" стає довгим (скажімо, 10 рядків), і ви не хочете створювати для нього функцію. Повторювати ці 10 рядків нерозумно. Але в багатьох випадках код можна реструктуризувати, щоб уникнути дублювання цих 10 рядків, але все одно використовуючи "while".
Дастін Босуелл,

1
Звичайно, досить погано для обслуговування мати один і той же код двічі.
Носредна

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

Інша переписування буде: for (bool first=true; first || cond; first=false) { ... }. Це не дублює код, а вводить додаткову змінну.
celtschk

2

Я навряд чи колись користуюся ними просто через наступне:

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

Візьміть зразок псевдокоду:

do {
   // get data 
   // process data
} while (data != null);

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

do {
   // get data 
   if (data != null) {
       // process data
   }
} while (data != null);

Додаткова перевірка "якщо" просто не варта IMO. Я знайшов дуже мало випадків, коли простіше виконувати виконання, а не цикл while, є більш стислим. YMMV.


Це насправді не відповідає на питання. У ситуації, коли ви спеціально зателефонуєте там, де ви, можливо, вже перебуваєте в стані, коли вам не потрібно запускати внутрішній код, це, безумовно, while() {...}є правильним вибором - while() {...}призначений саме для того випадку, коли вам потрібно запустити кодувати нуль або більше разів на основі умови. Однак це не означає, що ніколи не буває випадків do {...} while(), коли ситуації, коли ви знаєте, що код повинен запускатися принаймні один раз перед перевіркою умови (див. Вибрану відповідь від @ david-božjak).
Chaser324

2

У відповідь на запитання / коментар невідомого (google) на відповідь Дана Олсона:

"do {...}, в той час як (0) є важливою конструкцією для забезпечення належної поведінки макросів."

#define M do { doOneThing(); doAnother(); } while (0)
...
if (query) M;
...

Ти бачиш, що відбувається без do { ... } while(0)? Він завжди виконуватиме doA Another ().


1
Для цього вам не потрібно робити час. #define M {one (); два (); } буде достатньо.
Саймон Х.

1
Але тоді якщо (запит) M; else printf ("привіт"); є синтаксичною помилкою.
Jouni K. Seppänen

Тільки якщо ви ставите; після М. В іншому випадку це працює чудово.
Саймон Х.

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

@Markus: для цього конкретного прикладу, якщо do / while були недоступні на мові, ви можете замінити його на #define M ((void) (doOneThing (), doAnother ())).
Steve Jessop

2

Цикл виконання часу завжди можна переписати як цикл while.

Чи використовувати лише цикли while, чи while, do-while та for-loop (або будь-яку їх комбінацію), значною мірою залежить від вашого естетичного смаку та умов роботи проекту, над яким ви працюєте.

Особисто я віддаю перевагу циклам while, оскільки це спрощує міркування про інваріанти циклу IMHO.

Щодо того, чи існують ситуації, коли вам потрібні цикли виконуваної роботи: Замість

do
{
  loopBody();
} while (condition());

Ви можете завжди

loopBody();
while(condition())
{
  loopBody();
}

так, ні, вам ніколи не потрібно використовувати do-while, якщо з якихось причин ви не можете. (Звичайно, цей приклад порушує СУХУ, але це лише доказ концепції. На моєму досвіді, як правило, існує спосіб перетворення циклу виконуваної роботи у цикл «час», а не порушення «СУХОГО» в конкретному випадку використання)

"Коли знаходишся в Римі, чини так, як римляни".

BTW: Цитата, яку ви шукаєте, можливо ця ([1], останній абзац розділу 6.3.3):

З мого досвіду, твердження do є джерелом помилок і плутанини. Причина полягає в тому, що його тіло завжди виконується один раз перед тестуванням стану. Однак для правильного функціонування організму при першому ж пробігу має виконуватися стан, подібний до остаточного. Частіше, ніж я очікував, я вважав, що ці умови не відповідають дійсності. Це було так як тоді, коли я писав відповідну програму з нуля, а потім тестував її, а також після зміни коду. Крім того, я віддаю перевагу умові "спереду, де я це бачу". Тому я, як правило, уникаю тверджень.

(Примітка: Це мій переклад німецького видання. Якщо вам належить англійське видання, сміливо редагуйте цитату, щоб відповідати його оригінальному формулюванню. На жаль, Аддісон-Веслі ненавидить Google.)

[1] Б. Строуструп: Мова програмування С ++. 3-е видання. Аддісон-Весслі, Редінг, 1997.


12
"gotos? Розкіш! Коли я був хлопцем, ми захопили вказівник інструкції голими руками і перетягнули його до коду, який ми хотіли виконати наступним"
Стів Джессоп,

2
"Перетягування? Ви, щасливі мерзотники. Нам довелося збивати рабів, щоб проштовхувати наші вказівники, що було дуже важко, тому що ми були своїми рабами. Тоді наші мами били нас спати розбитими пляшками".
Kieveli

@Tobias - дякую, що знайшли цитату. Я вклав це в оригінальне запитання (взято з мого англійського видання книги).
Дастін Босуелл,

@Tobias Ось приклад того, як переписана версія лише додала плутанини до абсолютно чистого коду: припустимо, у вас є змінна лічильника циклів, підраховуючи, скільки разів цикл був виконаний. У вашому переписаному циклі while, чи повинен такий лічильник знаходитися всередині функції loopBody або всередині циклу while? Відповідь "це залежить", очевидної відповіді немає. Але в циклі do-while це не мало б значення.
Лундін

2

Перш за все, я погоджуюсь, що do-whileце менш читабельно, ніж while.

Але я вражений тим, що після стількох відповідей ніхто не замислювався, чому do-whileвзагалі існує мова. Причиною є ефективність.

Скажімо, у нас є do-whileцикл із Nперевірками стану, де результат умови залежить від тіла циклу. Тоді, якщо ми замінимо його на whileцикл, N+1замість цього ми отримаємо перевірку стану, де додаткова перевірка безглузда. Це нічого страшного, якщо умова циклу містить лише перевірку цілочисельного значення, але, скажімо, маємо

something_t* x = NULL;

while( very_slowly_check_if_something_is_done(x) )
{
  set_something(x);
}

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

Я часто використовую do-while саме з цією метою, коли кодую вбудовані системи реального часу, де код всередині умови є відносно повільним (перевіряючи відповідь деякої повільної апаратної периферії).


додавання чека до while (x == null || check(x)) . . .вирішення цієї проблеми
DB Fred

@DBFred Це все одно дає гіршу продуктивність, оскільки ви потенційно можете додати додаткову гілку до кожного кола в циклі.
Лундін,

1

розглянемо щось подібне:

int SumOfString(char* s)
{
    int res = 0;
    do
    {
        res += *s;
        ++s;
    } while (*s != '\0');
}

Так трапляється, що '\ 0' дорівнює 0, але я сподіваюся, ви зрозуміли суть.


в який спосіб зробити {}, поки тут чистіше, ніж коли {}?
codymanix

в While {} вам довелося б додати код в кінці циклу.
Нефцен

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

1

Моя проблема з do / while полягає суто в його реалізації в C. Через повторне використання ключового слова while , воно часто відвертає людей, оскільки це виглядає як помилка.

Якби while було зарезервовано лише для циклів while і do / while було змінено на do / until або repeat / until , я не думаю, що цикл (що, звичайно, зручно і "правильний" спосіб кодування певних циклів) стільки клопоту.

Я вже оцінював це щодо JavaScript , який також успадкував цей вибачливий вибір від C.


Але кожен великий стиль кодування там відповідає, вони завжди писати whileпро do-whileна тому самому рядку, що і }. І в кінці завжди є крапка з комою. Ні C програміст принаймні невеликий шматочок компетенції заплутається або при зустрічі з лінії «спіткнувся»: } while(something);.
Лундін

1

Ну, можливо, це повертається на кілька кроків назад, але у випадку з

do
{
     output("enter a number");
     int x = getInput();

     //do stuff with input
}while(x != 0);

Це було б можливо, хоча і не обов’язково для читання

int x;
while(x = getInput())
{
    //do stuff with input
}

Тепер, якщо ви хочете використовувати число, відмінне від 0, щоб вийти з циклу

while((x = getInput()) != 4)
{
    //do stuff with input
}

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


0

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

int main() {
    char c;

    do {
        printf("enter a number");
        scanf("%c", &c);

    } while (c < '0' ||  c > '9'); 
}

може стати таким:

int main() {
    char c = askForCharacter();
    while (c < '0' ||  c > '9') {
        c = askForCharacter();
    }
}

char askForCharacter() {
    char c;
    printf("enter a number");
    scanf("%c", &c);
    return c;
}

(вибачте за неправильний синтаксис; я не програміст на С)

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