Чому немає ICloneable <T>?


222

Чи є певна причина, чому родового ICloneable<T>не існує?

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


6
Немає! з усією повагою до "причин", я згоден з вами, вони повинні були це здійснити!
Шиммі Вайцхандлер

Було б непогано, щоб Microsoft визначила (проблема з власними інтерфейсами brew - це те, що інтерфейси у двох різних складах будуть несумісними, навіть якщо вони семантично однакові). Якби я розробляв інтерфейс, він мав би три члени: Clone, Self та CloneIfMutable, всі вони повертають T (останній член повертав би Clone або Self, як годиться). Член Self дасть можливість прийняти ICloneable (Foo) як параметр, а потім використовувати його як Foo, не потребуючи набору тексту.
supercat

Це дозволило б створити належну ієрархію класів клонування, де спадкові класи піддають захищений метод "клонування" та мають запечатані похідні, що піддають відкритий спосіб. Наприклад, можна мати Pile, CloneablePile: Pile, EnhancedPile: Pile, і CloneableEnhancedPile: EnhancedPile, жоден з яких не буде зламаний, якщо клонується (хоча не всі піддають публічний метод клонування), і NextEnhancedPile: EnhancedPile (який би був порушений якщо клоновано, але не піддається жодному методу клонування). Підпрограма, яка приймає ICloneable (з Pile), може прийняти CloneablePile або CloneableEnhancedPile ...
supercat

... навіть незважаючи на те, що CloneableEnhancedPile не успадковує від CloneablePile. Зауважте, що якщо EnhancedPile успадкований від CloneablePile, даліEnhancedPile повинен був би відкрити публічний метод клонування, і його можна було б передати до коду, який очікував би його клонувати, порушуючи Принцип заміщення Ліскова. Оскільки CloneableEnhancedPile буде реалізовувати ICloneable (Of EnhancedPile) та шляхом імплікації ICloneable (Of Pile), це може бути переведено на процедуру, очікуючи похибну похідну Pile.
supercat

Відповіді:


111

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

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


8
Я не погоджуюся, погано чи погано, це дуже корисно колись для КЛОНУВАННЯ клонування, і в тих випадках справді потрібен Клон <T>, щоб зберегти зайвий бокс та розпакування.
Шиммі Вайцхандлер

2
@AndreyShchekin: Що незрозумілого в глибокому та мілкому клонуванні? Якщо List<T>б був метод клонування, я б очікував, що він List<T>отримає ті елементи, чиї елементи мають ті ж тотожності, що і ті, що були в початковому списку, але я би сподівався, що будь-які внутрішні структури даних будуть дублюватись за необхідності, щоб гарантувати, що нічого, зроблене для одного списку, не вплине в тотожність елементів , що зберігаються в інших. Де двозначність? Більша проблема з клонуванням виникає з варіацією "алмазної проблеми": якщо CloneableFooуспадковується від [не публічно закритого] Foo, має CloneableDerivedFooвипливати з ...
supercat

1
@supercat: Не можу сказати, що я повністю розумію ваше сподівання, як ви вважатимете identity, наприклад, сам список, (у випадку, якщо список списків)? Однак, ігноруючи це, ваші очікування - не єдина можлива ідея, яку люди можуть мати під час дзвінка чи реалізації Clone. Що робити, якщо автори бібліотек, які впроваджують якийсь інший список, не відповідають вашим очікуванням? API повинен бути тривіально однозначним, не може бути однозначним.
Андрій Щекін

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

5
@supercat ви не враховуєте, що це частина .NET Framework, а не частина вашої програми. тож якщо ви створюєте клас, який не робить глибокого клонування, то хтось інший робить клас, який робить глибоке клонування і закликає Cloneвсі його частини, він не працюватиме передбачувано - залежно від того, чи була ця частина реалізована вами чи тією особою хто любить глибоке клонування. Ваша думка щодо шаблонів справедлива, але наявність IMHO в API недостатньо зрозуміла - її або слід викликати, ShallowCopyщоб підкреслити цю точку, або взагалі не надано.
Андрій Щекін

141

На додаток до відповіді Андрея (з якою я згоден, +1) - коли ICloneable це буде зроблено, ви також можете вибрати явну реалізацію, щоб громадськість Clone()повернула введений об’єкт:

