Студопедия

КАТЕГОРИИ:


Архитектура-(3434)Астрономия-(809)Биология-(7483)Биотехнологии-(1457)Военное дело-(14632)Высокие технологии-(1363)География-(913)Геология-(1438)Государство-(451)Демография-(1065)Дом-(47672)Журналистика и СМИ-(912)Изобретательство-(14524)Иностранные языки-(4268)Информатика-(17799)Искусство-(1338)История-(13644)Компьютеры-(11121)Косметика-(55)Кулинария-(373)Культура-(8427)Лингвистика-(374)Литература-(1642)Маркетинг-(23702)Математика-(16968)Машиностроение-(1700)Медицина-(12668)Менеджмент-(24684)Механика-(15423)Науковедение-(506)Образование-(11852)Охрана труда-(3308)Педагогика-(5571)Полиграфия-(1312)Политика-(7869)Право-(5454)Приборостроение-(1369)Программирование-(2801)Производство-(97182)Промышленность-(8706)Психология-(18388)Религия-(3217)Связь-(10668)Сельское хозяйство-(299)Социология-(6455)Спорт-(42831)Строительство-(4793)Торговля-(5050)Транспорт-(2929)Туризм-(1568)Физика-(3942)Философия-(17015)Финансы-(26596)Химия-(22929)Экология-(12095)Экономика-(9961)Электроника-(8441)Электротехника-(4623)Энергетика-(12629)Юриспруденция-(1492)Ядерная техника-(1748)

Деструктор




 

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

Синтаксис деструктору:

 

[атрибути] [extern] ~ ім'я_класу()

тіло

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

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

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

 

7.8. Вкладені типи

 

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

Наприклад, введемо в наш клас Monster допоміжний клас Gun. Об'єкти цього класу без “господаря” даремні, тому його можна визначити як внутрішній:

 

using System;

namespace ConsoleApplication1

{

class Monster

{

class Gun

{

}

}

}

 

7.9. Рекомендації по програмуванню

 

Як правило, клас як тип, визначений користувачем, повинен містити приховані (private) поля і наступні функціональні елементи:

§ конструктори, що визначають, як ініціалізувати об'єкти класу;

§ набір методів і властивостей, що реалізовують характеристики класу;

§ класи виключень, використовувані для повідомлень про помилки шляхом генерації виняткових ситуацій.

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

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

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

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

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

 


РОЗДІЛ 8. ІЄРАРХІЇ КЛАСІВ

 

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

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

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

Отже, спадкоємство застосовується для наступних взаємозв'язаних цілей:

– виключення з програми фрагментів коду, що повторюються;

– спрощення модифікації програми;

– спрощення створення нових програм на основі тих, що існують.

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

 

8.1. Спадкоємство

 

Клас в С# може мати довільну кількість нащадків і лише одного предка. При описі класу ім'я його предка записується в заголовку класу після двокрапки. Якщо ім'я предка не вказане, за предка вважається базовий клас всієї ієрархії System.Object:

 

[атрибути] [специфікатори] class ім'я_класу [: предки]

тіло_класу

 

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

 

Розглянемо спадкоємство класів на прикладі. У 5 розділі був описаний клас Monster, моделюючий персонаж комп'ютерної гри. Допустимо, нам потрібно ввести в гру ще один тип персонажів, який повинен володіти властивостями об'єкту Monster. Буде логічне зробити новий об'єкт нащадком об'єкту Monster (лістинг 8.1).

 

Лістинг 8.1. Клас Daemon, предок класу Monster

using System;

namespace examp48

{

class Monster

{

public Monster()

{

this.name = "Noname";

this.health = 100;

this.ammo = 100;

}

 

public Monster(string name): this()

{

this.name = name;

}

public Monster(int health, int ammo, string name)

{

this.name = name;

this.health = health;

this.ammo = ammo;

}

public string GetName()

{

return name;

}

public int GetHealth()

{

return health;

}

public int GetAmmo()

{

return ammo;

}

public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}", name, health, ammo);

 

}

 

protected string name; // закриті поля

public int health, ammo;

}

 

class Daemon: Monster

{

public Daemon()

{

brain = 1;

}

 

public Daemon(string name, int brain)

: base(name) // 1

{

this.brain = brain;

}

 

public Daemon(int health, int ammo, string name, int brain)

: base(health, ammo, name) // 2

{

this.brain = brain;

}

 

new public void Passport() // 3

{

Console.WriteLine(

"Daemon {0} \t health = {1} ammo = {2} brain = {3}",

name, health, ammo, brain);

 

}

 

public void Think() // 4

{

Console.Write(name + " is ");

for (int i = 0; i < brain; ++i) Console.Write(" thinking");

Console.WriteLine(" ... ");

}

int brain; // закрите поле

}

 

class Classl

{

static void Main()

{

Daemon Dima = new Daemon("Dima", 3); // 5

Dima. Passport(); // 6

Dima.Think(); // 7

Dima.GetHealth(); // 8

Dima.Passport();

}

}

}

