Чи образливо використовувати IDisposable і «використовувати» як засіб для отримання «масштабної поведінки» для безпеки винятків?


112

Щось, що я часто використовував у C ++, було дозволити класу Aобробляти стан входу та виходу стану для іншого класу Bчерез Aконструктор та деструктор, щоб переконатися, що якщо щось із цього діапазону кине виняток, то B матиме відомий стан, коли сфера була вимкнена. Це не є чистим RAII, наскільки це абревіатура, але це все-таки встановлена ​​закономірність.

У C # мені часто хочеться це робити

class FrobbleManager
{
    ...

    private void FiddleTheFrobble()
    {
        this.Frobble.Unlock();
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
        this.Frobble.Lock();
    }
}

Що потрібно зробити так

private void FiddleTheFrobble()
{
    this.Frobble.Unlock();

    try
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
    finally
    {
        this.Frobble.Lock();
    }
}

якщо я хочу гарантувати Frobbleдержаву при FiddleTheFrobbleповерненні. Код буде приємніше

private void FiddleTheFrobble()
{
    using (var janitor = new FrobbleJanitor(this.Frobble))
    {            
        Foo();                  // Can throw
        this.Frobble.Fiddle();  // Can throw
        Bar();                  // Can throw
    }
}

де FrobbleJanitorвиглядає приблизно так

class FrobbleJanitor : IDisposable
{
    private Frobble frobble;

    public FrobbleJanitor(Frobble frobble)
    {
        this.frobble = frobble;
        this.frobble.Unlock();
    }

    public void Dispose()
    {
        this.frobble.Lock();
    }
}

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

Питання: Чи вважатиметься вищенаведеним як зловживання usingта IDisposable?


23
+1 лише для імен класу та методу :-). Ну, щоб бути справедливим, і власне питання.
Джої

2
@Johannes: Так, я можу зрозуміти, чому - більшість людей просто обмацують свою загадку, але я просто повинен протестувати проти цього. ;-) І, до речі, +1 за подібність вашого імені.
Йоганн Герелл

Можливо, ви можете використовувати область блокування (цього) {..} для досягнення того ж результату в цьому прикладі?
оɔɯǝɹ

1
@ oɔɯǝɹ: Ви використовуєте lock () для запобігання одночасного доступу до чогось із різних потоків. Нічого спільного зі сценарієм, який я описую.
Йоганн Герелл

1
IScopeable без фіналайзера в наступній версії C # :)
Джон Рейнор

Відповіді:


33

Я не думаю, що це обов'язково. Технічно ID, що використовується, призначений для використання для речей, які не мають керованих ресурсів, але тоді директива щодо використання є лише акуратним способом реалізації загальної структури try .. finally { dispose }.

Пурист стверджував би «так - це образливо», і в пуристському розумінні це так; але більшість із нас кодує не з пуристської точки зору, а з напівхудожнього. Використовувати конструкцію "використовувати" таким чином, на мою думку, справді досить художньо.

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

Є багато інших альтернатив для цього, але, в кінцевому рахунку, я не можу придумати жодного, що буде таким охайним, як це, тому йдіть на це!


2
Я вважаю, що це також мистецьке використання "використання". Я думаю, що повідомлення Самуала Джека, яке посилається на коментар Еріка Ганнерсона, підтверджує цю реалізацію "використання". Я бачу чудові моменти з боку Андраса та Еріка.
IАнотація

1
Я поговорив з Еріком Ганнерсоном про це деякий час назад, і за його словами, це було намір дизайнерської команди C #, яка використовує позначати сфери застосування. Насправді, він постулював, що якби вони не розроблялися перед операцією блокування, можливо, навіть не було заяви про блокування. Це було б використання блоку без дзвінка Монітора або щось подібне. ОНОВЛЕННЯ: Щойно зрозумів, що наступна відповідь - Ерік Ліпперт, який також є в мовній команді. ;) Можливо, сама команда C # не повністю з цим погоджується? Контекстом мого обговорення з Ганнерсоном був клас TimedLock
Haacked