public Foo Clone() { /* your code */ }
object ICloneable.Clone() {return Clone();}

Звичайно, є другий випуск із загальним ICloneable<T> - успадкування.

Якщо я маю:

public class Foo {}
public class Bar : Foo {}

І я реалізував ICloneable<T>, то чи реалізую ICloneable<Foo>? ICloneable<Bar>? Ви швидко починаєте впроваджувати безліч однакових інтерфейсів ... Порівняйте з акторською командою ... і чи справді це так погано?


10
+1 Я завжди думав, що проблема коваріації була справжньою причиною
Тамаш Цінеге

У цьому є проблема: явна реалізація інтерфейсу повинна бути приватною , що може спричинити проблеми, якщо Фоо в якийсь момент повинен бути переданий на ICloneable ... Я щось тут пропускаю?
Джоель у Ґо

3
Моя помилка: виявляється, що, незважаючи на те, що він визначений як приватний, метод буде викликаний у випадку, якщо Foo буде передано на IClonable (CLR через C # 2nd Ed, с.319). Наче дивне дизайнерське рішення, але воно є. Так Foo.Clone () дає перший метод, а ((ICloneable) Foo) .Clone () дає другий метод.
Джоель у Ґо

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

8
Запропоноване рішення спочатку здається чудовим ... поки ви не зрозумієте, що в ієрархії класів ви хочете, щоб "public Foo Clone ()" був віртуальним і переосмисленим у похідних класах, щоб вони могли повернути власний тип (і клонувати власні поля звичайно). На жаль, ви не можете змінити тип повернення в заміні (згадана проблема коваріації). Отже, ви знову повертаєтесь до повного кола до початкової проблеми - явна реалізація насправді не купує вас багато (крім повернення базового типу вашої ієрархії замість «об’єкта»), і ви повертаєтесь до кастингу більшості своїх Результати виклику клонують (). Гидота!
Кен Беккет

18

Мені потрібно запитати, що саме ви зробили б з іншим інтерфейсом його реалізації? Інтерфейси, як правило, корисні лише тоді, коли ви передаєте до нього (тобто, чи підтримує цей клас 'IBar'), або у ньому є параметри або сеттери, які приймають його (тобто я приймаю 'IBar'). За допомогою ICloneable - ми пройшли всю Раму і не змогли знайти ніде одного використання ніде, що було б іншим, ніж її реалізація. Нам також не вдалося знайти використання у реальному світі, що також робить щось інше, ніж реалізовувати це (у ~ 60 000 додатках, до яких ми маємо доступ).

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

Девід Кін (команда BCL)


1
Негенерізований ICloneable не дуже корисний, але ICloneable<out T>може бути дуже корисним, якщо він успадковується від ISelf<out T>одного методу Selfтипу T. Людині часто не потрібно «щось, що може бути клоноване», але, можливо, дуже потрібне саме Tте, що може бути клоноване. Якщо об'єкт, що клонується, реалізується ISelf<itsOwnType>, програма, яка потребує Tклонування, може приймати параметр типу ICloneable<T>, навіть якщо не всі похідні, що підлягають клонуванню, Tмають спільного предка.
суперкарт

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

Одне, що іноді незручно в .net - це керувати колекціями, що змінюються, коли вміст колекції повинен розглядатися як значення (тобто я хочу пізніше дізнатися про набір елементів, які зараз є у колекції). Щось подібне ICloneable<T>може бути корисним для цього, хоча більш корисна може бути більш широка рамка для підтримки паралельних змінних та незмінних класів. Іншими словами, код, який повинен бачити, що Fooмістить якийсь тип, але не буде його вимкнути, і не очікує, що він ніколи не зміниться, може використовувати IReadableFoo, в той час як ...
supercat

... код, який хоче вмістити вміст, Fooможе використовувати ImmutableFooкод час, який для маніпулювання ним може використовувати MutableFoo. Код, що надається будь-якому типу, IReadableFooповинен мати можливість змінювати чи змінювати версію. Така рамка була б непоганою, але, на жаль, я не можу знайти жодного приємного способу налаштувати речі на загальний лад. Якби існував послідовний спосіб зробити обгортку лише для читання для класу, таку річ можна було б використовувати в поєднанні з тим, ICloneable<T>щоб зробити незмінну копію класу, який містить T".
supercat

@supercat Якщо ви хочете клонувати a List<T>, таким чином, що клонований List<T>- це нова колекція, що містить покажчики на всі ті самі об’єкти в оригінальній колекції, є два простих способи зробити це без ICloneable<T>. Перший - це Enumerable.ToList()метод розширення: List<foo> clone = original.ToList();другий - це List<T>конструктор, який приймає IEnumerable<T>: List<foo> clone = new List<foo>(original);я підозрюю, що метод розширення, ймовірно, просто викликає конструктор, але обидва вони будуть робити те, що ви вимагаєте. ;)
CptRobby