У класі Daemon введені закрите поле brain і метод Think, визначені власні конструктори, а також перевизначений метод Passport. Всі поля і властивості класу Monster успадковуються в класі Daemon.

Результат роботи програми:

 

Daemon Dima health = 100 ammo = 100 brain = 3

Dima is thinking thinking thinking...

Daemon Dima health = 90 ammo = 100 brain = 3

 

Екземпляр класу Daemon використовує як власні (оператори 5-7), так і успадковані (оператор 8) елементи класу. Розглянемо загальні правила спадкоємства, використовуючи як приклад лістинг 8.1.

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

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

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

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

Поля, методи і властивості класу успадковуються, тому за бажання замінити елемент базового класу новим елементом слід явним чином вказати компілятору свій намір за допомогою ключового слова new. У лістингу 8.1 таким чином перевизначений метод виведення інформації про об'єкт Passport.

Метод Passport класу Daemon заміщає відповідний метод базового класу, проте можливість доступу до методу базового класу з методу похідного класу зберігається. Для цього перед викликом методу указується слово base, наприклад:

 

base.Passport();

 

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

Ось, наприклад, як виглядав би метод Passport, якби ми в класі Daemon хотіли не повністю перевизначити поведінку його предка, а доповнити його:

 

new public void Passport()

{

base.Passport();

Console.WriteLine(" brain = {1}", brain);

}

Елементи базового класу, визначені як private, в похідному класі недоступні. Тому в методі Passport для доступу до полів name, health і ammo довелося використовувати відповідні властивості базового класу. Інше рішення полягає в тому, щоб визначити ці поля зі специфікатором protected, в цьому випадку вони будуть доступні методам всіх класів, похідних від Monster. Обидва рішення мають свої переваги і недоліки.

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

§ об'єкт, в який під час виконання програми заносяться посилання на об'єкти різних класів ієрархії;

§ контейнер, в якому зберігаються об'єкти різних класів, які відносяться до однієї ієрархії;

§ метод, в який можуть передаватися об'єкти різних класів ієрархії;

§ метод, з якого залежно від типу об'єкту, який викликав його, викликаються відповідні методи.

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

Давайте спробуємо описати масив об'єктів базового класу і занести туди об'єкти похідного класу. У лістингу 8.2 в масиві типу Monster зберігаються два об'єкти типу Monster і один - типу Daemon

 

Лістинг 8.2. Масив об’єктів різних типів

using System;

namespace examp48

{

class Monster

{

// Див. лістинг 8.1

}

 

class Daemon: Monster

{

// Див. лістинг 8.1

}

 

class Classl

{

static void Main()

{

const int n = 3;

Monster[] stado = new Monster[n];

stado[0] = new Monster("Monia");

stado[1] = new Monster("Monk");

stado[2] = new Daemon ("Dimon", 3);

foreach (Monster elem in stado) elem.Passport(); //1

for (int i = 0; i < n; ++i) stado[i].ammo = 0; //2

Console.WriteLine();

foreach (Monster elem in stado) elem.Passport(); //3

}

}

}

 

Результат роботи програми:

Monster Monia health = 100 ammo = 100

Monster Monk health = 100 ammo = 100

Monster Dimon health = 100 ammo = 100

 

Monster Monia health = 100 ammo = 0

Monster Monk health = 100 ammo = 0

Monster Dimon health = 100 ammo = 0

 

Об'єкт типу Daemon дійсно можна помістити в масив, що складається з елементів типу Monster, але для нього викликаються тільки методи і властивості, успадковані від предка. Це влаштовує нас в операторові 2, а в операторах 1 і 3 хотілося б, щоб викликався метод Passport, перевизначений в нащадку.

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

Це і зрозуміло: адже компілятор повинен ще до виконання програми вирішити, який метод викликати, і вставити в код фрагмент, передавальний управління на цей метод (цей процес називається раннім зв’язуванням). При цьому компілятор може керуватися тільки типом змінної, для якої викликається метод або властивість (наприклад, stado[i].Ammo). Те, що в цій змінній в різні моменти часу можуть знаходитися посилання на об'єкти різних типів, компілятор врахувати не може.

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

 

8.2. Віртуальні методи

 

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

 

virtual public void Passport()

 

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

Для його реалізації необхідно, щоб адреси віртуальних методів зберігалися там, де ними можна буде у будь-який момент скористатися. Компілятор формує для цих методів таблицю віртуальних методів (Virtual Method Table, VMT). У цю таблицю записуються адреси віртуальних методів (зокрема успадкованих) в порядку опису в класі. Для кожного класу створюється одна таблиця.

Кожен об'єкт під час виконання повинен мати доступ до VMT. Забезпечення цього зв'язку не можна доручити компілятору, оскільки він повинен встановлюватися під час виконання програми при створенні об'єкту. Тому зв'язок екземпляра об'єкту з VMT встановлюється за допомогою спеціального коду, що автоматично поміщається компілятором в конструктор об'єкту.

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

 