2
@Haacked - Забавно чи не так; ваш коментар повністю доводить мою думку; що ви говорили з одним хлопцем у команді, і він був усім за це; потім вказуючи Еріка Ліпперта внизу, з тієї ж команди не погоджується. З моєї точки зору; C # надає ключове слово, яке експлуатує інтерфейс, а також дає приємний вигляд кодового шаблону, який працює у багатьох сценаріях. Якщо ми не повинні зловживати цим, тоді C # повинен знайти інший спосіб нав'язати це. Так чи інакше, дизайнерам, напевно, тут потрібно завести своїх качок підряд! Тим часом: using(Html.BeginForm())сер? не проти, якщо я сер! :)
Андрас Золтан

71

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

Я вважаю це зловживанням з трьох причин.

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

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

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

Якщо ви донесли це до перегляду коду в моєму кабінеті, перше питання, яке я б поставив, - це чи справді правильно заблокувати фробла, якщо викинутий виняток? У вашій програмі очевидно, що ця річ агресивно повторно блокує фробу, незалежно від того, що відбувається. Це так? Виняток викинуто. Програма знаходиться в невідомому стані. Ми не знаємо, кинули Фу, Фіддл або Бар, чому вони кинули, або які мутації вони виконували іншим станом, які не були очищені. Чи можете ви переконати мене, що в тій жахливій ситуації завжди правильно зробити, щоб переблокувати?

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

Використання блоку "використання" для семантичного ефекту робить цей фрагмент програми:

}

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

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

bool needsLock = false;
try
{
    // must be carefully written so that needsLock is set
    // if and only if the unlock happened:

    this.Frobble.AtomicUnlock(ref needsLock);
    blah blah blah
}
finally
{
    if (needsLock) this.Frobble.Lock();
}

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


18
Хоча я би відповів на питання інакше, я вважаю, що це досить аргументоване повідомлення. Однак я сприймаю ваше твердження, що usingце ввічливість, а не питання коректності. Я вважаю, що витоки ресурсів - це питання коректності, хоча вони часто є незначними, що не є помилками з високим пріоритетом. Таким чином, usingблоки вже мають семантичний вплив на стан програми, і те, що вони перешкоджають, - це не просто "незручність" для інших процесів, але, можливо, зламане середовище. Це не означає, що різний тип смислового впливу, що передбачається питанням, також є доцільним.
kvb

13
Витік ресурсів - це питання коректності, так. Але невикористання "використання" не протікає ресурс; ресурс буде очищений, врешті-решт, при запуску фіналізатора. Очищення може бути не таким агресивним і своєчасним, як ви хочете, але це станеться.
Ерік Ліпперт

7
Чудовий пост, ось два мої центи :) Я підозрюю, що багато "зловживань" usingє через те, що це орієнтована на бідного чоловіка ткацька в C #. Те, що розробник дійсно хоче, - це спосіб гарантувати, що якийсь загальний код буде працювати в кінці блоку без необхідності дублювання цього коду. C # не підтримує AOP сьогодні (MarshalByRefObject & PostSharp не витримує). Однак трансформація компілятора використовуючого оператора дозволяє досягти простої АОП шляхом повторного призначення семантики детермінованого розміщення ресурсів. Хоча це не є гарною практикою, іноді привабливо пройти .....
Л.Бушкін

11
Хоча я, як правило, погоджуюся з вашою позицією, є деякі випадки, коли вигода від повторної цілі usingзанадто приваблива, щоб відмовитися. Найкращий приклад, який я можу придумати, - це відслідковувати та повідомляти про терміни коду. using( TimingTrace.Start("MyMethod") ) { /* code */ } Знову ж таки, це AOP - Start()фіксує час початку до початку блоку, Dispose()фіксує час завершення та реєструє активність. Це не змінює стан програми та є агностичним до винятків. Він також привабливий тим, що дозволяє уникнути невеликої кількості сантехніки, а його часте використання як візерунок пом'якшує заплутану семантику.
Л.Бушкін

6
Смішно, я завжди думав використовувати як засіб підтримки RAII. Мені здається досить інтуїтивним вважати замок типом ресурсу.
Брайан

27

Якщо ви просто хочете отримати чистий, обширний код, ви також можете використовувати лямбда, ля ля

myFribble.SafeExecute(() =>
    {
        myFribble.DangerDanger();
        myFribble.LiveOnTheEdge();
    });

де .SafeExecute(Action fribbleAction)метод обгортає блок try- catch- finally.


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

1
Ах! Гаразд, я думаю, ви маєте на увазі, що SafeExecuteце Fribbleметод, який викликає Unlock()і Lock()в завершеній спробі / нарешті. Мої вибачення тоді. Я одразу розглядав SafeExecuteяк загальний метод розширення і тому згадував відсутність стану входу та виходу. Моє ліжко.
Йоганн Герелл