12

Я думаю, що питання "чому" зайве. Існує багато інтерфейсів / класів / тощо ..., що дуже корисно, але не є частиною базової бібліотеки .NET Frameworku.

Але, в основному, ви можете це зробити самостійно.

public interface ICloneable<T> : ICloneable {
    new T Clone();
}

public abstract class CloneableBase<T> : ICloneable<T> where T : CloneableBase<T> {
    public abstract T Clone();
    object ICloneable.Clone() { return this.Clone(); }
}

public abstract class CloneableExBase<T> : CloneableBase<T> where T : CloneableExBase<T> {
    protected abstract T CreateClone();
    protected abstract void FillClone( T clone );
    public override T Clone() {
        T clone = this.CreateClone();
        if ( object.ReferenceEquals( clone, null ) ) { throw new NullReferenceException( "Clone was not created." ); }
        return clone
    }
}

public abstract class PersonBase<T> : CloneableExBase<T> where T : PersonBase<T> {
    public string Name { get; set; }

    protected override void FillClone( T clone ) {
        clone.Name = this.Name;
    }
}

public sealed class Person : PersonBase<Person> {
    protected override Person CreateClone() { return new Person(); }
}

public abstract class EmployeeBase<T> : PersonBase<T> where T : EmployeeBase<T> {
    public string Department { get; set; }

    protected override void FillClone( T clone ) {
        base.FillClone( clone );
        clone.Department = this.Department;
    }
}

public sealed class Employee : EmployeeBase<Employee> {
    protected override Employee CreateClone() { return new Employee(); }
}

Будь-який працездатний метод клонування для спадкового класу повинен використовувати Object.MemberwiseClone як вихідну точку (або використовувати інше відбиття), оскільки в іншому випадку немає гарантії того, що клон буде того ж типу, що і вихідний об'єкт. У мене досить приємний зразок, якщо вам цікаво.
supercat

@supercat чому б не зробити це відповідь тоді?
nawfal

"Існує багато інтерфейсів / класів / тощо ..., що дуже корисно, але не є частиною базової бібліотеки .NET Frameworku." Чи можете ви навести приклади?
Krythic

1
@Krythic, тобто: розширені структури даних, такі як b-дерево, червоно-чорне дерево, буфер кола. У '09 році не було кортежів, загальних слабких посилань, одночасних колекцій ...
TcKs

10

Написати інтерфейс досить просто, якщо він вам потрібен:

public interface ICloneable<T> : ICloneable
        where T : ICloneable<T>
{
    new T Clone();
}

Я включив фрагмент, оскільки через честь
авторських

2
І як саме повинен працювати цей інтерфейс? Для того, щоб результат клонування можна було видати типу T, об'єкт, який клонується, повинен походити від T. Немає способу встановити таке обмеження типу. Реально кажучи, єдине, у чому я бачу результат повернення iCloneable - це тип iCloneable.
supercat

@supercat: так, саме так повертається Clone (): T, який реалізує ICloneable <T>. MbUnit використовує цей інтерфейс роками , так що так, він працює. Щодо реалізації, погляньте на джерело MbUnit.
Маурісіо Шеффер

2
@Mauricio Scheffer: Я бачу, що відбувається. Жоден тип насправді не реалізує iCloneable (з T). Натомість кожен тип реалізує iCloneable (про себе). Гадаю, це працювало б, хоча все, що це робиться, - це перемістити машинопис. Це дуже погано, що немає способу оголосити поле обмеженням спадкування та інтерфейсом; було б корисно, щоб метод клонування повертав базовий тип об'єкта напівколоніруемого (клонування, викритого лише як Захищений метод), але з обмеженням типу, що клонується. Це дозволило б об'єкту прийняти клоновані похідні напівклонних похідних базового типу.
supercat

