Найкращі практики управління винятками в Java або C # [закрито]


117

Я застряг, вирішуючи, як поводитися з винятками у своїй програмі.

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

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

Ось приклад типового коду (в JAVA) типового методу)

public boolean doSomething(Object p_somthingToDoOn)
{
    boolean result = false;

    try{
        // if dirty object then clean
        doactualStuffOnObject(p_jsonObject);

        //assume success (no exception thrown)
        result = true;
    }
    catch(Exception Ex)
    {
        //don't care about exceptions
        Ex.printStackTrace();
    }
    return result;
}

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

Підсумовуючи основні питання:

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

Слідування / редагування

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

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

Крім того, слідкуйте за кодом гниття за допомогою надмірної спроби / лову або не надаючи винятку його поваги (виняток попереджає систему, що ще потрібно попередити?).

Також це хороший коментар щодо вибору від m3rLinEz .

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

З цього коментаря виникає кілька питань, над якими слід замислюватися при роботі з винятками:

  • Який сенс кидати цей виняток?
  • Як має сенс поводитися з цим?
  • Чи дійсно абонент дбає про виняток чи їх просто хвилює, чи дзвінок був успішним?
  • Чи вимушене примушувати абонента керувати потенційним винятком?
  • Ви поважаєте ідоми мови?
    • Вам справді потрібно повернути прапор успіху, як булевий? Повернення булевого (або int) - це більше C-мислення, ніж Java (у Java ви просто обробляєте виняток).
    • Дотримуйтесь конструкцій управління помилками, пов'язаних з мовою :)!

Хоча стаття від спільноти Oracle є в java, це загальна порада щодо досить широкого класу мов. Приємна стаття, саме те, що я шукав.
Кролик із

Відповіді:


61

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

Що стосується ваших питань:

  1. Вам слід ловити лише винятки, з якими ви справді можете впоратися. Просто вилов винятків - це не правильна справа в більшості випадків. Є декілька винятків (наприклад, винятки в журналі та маршаллінгу між потоками), але навіть у цих випадках ви, як правило, повинні переосмислити винятки.
  2. У вашому коді напевно не повинно бути багато заяв про спроби / ловіння. Знову ж таки, ідея полягає лише у вилученні винятків, з якими ви можете впоратися. Ви можете включити найвищий обробник винятків, щоб перетворити будь-які необроблені винятки в щось дещо корисне для кінцевого користувача, але в іншому випадку не слід намагатися ловити кожен виняток у кожному можливому місці.

Що стосується того, чому хтось хотів би кодів помилок замість винятків ... Я завжди вважав, що дивно, що HTTP все ще використовує коди помилок, хоча моя програма генерує виняток. Чому HTTP не може дозволити мені передавати виняток через "є"?
Трежказ

найкраще, що ви можете зробити, - загорнути коди помилок у монаду
lindenrovio

@Trejkaz Ви не хочете повертати користувачам детальну інформацію про винятки, оскільки це ризик для безпеки. Ось чому HTML-сервери повертають коди помилок. Це також проблема локалізації для повернення повідомлення про помилку, і, ймовірно, робить HTML більшим за розміром і повільніше повертатись. Все це, чому я думаю, що HTML-сервери повертають коди помилок.
Дідьє А.

Я думаю, що краще сказати: "Вам слід подавити лише винятки, з якими ви справді можете впоратися".
Дідьє А.

@didibus Чому, на вашу думку, проблеми безпеки повинні створювати незручності в розвитку? Я ніколи нічого не говорив про виробництво. Що стосується локалізації повідомлень про помилки, то цілий веб-сайт уже має цю проблему, і, здається, люди справляються.
Трежказ

25

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

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

Загалом я використовую такі правила:

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

Я вважаю такий код запахом:

try
{
    //do something
}
catch(Exception)
{
   throw;
}

Цей код не має жодної точки і не повинен включатися.


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

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

