Винятки як твердження чи помилки?


10

Я професійний програміст на C і любитель програми Obj-C (OS X). Останнім часом я спокусився розширитись на C ++ через дуже ситний синтаксис.

Поки що кодування я не мав великого значення з винятками. Objective-C має їх, але політика Apple досить сувора:

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

Здається, C ++ частіше використовує винятки. Наприклад, приклад wikipedia у RAII видає виняток, якщо файл неможливо відкрити. Objective-C буде return nilз помилкою, надісланою вихідним парам. Зокрема, здається, що std :: ofstream можна встановити будь-яким способом.

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

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

Правка: Хоча це не зовсім актуально, я, мабуть, повинен уточнити, що nilтаке. Технічно це те саме, що NULL, але річ у тому, що це нормально, щоб надіслати повідомлення nil. Так ви можете зробити щось на кшталт

NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];

[obj retain];

навіть якщо перший дзвінок повернувся nil. І як ви ніколи не робите *objв Obj-C, немає ніякого ризику відхилення вказівника NULL.


Імхо, було б краще, якби ви показали фрагмент коду Objective C, як ви працюєте з помилками. Здається, люди говорять про щось інше, ніж ви просите.
Gangnus

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

Здається, для C ++ вам потрібно досить виправдання, щоб не використовувати їх. Відповідно до реакцій тут.
Gangnus

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

"Мені не подобається ... використовувати винятки для речей, які не є винятковими", - погодився.
Gangnus

Відповіді:


1

Здається, C ++ частіше використовує винятки.

Я б хотів би запропонувати фактично менше, ніж Objective-C, в деяких аспектах, тому що стандартна бібліотека C ++ взагалі не спричиняє помилок програміста, таких як вихід поза межі послідовності з випадковим доступом у його найбільш поширеній формі дизайну (у operator[], тобто) або намагаючись знешкодити недійсний ітератор. Мова не кидає доступ до масиву поза межами, або перенаправлення нульового вказівника, або що-небудь подібне.

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

Sutter навіть рекомендує уникати винятків у таких випадках у стандартах кодування C ++:

Основним недоліком використання виключення для повідомлення про помилку програмування є те, що ви насправді не хочете, щоб розмотування стека виникало, коли ви хочете, щоб запуск налагоджувача запускався в точній рядку, де було виявлено порушення, при цьому стан лінії є неушкодженим. У підсумку: Є помилки, які, на вашу думку, можуть статися (див. Пункти 69 до 75). У всьому іншому, що не повинно, і виною програміста, якщо він є, є assert.

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

Зовнішні винятки

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

Операції

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

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

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

Продуктивність

Іншим керівним фактором того, чи потрібно використовувати винятки чи ні, є ефективність, і я не маю на увазі нав'язливий, копійчастий, контрпродуктивний спосіб. Дуже багато компіляторів C ++ реалізують те, що називається "Обробка виключень з нульовою витратою".

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

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

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

Непомилки

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

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

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

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

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

Запитання

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

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

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

Моє обґрунтування цього полягає в тому, що я не звик бачити команди у виробничих середовищах ретельно перевіряти наявність усіх можливих помилок, на жаль, коли повертаються коди помилок. Якби вони були ретельними, деякі C-API можуть зіткнутися з помилкою практично при кожному виклику API C, і ретельна перевірка потребує чогось типу:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

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

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

Мені здається, оскільки вказівники рідкісні, мені доведеться переходити з внутрішніми прапорами помилок, якщо я вирішу уникати винятків.

Я б не обов'язково говорив, що вказівники рідкісні. В C ++ 11 і далі існують навіть методи, щоб знайти основні вказівники даних контейнерів та нове nullptrключове слово. Зазвичай вважається нерозумним використовувати сировинні покажчики для володіння / управління пам’яттю, якщо ви можете використовувати щось на зразок, unique_ptrвраховуючи, наскільки важливим є відповідність RAII за наявності винятків. Але необроблені покажчики, які не володіють пам’яттю та керують нею, не обов'язково вважаються настільки поганими (навіть від таких людей, як Саттер та Струструп), а іноді дуже практичні як спосіб вказувати на речі (поряд з показниками, які вказують на речі).

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


14

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

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