1
Будьте дуже обережні з цим підходом. У випадку захоплених місцевих жителів можуть бути небезпечні незначні продовження терміну експлуатації!
Ясон

4
Юк. Навіщо ховати спробувати / ловити / нарешті? Робить ваш код прихованим для читання для інших.
Френк Швітерман

2
@Yukky Frank: У мене не було ідеї щось "приховувати", це був запит запитувача. :-P ... Однак, прохання, нарешті, є питанням "Не повторюй себе". У вас може бути безліч методів, які вимагають того ж «кадру» котла, щоб щось чітко придбати / випустити, і ви не хочете нав'язувати це абоненту (подумайте про інкапсуляцію). Також можна назвати такі методи, як .SafeExecute (...) більш чітко, щоб достатньо добре спілкуватися з тим, що вони роблять.
herzmeister

25

Ерік Ганнерсон, який був у команді дизайнерів мови C #, дав цю відповідь майже на те саме питання:

Дуг запитує:

re: Заява про блокування з таймаутом ...

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

Дуг,

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


4
Добре, що цитата повинна сказати, на що він мав на увазі, коли сказав "саме цей сценарій", як я сумніваюся, він мав на увазі це майбутнє питання.
Френк Швітерман

4
@Frank Schwieterman: Я завершив цитату. По- видимому, люди з команди # C дійсно думаю , що usingфункція була НЕ обмежуватися утилізації ресурсів.
paercebal

11

Це слизький схил. У IDisposable є контракт, підкріплений фіналізатором. Фіналізатор у вашому випадку марний. Ви не можете змусити клієнта використовувати оператор, що використовує, лише спонукайте його до цього. Ви можете застосувати його таким способом:

void UseMeUnlocked(Action callback) {
  Unlock();
  try {
    callback();
  }
  finally {
    Lock();
  }
}

Але це, як правило, стає трохи незручним без лямд. Це означає, що я використовував IDisposable, як ви.

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

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

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


Вимушений випадок був наданий "herzmeister der welten" вище, я думаю - навіть якщо ОП не здається, що приклад.
Бенджамін Подшун

Так, я трактував його SafeExecuteяк загальний метод розширення. Зараз я бачу, що це, ймовірно, означалося як Fribbleметод, який викликає Unlock()і Lock()в завершеній спробі / нарешті. Моє ліжко.
Йоганн Герелл

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

7

Справжній приклад світу - BeginForm ASP.net MVC. В основному ви можете написати:

Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();

або

using(Html.BeginForm(...)){
    Html.TextBox(...);
}

Html.EndForm викликає розпорядження, а розпорядження просто виводить </form>тег. Хороша річ у тому, що {} дужки створюють видимий "область", що полегшує зрозуміти, що знаходиться у формі, а що ні.

Я б не зловживав цим, але, по суті, IDisposable - це лише спосіб сказати "ВИ ХОТИ викликати цю функцію, коли закінчите з нею". MvcForm використовує його, щоб переконатися, що форма закрита, Stream використовує її, щоб переконатися, що потік закритий, ви можете використовувати його, щоб переконатися, що об’єкт розблокований.

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

  • Dispose має бути функцією, яку завжди потрібно виконувати, тому не повинно бути ніяких умов, крім Null-Checks
  • Після Dispose () об'єкт не повинен бути повторно використаний. Якби я хотів об'єкт багаторазового використання, я б швидше дав йому відкрити / закрити методи, а не розпоряджатися. Тому я кидаю InvalidOperationException при спробі використовувати розміщений об'єкт.

Врешті-решт, усе стосується очікувань. Якщо об’єкт реалізує IDisposable, я припускаю, що він повинен зробити деяке очищення, тому я його називаю. Я думаю, що зазвичай б'є функція "Shutdown".

Це, як сказано, мені не подобається:

this.Frobble.Fiddle();

Оскільки FrobbleJanitor "володіє" Frobble зараз, мені цікаво, чи не було б краще замість цього подзвонити Фіддл на "Frobble" у "Двірник"?


Щодо вашого "Мені не подобається цей рядок" - я насправді коротко думав зробити це саме так, формулюючи питання, але мені не хотілося захаращувати семантику мого питання. Але я з тобою згоден. Типу. :)
Йоганн Герелл