@Mauricio Scheffer: BTW, я б запропонував, що ICloneable (з T) також підтримує властивість Self Readonly (типу повернення T). Це дозволило б методу прийняти ICloneable (Foo) і використовувати його (через властивість Self) як Foo.
supercat

9

Нещодавно прочитавши статтю Чому копіювати об’єкт - це страшно? , Я думаю, що це питання потребує додаткового уточнення. Інші відповіді тут дають хороші поради, але все ж відповідь не є повною - чому ні ICloneable<T>?

  1. Використання

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

    Тоді ви могли отримати метод, який перевіряє, чи є об'єкт is ICloneable. Що тепер? Ви не можете зробити, is ICloneable<>і як ви не знаєте тип об'єкта при компіляційному типі, ви не можете зробити метод загальним. Перша реальна проблема.

    Отже, вам потрібно мати ICloneable<T>і те ICloneable, і перше, що реалізує останнє. Таким чином, реалізатору потрібно буде реалізувати обидва методи - object Clone()і T Clone(). Ні, дякую, нам уже досить весело IEnumerable.

    Як уже вказувалося, існує також складність успадкування. Хоча коваріація може здатися вирішити цю проблему, похідному типу потрібно реалізувати ICloneable<T>власний тип, але вже існує метод з тим же підписом (= параметри, в основному) - Clone()базового класу. Зробити явний новий інтерфейс методу клонування безглуздим, ви втратите перевагу, яку ви шукали при створенні ICloneable<T>. Тому додайте newключове слово. Але не забувайте, що вам також знадобиться переосмислити базовий клас ' Clone()(реалізація повинна залишатися рівномірною для всіх похідних класів, тобто повертати один і той же об'єкт від кожного методу клонування, тому метод базового клонування повинен бути virtual)! Але, на жаль, ви не можете обоєoverride іnewметоди з однаковим підписом. Вибираючи перше ключове слово, ви втратите мету, яку хотіли мати при додаванні ICloneable<T>. Вибираючи другий, ви зламаєте сам інтерфейс, роблячи методи, які повинні робити те саме, що повертають різні об'єкти.

  2. Точка

    Ви хочете ICloneable<T>для комфорту, але комфорт - це не те, для чого призначені інтерфейси, їх значення - це (загалом OOP) для уніфікації поведінки об'єктів (хоча в C # вона обмежується уніфікацією зовнішньої поведінки, наприклад, методів та властивостей, а не їхня робота).

    Якщо перша причина вас ще не переконала, ви можете заперечити, що ICloneable<T>також може працювати обмежувально, щоб обмежити тип, повернений із методу клонування. Однак неприємний програміст може реалізувати ICloneable<T>там, де T - не той тип, який його реалізує. Отже, щоб досягти обмеження, ви можете додати приємне обмеження до загального параметра:
    public interface ICloneable<T> : ICloneable where T : ICloneable<T>
    Безумовно, більш обмежуючим, ніж той, без якого where, ви все одно не можете обмежувати, що T - тип, що реалізує інтерфейс (ви можете отримати ICloneable<T>інший тип що реалізує це).

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

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

Але повернемось до питання, чого ви дійсно прагнете - це мати комфорт під час клонування предмета. Є два способи зробити це:

Додаткові методи

public class Base : ICloneable
{
    public Base Clone()
    {
        return this.CloneImpl() as Base;
    }

    object ICloneable.Clone()
    {
        return this.CloneImpl();
    }

    protected virtual object CloneImpl()
    {
        return new Base();
    }
}

public class Derived : Base
{
    public new Derived Clone()
    {
        return this.CloneImpl() as Derived;
    }

    protected override object CloneImpl()
    {
        return new Derived();
    }
}

Це рішення забезпечує як комфорт, так і цілеспрямовану поведінку користувачів, але це також занадто довго для реалізації. Якщо ми не хотіли, щоб «комфортний» метод повертав поточний тип, це набагато простіше просто public virtual object Clone().

Тож давайте подивимось на "остаточне" рішення - що C # насправді має намір забезпечити нам комфорт?

Методи розширення!

public class Base : ICloneable
{
    public virtual object Clone()
    {
        return new Base();
    }
}

public class Derived : Base
{
    public override object Clone()
    {
        return new Derived();
    }
}