Код служить крапкою: Ви можете встановити точку розриву при "кидці".
Раухоц

3
Слабкий момент, ви можете сказати VS, щоб він перервався на будь-який викинутий виняток, або ви можете його звузити і вибрати конкретний виняток. У VS2008 є пункт меню під налагодженням (Вам потрібно буде налаштувати панелі інструментів, щоб знайти його) Викликані винятки
JoshBerke

Приклад "кодового запаху" має певний побічний ефект, навіть у тому простому вигляді. Якщо // do somethingвключають будь-які try/finallyблоки навколо точки, яка кидає, finallyблоки виконуватимуться перед catchблоком. Без цього try/catchвиняток пролетить до вершини стека, не finallyвиконуючи жодних блоків. Це дозволяє обробнику верхнього рівня вирішувати, чи потрібно виконувати finallyблоки.
Даніель Ервікер

9

Я хотів би порекомендувати ще одне хороше джерело з цієї теми. Це інтерв'ю з винахідниками C # та Java, Андерсом Хейлсбергом та Джеймсом Гослінг відповідно на тему перевіреного виключення Java.

Невдача та винятки

Також є великі ресурси внизу сторінки.

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

Білл Веннерс : Ви згадали про проблему з масштабованістю та версіями стосовно перевірених винятків. Чи можете ви пояснити, що ви маєте на увазі під цими двома питаннями?

Андерс Хейльсберг : Почнемо з версії, тому що проблеми там досить легко побачити. Скажімо, я створюю метод foo, який заявляє, що він викидає винятки A, B і C. У другій версії foo я хочу додати купу функцій, і тепер foo може кинути виняток D. Це для мене переломна зміна додайте D до пункту кидок цього методу, оскільки існуючий абонент цього методу майже точно не обробляє цей виняток.

Додавання нового винятку до пункту кидків у новій версії порушує код клієнта. Це як додати метод до інтерфейсу. Після публікації інтерфейсу він незмінний для всіх практичних цілей, оскільки будь-яка його реалізація може мати методи, які ви хочете додати в наступній версії. Тож вам доведеться створити новий інтерфейс. Так само, як і у винятках, вам доведеться створити цілком новий метод, який називається foo2, який викидає більше винятків, або вам доведеться спіймати виняток D у новому foo та перетворювати D у A, B або C.

Білл Веннерс : Але ви все одно не порушуєте їх код, навіть мовою без перевірених винятків? Якщо нова версія foo створить новий виняток, що клієнти повинні подумати про обробку, чи не порушений їх код лише тим, що вони не очікували цього винятку, коли писали код?

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

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

EDIT: Додано більше інформації про конверсію


Дякую за це! Я оновив своє запитання інформацією про вашу відповідь!
AtariPete

Здається, містер Хейлсберг виправдовує поводження з виключеннями Покемона. Одна з величезних проблем з дизайном винятків в Java і C # є те , що занадто багато інформації кодується в типі виключення, в той час як інформація повинна повинна зберігатися в тих випадках виключення не доступні в будь-якому несуперечливим чином. Винятки повинні розповсюджувати стек викликів до тих пір, поки всі ненормальні умови, представлені цим, не будуть вирішені; на жаль, тип винятку - навіть якщо він визнаний - мало вказує на те, чи вирішена ситуація. Якщо виняток не визнано ...
supercat

