Найкраща практика поводження з винятками у програмі Windows Forms?


118

Наразі я переглядаю свою першу заявку на Windows Forms. Зараз я прочитав декілька книг C #, тому порівняно добре розумію, які мовні особливості C # має справу з винятками. Вони всі цілком теоретичні, однак, у мене поки що немає почуття, як перетворити основні поняття в хорошу модель обробки винятків у моїй заявці.

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

Основні речі, які я зараз намагаюся розробити, це:

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

Всі поради вдячно отримані!

Відповіді:


79

Ще кілька біт ...

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

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

Ніколи не "ковтайте" виняток, за винятком найбільш добре задокументованих випадків, коли ви абсолютно впевнені, що викид, який викидаєте, є придатним. Це майже ніколи не буде. (А якщо це так, переконайтеся , що ви ковтання тільки конкретним класом виключення - НЕ коли - небудь проковтнути System.Exception.)

Створюючи бібліотеки (використовувані вашою програмою), не ковтайте винятки, і не бійтеся пускати виключення. Не кидайте повторно, якщо у вас є щось корисне додати. Ніколи (в C #) не робіть цього:

throw ex;

У міру стирання стека викликів. Якщо вам потрібно повторно кинути (що час від часу є необхідним, наприклад, при використанні блоку обробки винятків бібліотеки підприємств), використовуйте наступне:

throw;

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

Обробка винятків у .NET - це більше мистецтво, ніж наука. Тут кожен зможе поділитися своїми фаворитами. Це лише декілька порад, які я взяв за допомогою .NET з 1-го дня, методи, які рятували моє бекон не один раз. Ваш пробіг може відрізнятися.


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

2
@supercat Написавши специфічні підкласи ApplicationExceptionдля тих випадків відмов, які додаток повинен розумно вміти розмежовувати та обробляти.
Метт Енрайт

@Matt Enright: Безумовно, можна зловити власні винятки, але я не знаю нічого, що віддалено нагадує конвенцію, за допомогою якої модулі, які викидають винятки, вказують, чи вказують вони на корупцію будь-якої держави поза межами того, що має на увазі невдача. В ідеалі такий метод, як SuperDocument.CreateFromFile (), може досягти успіху, викинути виключення CleanFailure або викинути SomethingReallyBadHappenedException. На жаль, якщо хтось не загортає все, що може кинути виняток у власному блоці вилову, немає жодного способу дізнатися, чи є InvalidOperationException ...
supercat

@Matt Enright: ... має бути загорнуто в CleanFailureException або щось реальноBadHappenedException. І загортання всього в індивідуальні блоки пробного лову перешкоджає б усьому цілі - у першу чергу є винятки.
supercat

2
Я думаю, що це поганий дизайн бібліотеки, щоб приховати режими відмови з неточним типом винятків. Не кидайте FileNotFoundException, якщо ви насправді маєте на увазі IOException або InvalidDataException, оскільки програма повинна реагувати по-різному на кожен випадок. Такі речі, як StackOverflowException або OutOfMemoryException, не можуть розумно обробляти додаток, тому просто нехай вони пустують, оскільки добре сприйнята програма додатково має централізований обробник "останньої інстанції".
Метт Енрайт

63

Тут є чудова стаття коду CodeProject . Ось кілька важливих моментів:

  • План найгіршого *
  • Перевірте це рано
  • Не довіряйте зовнішнім даним
  • Єдині надійні пристрої: відео, миша та клавіатура.
  • Неможливо також писати записи
  • Код надійно
  • Не кидайте новий виняток ()
  • Не ставте важливу інформацію про виключення в поле Повідомлення
  • Помістіть один загін (виняток ex) на кожну нитку
  • Загальні винятки, що трапляються, повинні бути опубліковані
  • Виняток з журналу.ToString (); ніколи не записуйте лише Exception.Message!
  • Не вловлюйте (виняток) більше одного разу за нитку
  • Ніколи не ковтайте винятки
  • Код очищення слід розміщувати, нарешті, блоками
  • Використовуйте "використання" скрізь
  • Не повертайте спеціальних значень за помилок
  • Не використовуйте винятки, щоб вказати на відсутність ресурсу
  • Не використовуйте обробку винятків як спосіб повернення інформації з методу
  • Використовуйте винятки для помилок, які не слід ігнорувати
  • Не очищайте слід стека під час повторного накидання винятку
  • Уникайте зміни винятків, не додаючи смислового значення
  • Винятки мають бути позначені [Serializable]
  • Якщо ви сумніваєтесь, не стверджуйте, киньте виняток
  • Кожен клас винятків повинен мати щонайменше три оригінальних конструктора
  • Будьте обережні при використанні події AppDomain.UnhandledException
  • Не винаходити колесо
  • Не використовувати неструктуровану обробку помилок (VB.Net)

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

Посилання на посилання на цю статтю минуло. Проект Code передбачає, що стаття може бути перенесена сюди .
DavidRR

15

Зауважте, що у Windows Forms є свій механізм обробки виключень. Якщо натиснути кнопку у формі та її обробник викине виняток, який не потрапив у обробник, Windows Forms відобразить власний діалоговий діалог Unhandled Exception.

Щоб запобігти відображенню діалогового діалогу винятків та вилучення таких винятків для реєстрації та / або надання власного діалогового вікна помилок, ви можете долучитись до події Application.ThreadException перед викликом до Application.Run () у вашому методі Main ().


Дякую за цю пропозицію, я дізнався про це занадто пізно, я просто перевірив це у linqpad, працює як очікується, делегат: void Form1_UIThreadException (відправник об'єкта, ThreadExceptionEventArgs t) Ще одне хороше джерело з цієї теми - richnewman.wordpress.com/2007/ 04/08 /… Для загального керованого оброблення винятків: подія AppDomain.UnhandledException призначена для необроблених винятків, викинутих з потоку non-main-UI.
zhaorufei

14

Усі поради, розміщені тут, є добрими і варто прислухатися.

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

Наївне правило: "блоки" спробувати / ловити дорого ". Це насправді не так. Спробувати не дорого. Це ловить, коли система повинна створити об’єкт Exception і завантажити його слідом стека, що дорого. Існує багато випадків, коли виняток є, достатньо винятковим, що цілком чудово загортати код у блок спробу / лову.

Наприклад, якщо ви заповнюєте словник, це:

try
{
   dict.Add(key, value);
}
catch(KeyException)
{
}

часто швидше, ніж робити це:

if (!dict.ContainsKey(key))
{
   dict.Add(key, value);
}

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

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

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


3
у випадку dict[key] = value
диктату

9

Ось декілька вказівок, яких я дотримуюся

  1. Fail-Fast: Це скоріше правило, що генерує виняток. Для кожного припущення, яке ви робите, та кожного параметра, який ви отримуєте у функції, зробіть перевірку, щоб переконатися, що ви починаєте з потрібних даних та чи припущення вам Ви правильні. Типові перевірки включають: аргумент недійсний, аргумент у очікуваному діапазоні тощо.

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

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

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

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

  6. Однозначно є випадки, коли потрібні порожні блоки вилову, я вважаю, що люди, які говорять інакше, не працювали над кодами, що склалися протягом кількох релізів. Але їх слід коментувати та переглядати, щоб переконатися, що вони справді потрібні. Найбільш типовим прикладом є розробники, які використовують try / catch для перетворення рядка в ціле число замість ParseInt ().

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


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

Ви мали на увазі обробляти Application.ThreadExceptionподію, коли ви посилалися на подію "потік без керування винятком"?
Джефф Б

4

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

Я ненавиджу його, коли бачу такий код, як:

try
{
   // some stuff is done here
}
catch
{
}

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

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

Я думаю, що код слід писати проактивно і винятки повинні бути у виняткових ситуаціях, а не уникнути тестування на умови.


@Kiquenet Вибачте, мені було трохи незрозуміло. Що я мав на увазі: не робіть таких порожніх ловів, замість цього додайте обробник до Dispatcher.UnhandledException і додайте принаймні журнал, щоб його не їли без панірувальних сухарів :) Але, якщо у вас немає вимоги мовчазного компонента, слід завжди включати винятки у ваш дизайн. Киньте їх, коли їх потрібно кинути, складіть чіткі контракти для вас Інтерфейси / API / будь-який клас.
LuckyLikey

4

Я просто виходжу, але коротко розкажу про те, де використовувати обробку виключень. Я спробую звернутися до Ваших інших питань, коли повернусь :)

  1. Явно перевірте, чи всі відомі умови помилки *
  2. Додайте спробу / ловіть код, якщо ви не впевнені, чи змогли ви вирішити всі справи
  3. Додайте спробу / лову навколо коду, якщо інтерфейс .NET, який ви телефонуєте, кидає виняток
  4. Додайте спробу / спіймання коду, якщо він переступив для вас поріг складності
  5. Додайте спробу / ловіть код, якщо для перевірки обгрунтованості: Ви стверджуєте, що НЕ БУДЕ НІКОЛИ ДІЙСЯ
  6. Як правило, я не використовую винятки в якості заміни повертаючих кодів. Це добре для .NET, але не для мене. У мене є винятки (hehe) з цього правила, однак це залежить від архітектури програми, над якою ви працюєте.