1
Запропоновано приклад прикладу від самого Microsoft. Зауважте, що у випадку, про який ви згадуєте, слід викинути конкретний виняток: ObjectDisposedException.
Antitoon

4

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


4

Звертаючись до цього: я згоден з більшістю тут, що це крихко, але корисно. Я хотів би вказати на клас System.Transaction.TransactionScope , який робить щось таке, як ви хочете зробити.

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


1
"Двірник" походить зі старої статті C ++ Андрія Олександреску про охоронців, ще до того, як ScopeGuard пробрався до Локі.
Йоганн Герелл

@Benjamin: Мені подобається клас TransactionScope, я не чув про нього раніше. Можливо тому, що я перебуваю на землі .Net CF, куди вона не включена ... :-(
Йоганн Герелл

4

Примітка. Моя точка зору, ймовірно, упереджена від мого C ++, тому значення моєї відповіді слід оцінювати відповідно до можливого зміщення ...

Що говорить специфікація мови C #?

Цитуючи специфікацію мови C # :

8.13 Застосування, що використовує

[...]

Ресурс є класом або структури , яка реалізує System.IDisposable, який включає в себе один метод з ім'ям ВИДАЛЕННЯ без параметрів. Код, який використовує ресурс, може зателефонувати Dispose, щоб вказати, що ресурс більше не потрібен. Якщо утилізацію не викликають, автоматичне вивезення зрештою відбувається як наслідок вивезення сміття.

Код , який використовує ресурс , звичайно, код , починаючи від usingключового слова та йти до обсягу приєднаного до using.

Тож я гадаю, що це нормально, тому що блокування - це ресурс.

Можливо, ключове слово usingбуло обрано погано. Можливо, це повинно було називатися scoped.

Тоді ми можемо розглядати майже будь-що як ресурс. Ручка файлу. Мережеве з'єднання ... Нитка?

Нитка ???

Використовуєте (або зловживаєте) usingключовим словом?

Було б блискучим (ab) використовувати usingключове слово, щоб переконатися, що робота нитки закінчена перед виходом із області застосування?

Герб Саттер, здається, вважає це блискучим , оскільки він пропонує цікаве використання шаблону IDispose, щоб чекати, коли робота нитки закінчиться:

http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095

Ось код, скопійований із статті:

// C# example
using( Active a = new Active() ) {    // creates private thread
       
       a.SomeWork();                  // enqueues work
       
       a.MoreWork();                   // enqueues work
       
} // waits for work to complete and joins with private thread

Хоча код C # для активного об'єкта не надається, він записується, що C # використовує шаблон шаблону IDispose для коду, версія C ++ якого міститься в деструкторі. І дивлячись на версію C ++, ми бачимо деструктор, який чекає, поки внутрішня нитка закінчиться перед виходом, як показано в цьому іншому витязі статті:

~Active() {
    // etc.
    thd->join();
 }

Отже, що стосується Трави, вона блискуча .


3

Я вважаю, що відповідь на ваше запитання - ні, це не було б зловживанням IDisposable.

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

Оскільки ви створюєте нове FrobbleJanitor явно кожного разу, коли ви переходите до usingоператора, ви ніколи не використовуєте один і той же FrobbeJanitorоб'єкт двічі. А оскільки його мета полягає в управлінні іншим об'єктом, Disposeвидається доцільним завдання звільнення цього ("керованого") ресурсу.

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

Єдине, за що я особисто переживаю, це те, що менш зрозуміло, що відбувається, using (var janitor = new FrobbleJanitor())ніж із більш чітким try..finallyблоком, де Lock& & Unlockоперації безпосередньо видно. Але який підхід застосовується, ймовірно, зводиться до питання особистих уподобань.


1

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


1

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

Я б перейменував FrobbleJanitor, хоча, ймовірно, на щось на зразок FrobbleLockSession.


Щодо перейменування - я використовував "Двірник" з основної причини, з якої я мав справу з "Блокування / Розблокування". Я думав, що це римується з іншими варіантами назви. ;-) Якби я використовував цей метод як зразок у всій своїй кодовій базі, я б обов'язково включив щось більш загальне описове, як ви мали на увазі "Сесія". Якби це були старі C ++ дні, я б, ймовірно, використовував FrobbleUnlockScope.
Йоганн Герелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.