public static T Copy<T>(this T obj) where T : class, ICloneable
{
    return obj.Clone() as T;
}

Він називається Copy, щоб не стикатися з поточними методами Clone (компілятор надає перевагу власним заявленим методам типу над розширеннями). classОбмеження існує для швидкості (не вимагає нульовий перевірки і т.д.).

Я сподіваюся, що це пояснює причину, чому її не зробити ICloneable<T>. Однак рекомендується взагалі не виконувати ICloneable.


Додаток №1: Єдине можливе використання узагальненого ICloneableдля типів значень, де воно може обійти бокс методом Clone, і це означає, що у вас є значення без коробки. Оскільки структури можуть бути клоновані (неглибоко) автоматично, немає необхідності в їх реалізації (якщо тільки ви не зробите це конкретним, що означає глибоку копію).
IllidanS4 хоче, щоб Моніка повернулася

2

Хоча запитання дуже давнє (5 років з моменту написання цієї відповіді :) і на нього вже відповіли, але я вважаю, що ця стаття відповідає на питання досить добре, перевірте її тут

Редагувати:

Ось цитата зі статті, яка відповідає на запитання (обов’язково прочитайте повну статтю, вона включає інші цікаві речі):

В Інтернеті є багато посилань, які вказують на повідомлення про блог Бреда Абрамса в 2003 році, який працював в Microsoft, і в якому обговорюються деякі думки про ICloneable. Запис у блозі можна знайти за цією адресою: Реалізація ICloneable . Незважаючи на оманливий заголовок, цей запис у блозі закликає не застосовувати ICloneable, головним чином через дрібну / глибоку плутанину. Стаття закінчується прямою пропозицією: Якщо вам потрібен механізм клонування, визначте власний метод Clone або Copy і переконайтесь, що ви чітко документуєте, чи це копія глибокої чи дрібної. Відповідний зразок:public <type> Copy();


1

Велика проблема полягає в тому, що вони не могли обмежити T тим самим класом. Для прикладу, що б заважало вам це робити:

interface IClonable<T>
{
    T Clone();
}

class Dog : IClonable<JackRabbit>
{
    //not what you would expect, but possible
    JackRabbit Clone()
    {
        return new JackRabbit();
    }

}

Їм потрібно обмеження параметрів, наприклад:

interfact IClonable<T> where T : implementing_type

8
Це не здається так погано, як class A : ICloneable { public object Clone() { return 1; } /* I can return whatever I want */ }
Микола Новак

Я насправді не бачу, в чому проблема. Жоден інтерфейс не може змусити реалізацію розумно поводитися. Навіть якщо це ICloneable<T>може обмежитися Tвідповідності своєму типу, це не змусить реалізацію Clone()повернути щось віддалено, що нагадує об'єкт, на який його клонували. Далі я б припустив, що якщо використовується коваріація інтерфейсу, можливо, найкраще мати класи, які реалізують, ICloneableбути запечатаними, щоб інтерфейс ICloneable<out T>включав Selfвластивість, яка, як очікується, повернеться, і ...
supercat

... у споживачів відлитих або стримуючих до ICloneable<BaseType>або ICloneable<ICloneable<BaseType>>. BaseTypeУ питанні повинен мати protectedметод клонування, який буде називатися по типу , який реалізує ICloneable. Цей дизайн дозволив би передбачити можливість того, що ви можете мати a Container, a CloneableContainer, a FancyContainerі a CloneableFancyContainer, останній може бути використаний в коді, який вимагає похідної, що може бути клонований, Containerабо який вимагає FancyContainer(але не важливо, чи він може бути клонований).
supercat

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

+1 - ця відповідь єдина з тих, кого я тут бачив, і справді відповідає на питання.
IllidanS4 хоче, щоб Моніка повернулася

0

Це дуже гарне запитання ... Ти можеш зробити свій власний, хоча:

interface ICloneable<T> : ICloneable
{
  new T Clone ( );
}

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

EDIT: Шаблон, який я використовую для "клонування" об'єкта, передає прототип в конструктор.

class C
{
  public C ( C prototype )
  {
    ...
  }
}

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


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

Денніс: Дивіться відповіді Марка. Проблеми коваріації / успадкування роблять цей інтерфейс в значній мірі непридатним для будь-якого сценарію, який принаймні незначно складний.
Tamas Czinege

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