* В межах розуму. Немає необхідності перевіряти, чи скажіть, що космічний промінь потрапив у ваші дані, спричинивши перевертання пари біт. Розуміння того, що є «розумним» - це набутий навик для інженера. Важко оцінити, але інтуїтивно легко. Тобто, я можу легко пояснити, чому я використовую спробу / ловити в будь-якому конкретному випадку, але мені дуже важко нав'язувати іншого цим самим знанням.

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


4

Золоте правило, яке намагалися дотримуватися, - це обробляти виняток якомога ближче до джерела.

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

Ще одне правило, якого я намагаюся дотримуватися, - це не використовувати try / catch як форму потоку програми, тому я перевіряю файли / з'єднання, переконуюсь, що об’єкти були ініційовані, тощо .. перед їх використанням. Спробуйте / ловити слід за винятками, те, що ви не можете контролювати.

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


Це "золоте правило" в кращому випадку довільне.
Еван Харпер

3

ви можете захопити подію ThreadException.

  1. Виберіть проект програми Windows у Провіднику рішень.

  2. Відкрийте створений файл Program.cs, двічі клацнувши по ньому.

  3. Додайте наступний рядок коду до верхньої частини кодового файлу:

    using System.Threading;
  4. У методі Main () додайте наступне як перший рядок методу:

    Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
  5. Додайте нижче метод Main ():

    static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        // Do logging or whatever here
        Application.Exit();
    }
  6. Додайте код для обробки необробленого винятку в обробнику події. Будь-який виняток, який не обробляється ніде в додатку, обробляється вищевказаним кодом. Найчастіше цей код повинен фіксувати помилку та відображати повідомлення користувачеві.