... ситуація ще гірша. Якщо виклик коду FetchDataі він викидає виняток несподіваного типу, він не може знати, чи це виняток просто означає, що дані недоступні (у такому випадку можливість коду обходитись без цього "вирішила б" їх, або чи означає це, що CPU горить, і система повинна зробити "безпечне вимкнення" при першій же можливості. Здається, містер Хейльсберг пропонує, що код повинен вважати перший; можливо, це найкраща можлива стратегія з огляду на існуючі ієрархії винятків, але все ж це здається хитким.
supercat

Я погоджуюсь, що кидати Екстрасін - це смішно, адже майже все може викинути виняток, ви просто повинні припустити, що це може статися. Але коли ви вказуєте винятки, такі як кидки A, B, C, для мене це лише примітка, вона повинна використовуватися як блок коментарів. Це як би сказати, ей, клієнт моєї функції, ось рада, можливо, ви хочете мати справу з A, B, C, тому що ці помилки, ймовірно, трапляються при використанні мене. Якщо ви додасте D у майбутньому, це не є великою справою, якщо з ним не вирішується, але це як додавання нової документації. О, до речі, тепер також було б корисно спіймати D.
Didier A.

8

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

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

  • Заради ремонтопридатності завжди виключайте журнал винятків, щоб, коли ви починаєте бачити помилки, журнал допоможе вказувати вам на місце, яке, швидше за все, почалася вашою помилкою. Ніколи не залишайте printStackTrace()або подобається це, швидше за все, хтось із ваших користувачів з часом отримає одне із цих слідів стека та точно знає , що з цим робити.
  • Ловіть винятки, з якими ви можете впоратись, і лише ті, що впорядковуються , не просто кидайте їх на стек.
  • Завжди ловіть певний клас винятків, і, як правило, ви ніколи не повинні ловити тип Exception, ви, швидше за все, проковтнете важливі винятки.
  • Ніколи (ніколи) не спіймай Error!! , що означає: Ніколи не ловіть Throwables, оскільки Errors є підкласами останнього. Errors - проблеми, з якими ви, швидше за все, ніколи не зможете впоратися (наприклад OutOfMemory, або інші проблеми JVM)

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


Перевірені винятки не є проблемою у C #, оскільки їх немає.
клент

3
Імхо, іноді корисно помилятися: я пам’ятаю дуже інтенсивну пам’ять Java, яку я написав. Я зловив OutOfMemory-Ex і показав користувачеві повідомлення про те, що у нього немає пам’яті, що він повинен вийти з інших програм і сказав йому, як запустити jvm з виділеним більше місця для купи. Я думаю, це допомогло.
Лена Шіммель

5

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


3

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

Вих.

try
{
   sendMessage();

   if(message == success)
   {
       doStuff();
   }
   else if(message == failed)
   {
       throw;
   }
}
catch(Exception)
{
    logAndRecover();
}

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


2

Все вищезазначене видається розумним, і часто на вашому робочому місці може бути встановлена ​​політика. В нашому місці ми визначили типи винятків: SystemException(невірно) та ApplicationException(перевірено).

Ми домовилися, що SystemExceptions навряд чи підлягає відшкодуванню, і будемо обробляти один раз на вершині. Для того, щоб забезпечити подальший контекст, наші SystemExceptions є exteneded , щоб вказати , де вони мали місце, наприклад RepositoryException, ServiceEceptionі т.д.

ApplicationExceptions може мати таке ділове значення, як InsufficientFundsExceptionі з ним слід керуватися кодом клієнта.

Witohut конкретний приклад, важко коментувати вашу реалізацію, але я ніколи не використовував би коди повернення, вони є проблемою обслуговування. Ви можете проковтнути Виняток, але вам потрібно вирішити, чому, і завжди реєструвати подію та стек-трек. Нарешті, оскільки ваш метод не має іншої обробки, він є надлишковим (за винятком інкапсуляції?), Тож doactualStuffOnObject(p_jsonObject);може повернути булевий стан!


1

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

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

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


1

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

Також розгляньте можливість використання фільтра винятку при реєстрації винятків для діагностичних цілей. VB має підтримку мови для фільтрів винятків. Посилання на блог Греггма має реалізацію, яку можна використовувати з C #. Фільтри винятку мають кращі властивості для налагодження над уловом та повторним скиданням. Зокрема, ви можете записати проблему у фільтр і дозволити продовження розповсюдження винятку. Цей метод дозволяє приєднати відладчик JIT (Just in Time), щоб мати повний оригінальний стек. Повторне відсікання відрізає стек у точці, де вона була скинута.

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

// throws NumberNotHexidecimalException
int ParseHexidecimal(string numberToParse); 

bool TryParseHexidecimal(string numberToParse, out int parsedInt)
{
     try
     {
         parsedInt = ParseHexidecimal(numberToParse);
         return true;
     }
     catch(NumberNotHexidecimalException ex)
     {
         parsedInt = 0;
         return false;
     }
     catch(Exception ex)
     {
         // Implement the error policy for unexpected exceptions:
         // log a callstack, assert if a debugger is attached etc.
         LogRetailAssert(ex);
         // rethrow the exception
         // The downside is that a JIT debugger will have the next
         // line as the place that threw the exception, rather than
         // the original location further down the stack.
         throw;
         // A better practice is to use an exception filter here.
         // see the link to Exception Filter Inject above
         // http://code.msdn.microsoft.com/ExceptionFilterInjct
     }
}

Чи використовуєте ви зразок, як TryXXX чи ні, це більше питання стилю. Питання про зловлення всіх винятків та їх проковтування не є питанням стилю. Переконайтеся, що дозволено поширювати несподівані винятки!


Мені подобається шаблон TryXXX в .net.
JoshBerke

1

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

Наприклад, java.lang.reflect.Array має статичний setметод:

static void set(Object array, int index, Object value);

Ш шлях був би

static int set(Object array, int index, Object value);

... при цьому повернене значення є показником успіху. Але ти вже не в світі С.

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

Як зазначали інші - ви повинні бути максимально конкретними у винятку, який ви ловите.


це дуже вагомий коментар, поважайте мову та як вона традиційно керує такими питаннями. Не приводьте мислення на C у світ Java.
AtariPete

0

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

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

Редагувати: Що стосується питання про завершення всього в пробі / улов, я думаю, що відповідь - так. Винятки повинні бути настільки рідкісними у вашому коді, що код у блоці вилову виконується так рідко, що він зовсім не досягає продуктивності. Виняток має становити стан, коли ваша державна машина зламалася і не знає, що робити. Принаймні, повторно скиньте виняток, який пояснює те, що відбувалося в той час, і всередині нього є спіймане виняток. "Виняток у методі doSomeStuff ()" не дуже корисний для тих, хто повинен з'ясувати, чому він зламався під час відпустки (або на новій роботі).


Не забувайте, що встановлення блоку винятків теж коштує ...
devstuff

0

Моя стратегія:

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

Якщо функція повинна щось повертати, тоді, коли виняток / помилка повертається нульовою , інакше повертається елемент.

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

У кожному випадку перед тим, як повернути що-небудь, запишіть помилку.


0

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


я цілком згоден. Я робив лише Ex.printStackTrace (); як приклад чогось, що я робив у лові (тобто не перекидаючи).
AtariPete

0

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

Проте, якщо вони розумно використовуються, вони творять чудеса в читанні, але вам слід просто дотримуватися двох простих правил:

  • використовувати їх (щадно) на низькому рівні, щоб знайти проблеми з обробкою бібліотеки та повернути їх у основний логічний потік. Більшість потрібних нам помилок повинні надходити з самого коду, як частини самих даних. Навіщо створювати особливі умови, якщо дані, що повертаються, не особливі?

  • використовувати один великий обробник на вищому рівні, щоб керувати будь-якими або всіма дивними умовами, що виникають у коді, які не вловлюються на низькому рівні. Зробіть щось корисне з помилками (журнали, перезавантаження, відновлення тощо).

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

Пол.


0

Я можу трохи запізнитися з відповіддю, але поводження з помилками - це те, що ми завжди можемо змінювати і розвиватись з часом. Якщо ви хочете прочитати щось більше про цю тему, я написав пост у своєму новому блозі про це. http://taoofdevelopment.wordpress.com

Щасливе кодування.

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