Але ось простий факт. Винятки виконують свою роботу автоматично. Коли ви кидаєте виняток, RAII бере на себе і все обробляє компілятор. Використовуючи коди помилок, ви повинні пам'ятати, щоб перевірити їх. Це по суті робить винятки значно безпечнішими, ніж коди помилок - вони як би перевіряють себе. Крім того, вони більш виразні. Накиньте виняток, і ви можете вивести рядок із повідомленням про помилку і навіть може містити конкретну інформацію, наприклад, "Неправильний параметр X, який мав значення Y замість Z". Отримайте код помилки 0xDEADBEEF і що саме пішло не так? Я впевнений, що документація є повною та актуальною, і навіть якщо ви отримаєте "Неправильний параметр", він ніколи не скаже вам, якийпараметр, яке значення воно мало і яке значення воно мало мати. Якщо ви ловите за посиланням, вони також можуть бути поліморфними. Нарешті, винятки можуть бути викинуті з місць, де коди помилок ніколи не можуть, як конструктори. А як щодо загальних алгоритмів? Як std::for_eachбуде оброблятися ваш код помилки? Підказка: Це не так.

Винятки значно перевершують коди помилок у будь-якому відношенні. Справжнє питання полягає у винятках та твердженнях.

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

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

Невдачі в твердженні завжди є фатальними, помилки, які не можна виправити. Пошкодження пам’яті, така річ.

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

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


Альтернатива не обов'язково є простим int. Я б більше шансів використовувати щось схоже на NSError, до якого я звик із Obj-C.
Пер Йоханссон

Він порівнює не C, а ціль C. Це велика різниця. І його коди помилок пояснюють рядки. Все, що ви говорите, правильно, тільки не відповіді на це питання якось. Жодного образи не означало.
Gangnus

Кожен, хто використовує Objective-C, звичайно, скаже вам, що ви абсолютно, абсолютно неправі.
gnasher729

5

Винятки були винайдені з причини, щоб уникнути того, щоб весь ваш код виглядав так:

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

Натомість ви отримуєте щось схоже на наступне, з одним обробником винятків, який знаходиться в mainіншому місці або зручно розташованому:

result1 = function1();
result2 = function2();

Деякі люди стверджують, що підхід не є винятком, але, на мою думку, читабельність переважає незначний приріст продуктивності, особливо коли ви включаєте час виконання для всіх if (!success)котлів, які вам доведеться посипати скрізь, або ризик важче налагодити segfault, якщо цього не зробити ' t включайте його, а врахування шансів на винятки трапляються порівняно рідко.

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


1
Це було б більше сенсу, якщо код без винятків насправді виглядає так, але зазвичай це не так. Ця закономірність з’являється, коли error_handler ніколи не повертається, але рідко інакше.
Пер Йоханссон

1

У публікації, на яку ви посилаєтесь, (винятки проти кодів помилок), я думаю, що відбувається дещо інша дискусія. Здається, виникає питання про те, чи є у вас глобальний список кодів #define помилок, ймовірно, доповнених такими іменами, як ERR001_FILE_NOT_WRITEABLE (або скороченим, якщо вам не пощастило). І я вважаю, що головний момент у цій темі полягає в тому, що якщо ви програмуєте на полімфортній мові, використовуючи об'єкти об'єктів, така процедурна конструкція не потрібна. Винятки можуть виражати, яка помилка трапляється просто в силу їх типу, і вони можуть інкапсулювати інформацію, наприклад, яке повідомлення для друку (та інші речі). Отже, я б вважав цю розмову однією з причин того, чи слід програмувати процедурно на об'єктно-орієнтованій мові чи ні.

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

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

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

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


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

0

У « Чистому кодексі» є чудова пара глав, яка розповідає про цю саму тему - мінус точки зору Apple.

Основна ідея полягає в тому, що ви ніколи не повертаєте нуль від своїх функцій, оскільки це:

  • Додає багато дублюється коду
  • Створює безлад ваш код - Т.е.: читати стає важче
  • Часто може призвести до помилок з нульовим покажчиком - або порушень доступу в залежності від конкретної програми "релігія"

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

Ще в часи розробки COM у мене був проект, де кожен збій викликав виняток, і кожен виняток потребував використання унікального ідентифікатора коду помилки на основі розширення стандартних винятків COM для Windows. Це був затримка попереднього проекту, де раніше використовувались коди помилок, але компанія хотіла перейти на всі об'єктно-орієнтовані та перестрибнути на смугу COM, не змінюючи занадто сильно те, що вони робили. На них знадобилося лише 4 місяці, коли кількість кодів помилок закінчилася, і мені довелося переробляти великі тракти коду, щоб замість цього використовувати виключення. Краще заощадити зусилля наперед та просто використовувати винятки розумно.


Дякую, але я не розглядаю можливість повернення NULL. Це не здається можливим перед обличчям конструкторів. Як я вже згадував, я, мабуть, повинен був би використати прапор помилки в об'єкті.
Пер Йоханссон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.