refrence: https://blogs.msmvps.com/deborahk/global-exception-handler-winforms/


@Kiquenet Вибачте, я не знаю про VB
Omid-RH

2

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

Не кидайте повторно, якщо дозвіл на виняток зросте так само добре. Ніколи не дозволяйте помилкам пройти непомітно.

приклад:

void Main()
{
  try {
    DoStuff();
  }
  catch(Exception ex) {
    LogStuff(ex.ToString());
  }

void DoStuff() {
... Stuff ...
}

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


1

Коли слід повторно кинути виняток?

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

Чи слід намагатися мати якийсь центральний механізм поводження з помилками?

Я пишу файл журналу ... досить просто для програми WinForm

Чи обробляються винятки, які можуть бути викинуті, мають результативність порівняно з попереднім тестуванням таких речей, як, чи існує файл на диску?

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

Чи повинен весь виконуваний код міститись у блоках спробу-лову нарешті?

дріжджі

Чи є моменти, коли порожній блок лову може бути прийнятним?

Так, скажімо, ви хочете показати дату, але у вас немає поняття, як зберігалися ці дати (dd / mm / yyyy, mm / dd / yyyy і т. Д.), Ви спробуєте розібрати її, але якщо це не вдасться, просто продовжуйте продовжувати .. Якщо це для вас неважливо ... я б сказав так, є


1

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

Я використовував порожні блоки записів у місцях, де користувач може зробити щось, що насправді не є "неправильним", але він може винести виняток ... Приклад, який спадає на думку, - це GridView, якщо користувач DoubleCLicks сірий заповнювач комірка у верхньому лівому куті запустить подію CellDoubleClick, але комірка не належить до ряду. У такому випадку ви цього не робите дійсно потрібно опублікувати повідомлення, але якщо ви його не впіймаєте, воно видасть користувачеві помилку обліку виключень.


1

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

Try
{
int a = 10 / 0;
}
catch(exception e){
//error logging
throw;
}

це призведе до закінчення сліду стека у виписці. (уникайте цього)

catch(Exception e)
// logging
throw e;
}