override public void Passport()

 

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

 

Додамо в лістинг 8.2 два чарівні слова - virtual і override - в описи методів Passport, відповідно, базового і похідного класів (лістинг 8.3).

 

Лістинг 8.3. Віртуальні методи

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

namespace examp49

{

class Monster

{

...

virtual public void Passport()

{

Console.WriteLine(

"Monster {0} \t health = {1} ammo = {2}",

name, health, ammo);

}

 

protected string name; // закриті поля

public int health, ammo;

}

 

class Daemon: Monster

{

override public void Passport()

{

Console.WriteLine(

"Daemon {0} \t health = {1} ammo = {2} brain = {3}", name, health, ammo, brain);

}

class Classl

{

static void Main()

{

const int n = 3;

Monster[] stado = new Monster[n];

stado[0] = new Monster("Monia");

stado[1] = new Monster("Monk");

stado[2] = new Daemon("Dimon", 3);

foreach (Monster elem in stado) elem.Passport();

for (int i = 0; i < n; ++i) stado[i].ammo = 0;

Console.WriteLine();

foreach (Monster elem in stado) elem.Passport();

}

}

}

 

Результат роботи програми:

Monster Monia health = 100 ammo = 100

Monster Monk health = 100 ammo = 100

Daemon Dimon health = 100 ammo = 100 brain = 3

 

Monster Monia health = 100 ammo = 0

Monster Monk health = 100 ammo = 0

Daemon Dimon health = 100 ammo = 0 brain = 3

 

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

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

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

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

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

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

 

8.3. Абстрактні класи

 

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

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

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

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

 

Лістинг 8.4. Абстрактний клас

using System;

 

namespace examp50

{

abstract class Spirit

{

public abstract void Passport();

}

 

class Monster: Spirit

{

public Monster()

{

this.name = "Noname";

this.health = 100;

this.ammo = 100;

}

 

public Monster(string name)

: this()

{

this.name = name;

}

public Monster(int health, int ammo, string name)

{

this.name = name;

this.health = health;

this.ammo = ammo;

}

public string GetName()

{

return name;

}

 

public int GetHealth()

{

return health;

}

 

public int GetAmmo()

{

return ammo;

}

override public void Passport()

{

Console.WriteLine("Monster {0} \t health = {1} ammo = {2}", name, health, ammo);

}

 

public string name; // закриті поля

public int health, ammo;

}

 

class Daemon: Monster

{

public Daemon()

{

brain = 1;

}

 

public Daemon(string name, int brain)

: base(name) // 1

{

this.brain = brain;

}

 

public Daemon(int health, int ammo, string name, int brain)

: base(health, ammo, name) // 2

{

this.brain = brain;

}

 

 

override public void Passport() // 3

{

Console.WriteLine(

"Daemon {0} \t health = {1} ammo = {2} brain = {3}",

name, health, ammo, brain);

}

 

int brain; // закрите поле

}

 

class Classl

{

static void Main()

{

Daemon Dim1 = new Daemon("Dima", 3);

Dim1.Passport();

 

Monster Dim2 = new Daemon("Dima", 8);

Dim2.Passport();

 

Monster Dim3 = new Monster();

Dim3.Passport();

 

}

}

}

 

Результат роботи програми:

Daemon Dima health = 100 ammo = 100 brain = 3

Daemon Dima health = 100 ammo = 100 brain = 8

Monster Noname health = 100 ammo = 100

 

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

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

 

8.4. Безплідні класи

 

У С# є ключове слово sealed, що дозволяє описати клас, від якого, в протилежність абстрактному, успадковувати забороняється:

 

sealed class Spirit

{

.

}

// class Monster: Spirit { ... }помилка!

 

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

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

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

 

Лістинг 8.4. Модель включення

using System;

 

namespace examp51

{

class Двигатель

{

public void Запуск()

{

Console.WriteLine("вжжжж!!");

}

}

 

class Самолет

{

public Самолет()

{

левый = new Двигатель();

правый = new Двигатель();

}

public void Запустить_двигатели()

{

левый.Запуск();

правый.Запуск();

}

Двигатель левый, правый;

}

 

class Class1

{

static void Main()

{

Самолет AH24_1 = new Самолет();

AH24_1.Запустить_двигатели();

}

}

}

 

Результат роботи програми:

вжжжж!!

вжжжж!!

 

У методі “Запустити двигуни” запит на запуск двигунів передається, або, як прийнято говорити, делегується вкладеному класу.

 




Поделиться с друзьями:


Дата добавления: 2014-12-27; Просмотров: 602; Нарушение авторских прав?; Мы поможем в написании вашей работы!


Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет



studopedia.su - Студопедия (2013 - 2024) год. Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав! Последнее добавление




Генерация страницы за: 0.011 сек.