чи все ще це стосується останніх версій .NET Framework та .NET Core?
LuckyLikey

1

n мій досвід я вважаю за потрібне ловити винятки, коли знаю, що буду їх створювати. Для випадків, коли я перебуваю у веб-програмі та я роблю Response.Redirect, я знаю, що я отримаю System.ThreadAbortException. Оскільки це навмисно, я просто впіймаю конкретний тип і просто проковтну.

try
{
/*Doing stuff that may cause an exception*/
Response.Redirect("http:\\www.somewhereelse.com");
}
catch (ThreadAbortException tex){/*Ignore*/}
catch (Exception ex){/*HandleException*/}

1

Я глибоко погоджуюся з правилом:

  • Ніколи не дозволяйте помилкам пройти непомітно.

Причина в тому, що:

  • Коли ви вперше запишете код, швидше за все, ви не матимете повного знання тристороннього коду, .NET FCL-файлу або останніх внесків своїх колег. Насправді ви не можете відмовитись від написання коду, поки не будете добре знати всі можливі винятки. Так
  • Я постійно вважаю, що я використовую try / catch (Exception ex) лише тому, що я хочу захистити себе від невідомих речей, і, як ви помітили, я ловлю виняток, а не більш конкретний, наприклад OutOfMemoryException тощо. І я завжди роблю виняток що вискакує на мене (або QA) від ForceAssert.AlwaysAssert (false, ex.ToString ());

ForceAssert.AlwaysAssert - це мій особистий шлях Trace.Assert просто незалежно від того, визначено макрос DEBUG / TRACE.

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

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

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

Для коду WinForms золотим правилом, якого я завжди дотримуюся, є:

  • Завжди намагайтеся / виловити (виняток) код обробника події

це захистить ваш інтерфейс завжди корисним.

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

Виняток має бути мало шансів, інакше це не винятки.


-1

Ви повинні думати про користувача. Аварія програми - останнящо користувач хоче. Тому будь-яка операція, яка може завершитись, повинна мати блок пробування на рівні інтерфейсу. Не обов’язково використовувати спробу лову в кожному методі, але кожен раз, коли користувач щось робить, він повинен мати можливість обробляти загальні винятки. Це ні в якому разі не звільняє вас від перевірки всього, щоб запобігти виняткам у першому випадку, але немає жодної складної програми без помилок, і ОС може легко додати несподівані проблеми, тому ви повинні передбачити несподіване і переконатися, чи бажає користувач використовувати його операції втрати даних не буде, оскільки програма виходить з ладу. Ніколи не потрібно дозволяти вашому додатку виходити з ладу, якщо ви виберете винятки, він ніколи не буде в невизначеному стані, і користувач ВЖЕ неприємно спричиняє аварію. Навіть якщо виняток знаходиться на самому верхньому рівні, не збій означає, що користувач може швидко відтворити виняток або принаймні записати повідомлення про помилку, і тому значно допоможе вам усунути проблему. Звичайно, набагато більше, ніж отримувати просте повідомлення про помилку, а потім бачити лише діалогове вікно про помилку Windows чи щось подібне.

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

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


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