Студопедия

КАТЕГОРИИ:


Архитектура-(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)

Тип void. 2 страница




Несмотря на то, что обстоятельный разговор о наследовании, родителях и потомках еще предстоит, стоит с самого начала понимать отношения между родительским классом и классом потомком, отношения между объектами этих классов. Класс потомок при создании наследует все свойства и методы родителя. Родительский класс не имеет возможности наследовать свойства и методы, создаваемые его потомками. Наследование – это односторонняя операция от родителя к потомку. Ситуация с присваиванием симметричная. Объекту родительского класса присваивается объект класса потомка. Объекту класса потомка не может быть присвоен объект родительского класса. Присваивание – это односторонняя операция от потомка к родителю. Одностороннее присваивание реально означает, что ссылочная переменная родительского класса может быть связана с любыми объектами, имеющими тип потомков родительского класса.

Например, пусть задан некоторый класс Parent, а класс Child – его потомок, объявленный следующим образом:

class Child:Parent {…}

Пусть теперь в некотором классе, являющемся клиентом классов Parent и Child объявлены переменные этих классов и созданы связанные с ними объекты:

Parent p1 = new Parent(), p2 = new Parent();

Child ch1 = new Child(), ch2 = new Child();

Тогда допустимы присваивания:

p1 = p2; p2= p1; ch1=ch2; ch2 = ch1; p1 = ch1; p1 = ch2;

Но недопустимы присваивания:

ch1 = p1; ch2 = p1; ch2 = p2;

Заметьте, ситуация не столь удручающая – сын может вернуть себе переданный родителю объект, задав явное преобразование. Так что следующие присваивания допустимы:

p1 = ch1; … ch1 = (Child)p1;

Семантика присваивания справедлива и для другого важного случая – при рассмотрении соответствия между формальными и фактическими аргументами процедур и функций. Если формальный аргумент согласно объявлению имеет тип T, а выражение, задающее фактический аргумент, имеет тип T1, то имеет место согласование типов формального и фактического аргумента, если и только если класс T1 является потомком класса T. Отсюда незамедлительно следует, что если формальный параметр процедуры принадлежит классу object, то фактический аргумент может быть выражением любого типа.

Преобразование к типу object

Рассмотрим частный случай присваивания x = e; когда x имеет тип object. В этом случае гарантируется полная согласованность по присваиванию – выражение e может иметь любой тип. В результате присваивания значением переменной x становится ссылка на объект, заданный выражением e. Заметьте, текущим типом x становится тип объекта, заданного выражением e. Уже здесь проявляется одно из важных различий между классом и типом. Переменная, лучше сказать сущность x, согласно объявлению принадлежит классу object, но ее тип – тип того объекта, с которым она связана в текущий момент, может динамически изменяться.

Примеры преобразований

Перейдем к примерам. Класс Testing, содержащий примеры, представляет собой набор данных разного типа, над которыми выполняются операции, иллюстрирующие преобразования типов. Вот описание класса Testing:

using System;

namespace Types

{

/// <summary>

/// Класс Testing включает данные разных типов.

/// Каждый его открытый метод описывает некоторый пример,

/// демонстрирующий работу с типами.

/// Открытые методы могут вызывать закрытые методы класса.

/// </summary>

public class Testing

{

/// <summary>

/// набор скалярных данных разного типа.

/// </summary>

byte b = 255;

int x = 11;

uint ux = 1111;

float y = 5.5f;

double dy = 5.55;

string s = "Hello!";

string s1 = "25";

object obj = new Object();

//Далее идут методы класса, приводимые по ходу описания примеров

}

В набор данных класса входят скалярные данные арифметического типа, относящиеся к значимым типам, переменные строкового типа и типа object, принадлежащие ссылочным типам. Рассмотрим закрытый (private) метод этого класса – процедуру WhoIsWho с формальным аргументом класса object. Процедура выводит на консоль переданное ей имя аргумента, его тип и значение. Вот ее текст:

/// <summary>

/// Метод выводит на консоль информацию о типе и значении

/// фактического аргумента. Формальный аргумент имеет тип object,

/// Фактический аргумент может иметь любой тип,

/// поскольку всегда допустимо неявное преобразование в тип object.

/// </summary>

/// <param name="name"> - Имя второго аргумента </param>

/// <param name="any"> - Допустим аргумент любого типа </param>

void WhoIsWho(string name, object any)

{

Console.WriteLine("type {0} is {1}, value is {2}",

name, any.GetType(), any.ToString());

}

Вот открытый (public)метод класса Testing, в котором многократно вызывается метод WhoIsWho с аргументами разного типа:

/// <summary>

/// получаем информацию о типе и значении

/// переданного аргумента - переменной или выражения

/// </summary>

public void WhoTest()

{

WhoIsWho("x",x);

WhoIsWho("ux",ux);

WhoIsWho("y",y);

WhoIsWho("dy",dy);

WhoIsWho("s",s);

WhoIsWho("11 + 5.55 + 5.5f",11 + 5.55 + 5.5f);

obj = 11 + 5.55 + 5.5f;

WhoIsWho("obj",obj);

}

Заметьте, сущность any – формальный аргумент – класса object при каждом вызове динамически изменяет тип, связываясь с объектом, заданным фактическим аргументом. Поэтому тип аргумента, выдаваемый на консоль, – это тип фактического аргумента. Заметьте также, что наследуемый от класса object метод GetType возвращает тип FCL, то есть тот тип, на который отражается тип языка и с которым реально идет работа при выполнении модуля. В большинстве вызовов фактическим аргументом является переменная – соответствующее свойство класса Testing, – но в одном случае передается обычное арифметическое выражение, автоматически преобразуемое в объект. Аналогичная ситуация имеет место и при выполнении присваивания в рассматриваемой процедуре.

На рис. 3.1 показаны результаты вывода на консоль, полученные при вызове метода WhoTest в приведенной выше процедуре Main класса Class1.

Рис. 3.1. Вывод на печать результатов теста WhoTest

Семантика присваивания. Преобразования между ссылочными и значимыми типами

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

Цель и источник значимого типа. Тогда имеет место семантика значимого присваивания. В этом случае источник и цель имеют собственную память для хранения значений. Значения источника заменяют значения соответствующих полей цели. Источник и цель после этого продолжают жить независимо. У них своя память, хранящая после присваивания одинаковые значения.

Цель и источник ссылочного типа. Тогда имеет место семантика ссылочного присваивания. В этом случае значениями источника и цели являются ссылки на объекты, хранящиеся в памяти (куче). При ссылочном присваивании цель разрывает связь с тем объектом, на который она ссылалась до присваивания, и становится ссылкой на объект, связанный с источником. Результат ссылочного присваивания двоякий. Объект, на который ссылалась цель, теряет одну из своих ссылок и может стать висячим, так что его дальнейшую судьбу определит сборщик мусора. С объектом в памяти, на который ссылался источник, теперь связываются по меньшей мере две ссылки, рассматриваемые как различные имена одного объекта. Ссылочное присваивание приводит к созданию псевдонимов – к появлению разных имен у одного объекта. Особо следует учитывать ситуацию, когда цель и/или источник имеет значение void. Если это значение имеет источник, то в результате присваивания цель получает это значение и более не ссылается ни на какой объект. Если же цель имела значение void, а источник нет, то в результате присваивания ранее «висячая» цель становится ссылкой на объект, связанный с источником.

Цель ссылочного типа, источник значимого типа. В этом случае «на лету» значимый тип преобразуется в ссылочный. Как обеспечивается двойственность существования значимого и ссылочного типа – переменной и объекта? Ответ прост, – за счет специальных, эффективно реализованных операций, преобразующих переменную значимого типа в объект и обратно. Операция «упаковать» (boxing) выполняется автоматически и неявно в тот момент, когда по контексту требуется объект, а не переменная. Например, при вызове процедуры WhoIsWho требуется, чтобы аргумент any был объектом. Если фактический аргумент является переменной значимого типа, то автоматически выполняется операция «упаковать». При ее выполнении создается настоящий объект, хранящий значение переменной. Можно считать, что происходит упаковка переменной в объект. Необходимость в упаковке возникает достаточно часто. Примером может служить и процедура консольного вывода WriteLine класса Console, которой требуются объекты, а передаются зачастую переменные значимого типа.

Цель значимого типа, источник ссылочного типа. В этом случае «на лету» ссылочный тип преобразуется в значимый. Операция «распаковать» (unboxing) выполняет обратную операцию, – она «сдирает» объектную упаковку и извлекает хранимое значение. Заметьте, операция «распаковать» не является обратной к операции «упаковать» в строгом смысле этого слова. Оператор obj = x корректен, но выполняемый следом оператор x = obj приведет к ошибке. Недостаточно, чтобы хранимое значение в упакованном объекте точно совпадало по типу с переменной, которой присваивается объект. Необходимо явно заданное преобразование к нужному типу.

Операции упаковать и распаковать (boxing и unboxing). Примеры

В нашем следующем примере демонстрируется применение обеих операций – упаковки и распаковки. Поскольку формальный аргумент процедуры Back принадлежит классу object, то при передаче фактического аргумента значимого типа происходит упаковка значения в объект. Этот объект и возвращается процедурой. Его динамический тип определяется тем объектом памяти, на который указывает ссылка. Когда возвращаемый результат присваивается переменной значимого типа, то, несмотря на совпадение типа переменной с динамическим типом объекта, необходимо выполнить распаковку, содрать объектную упаковку и вернуть непосредственное значение. Вот как выглядит процедура Back и тестирующая ее процедура BackTest из класса Testing:

/// <summary>

/// Возвращает переданный ему аргумент.

/// Фактический аргумент может иметь произвольный тип.

/// Возвращается всегда объект класса object.

/// Клиент, вызывающий метод, должен при необходимости задать

/// явное преобразование получаемого результата

/// </summary>

/// <param name="any"> Допустим любой аргумент </param>

/// <returns></returns>

object Back(object any)

{

return (any);

}

/// <summary>

/// Неявное преобразование аргумента в тип object

/// Явное приведение типа результата.

/// </summary>

public void BackTest()

{

ux = (uint)Back(ux);

WhoIsWho("ux",ux);

s1 = (string)Back(s);

WhoIsWho("s1",s1);

x =(int)(uint)Back(ux);

WhoIsWho("x",x);

y = (float)(double)Back(11 + 5.55 + 5.5f);

WhoIsWho("y",y);

}

Обратите внимание, если значимый тип в левой части оператора присваивания не совпадает с динамическим типом объекта, то могут потребоваться две операции приведения. Вначале нужно распаковать значение, а затем привести его к нужному типу, что и происходит в двух последних операторах присваивания. Приведу результаты вывода на консоль, полученные при вызове процедуры BackTest в процедуре Main:

Рис. 3.2. Вывод на печать результатов теста BackTest

Две двойственные операции «упаковать» и «распаковать» позволяют в зависимости от контекста рассматривать значимые типы как ссылочные, переменные как объекты, и наоборот.

 

 

Вариант 1

1. К значимым типам языка C# относятся:

q все арифметические типы;

q массивы;

q строки;

q все арифметические типы, кроме типа double;

q тип char.

2. Отметьте истинные высказывания:

q существует неявное преобразование переменных арифметического типа в объекты;

q существует неявное преобразование объектов в переменные арифметического типа;

q существует явное преобразование объектов в переменные арифметического типа;

q в зависимости от контекста переменная арифметического типа представляет собой объект.

3. Цель и источник согласованы по присваиванию, если:

q базовый тип цели и тип источника совпадают;

q тип источника является потомком базового типа цели;

q базовый тип цели является потомком типа источника;

q базовый тип цели – object.

Вариант 2

7. К ссылочным типам языка C# относятся:

q тип double;

q массивы;

q строки;

q все арифметические типы, кроме типа double;

q структуры.

8. Отметьте истинные высказывания:

q тип double является классом, родителем которого является класс object;

q все типы являются наследниками класса object;

q значимые типы не входят в иерархию классов языка C#;

q тип string наследует методы родительского класса object и определяет собственные методы;

q тип int наследует методы родительского класса object и определяет собственные методы.

9. Если формальный аргумент метода объявлен класса T, то фактический аргумент может быть:

q только типа T;

q любого типа;

q типа object;

q типа, являющегося родителем типа T;

q типа, являющегося потомком типа T.

Вариант 3

7. К типам, определенным пользователем языка C#, относятся:

q классы;

q массивы;

q строки;

q перечисления;

q структуры.

8. Отметьте истинные высказывания:

q метод ToString () может быть вызван переменной X арифметического типа – X.ToString ();

q метод ToString () может быть вызван объектом X класса Т, созданного пользователем – X.ToString ();

q объекту базового класса Object можно присвоить объект класса Т, созданного пользователем;

q объекту класса Т, созданного пользователем, можно присвоить объект базового класса Object.

9. Если присваивание переменных x= y; допустимо, то обратное присваивание:

q всегда допустимо;

q всегда допустимо для переменных ссылочных типов;

q всегда допустимо для переменных значимых типов;

q может требовать явного преобразования типов;

q всегда требует явного преобразования типов.

 

 

Лекция 4. Преобразования типов

Преобразования типов. Преобразования внутри арифметического типа. Преобразования строкового типа. Класс Convert и его методы. Проверяемые преобразования. Управление проверкой арифметических преобразований.

Ключевые понятия: неявное преобразование; явное преобразование; проверяемый блок; непроверяемый блок; проверяемое выражение; непроверяемое выражение; отношение наследования; родительский класс; базовый класс; object; потомки; метод; свойства; тэги summary; XML-отчет; согласование по присваиванию; согласование типов; цель; источник; класс Convert; библиотека FCL; исключения; охраняемые блоки; класс ExceptionПреобразования значимых типов

Где, как и когда выполняются преобразования типов?

Необходимость в преобразовании типов возникает в выражениях, присваиваниях, замене формальных аргументов метода фактическими.

Если при вычислении выражения операнды операции имеют разные типы, то возникает необходимость приведения операндов к одному типу. Такая необходимость возникает и тогда, когда операнды имеют один тип, но он несогласован с типом операции. Например при выполнении сложения операнды типа byte должны быть приведены к типу int, поскольку сложение не определено над байтами. При выполнении присваивания x=e тип источника e и тип цели x должны быть согласованы. Аналогично, при вызове метода также должны быть согласованы типы источника и цели – фактического и формального аргументов.

Преобразования ссылочных типов

Поскольку операции над ссылочными типами не определены (исключением являются строки, но операции над ними, в том числе и присваивание, выполняется как над значимыми типами), то необходимость в них возникает только при присваиваниях и вызовах методов. Семантика таких преобразований рассмотрена в предыдущей лекции 3, где подробно обсуждалась семантика присваивания и совпадающая с ней семантика замены формальных аргументов фактическими. Там же подробно говорилось о преобразованиях между ссылочными и значимыми типами, выполняемых при этом операциях упаковки значений в объекты и обратной их распаковки.

Коротко повторю основные положения, связанные с преобразованиями ссылочных типов. При присваиваниях (замене аргументов) тип источника должен быть согласован с типом цели, что означает, что объект, связанный с источником, должен принадлежать классу, являющемуся потомком класса цели. В случае согласования типов ссылочная переменная цели связывается с объектом источника, и ее тип динамически изменяется, становясь типом источника. Это преобразование выполняется автоматически и неявно, не требуя от программиста никаких дополнительных указаний. Если же тип цели является потомком типа источника, то неявное преобразование отсутствует, даже если объект, связанный с источником, принадлежит типу цели. Явное преобразование, заданное программистом, позволяет справиться с этим случаем. Ответственность за корректность явных преобразований лежит на программисте, так что может возникнуть ошибка на этапе выполнения, если связываемый объект реально не является объектом класса цели. За примерами следует обратиться к лекции 3, еще раз обратив внимание на присваивания объектов классов Parent и Child из.

Преобразования типов в выражениях

Продолжая тему преобразований типов, рассмотрим привычные для программистов преобразования между значимыми типами и, прежде всего, преобразования внутри арифметического типа.

В C# такие преобразования делятся на неявные и явные. К неявным преобразованиям относятся те, результат выполнения которых всегда успешен и не приводит к потере точности данных. Неявные преобразования выполняются автоматически. Для арифметических данных это означает, что для неявных преобразований диапазон типа назначения содержит в себе диапазон исходного типа. Например, преобразование из типа byte в тип int относится к неявным, поскольку диапазон типа byte является подмножеством диапазона int. Это преобразование всегда успешно и не может приводить к потере точности. Заметьте, преобразования из целочисленных типов к типам с плавающей точкой относятся к неявным. Хотя здесь и может происходить некоторое искажение значения, но точность представления значения сохраняется, например, при преобразовании из long в double порядок значения сохраняется.

К явным преобразованиям относятся разрешенные преобразования, успех выполнения которых не гарантируется или может приводить к потере точности. Такие потенциально опасные преобразования должны быть явно заданы программистом. Преобразование из типа int в тип byte относится к явным, поскольку оно не безопасно и может приводить к потере значащих цифр. Заметьте, не для всех типов существуют явные преобразования. В этом случае требуется другие механизмы преобразования типов, которые будут рассмотрены позже.

Преобразования внутри арифметического типа

Арифметический тип, как показано в таблице 3-1, распадается на 11 подтипов. На рис. 4.1 показана схема преобразований внутри арифметического типа:

 

Рис. 4.1. Иерархия преобразований внутри арифметического типа

 

Диаграмма, приведенная на рисунке, позволяет отвечать на ряд важных вопросов, связанных с существованием преобразований между типами. Если на диаграмме задан путь (стрелками) от типа А к типу В, то это означает существование неявного преобразования из типа А в тип В. Все остальные преобразования между подтипами арифметического типа существуют, но являются явными. Заметьте, что циклов на диаграмме нет, все стрелки односторонние, так что преобразование, обратное к неявному, всегда должно быть задано явным образом.

Путь, указанный на диаграмме, может быть достаточно длинным, но это вовсе не значит, что выполняется вся последовательность преобразований, следуя данному пути. Наличие пути говорит лишь о существовании неявного преобразования, само преобразование выполняется только один раз, – из типа источника А в тип назначения В.

Иногда возникает ситуация, при которой для одного типа источника может одновременно существовать несколько типов назначений и необходимо осуществить выбор цели – типа назначения. Такие проблемы выбора возникают, например, при работе с перегруженными методами в классах.

Понятие перегрузки методов и операций подробно будет рассмотрено в последующих лекциях. См. лекция 8,??

Диаграмма, приведенная на рис.4.1 и здесь помогает понять, как делается выбор. Пусть существует две или более реализации перегруженного метода, отличающиеся типом формального аргумента. Тогда при вызове этого метода с аргументом типа T может возникнуть проблема выбора, какую реализацию выбрать, поскольку для нескольких реализаций может быть допустимым преобразование аргумента типа T в тип, заданный формальным аргументом данной реализации метода. Правило выбора реализации при вызове метода таково – выбирается та реализация, для которой путь преобразований, заданный на диаграмме, короче. Если есть точное соответствие параметров по типу (путь длины 0), то, естественно, именно эта реализация и будет выбрана.

Давайте рассмотрим еще один тестовый пример. В класс Testing включена группа перегруженных методов OnLoad с одним и двумя аргументами. Вот эти методы:

/// <summary>

/// Группа перегруженных методов OLoad

/// с одним или двумя аргументами арифметического типа.

/// Если фактический аргумент один, то будет вызван один из методов,

/// наиболее близко подходящий по типу аргумента.

/// При вызове метода с двумя аргументами, возможен конфликт выбора

/// подходящего метода, приводящий к ошибке периода компиляции.

/// </summary>

void OLoad(float par)

{

Console.WriteLine("float value {0}", par);

}

/// <summary>

/// Перегруженный метод OLoad с одним параметром типа long

/// </summary>

/// <param name="par"></param>

void OLoad(long par)

{

Console.WriteLine("long value {0}", par);

}

/// <summary>

/// Перегруженный метод OLoad с одним параметром типа ulong

/// </summary>

/// <param name="par"></param>

void OLoad(ulong par)

{

Console.WriteLine("ulong value {0}", par);

}

/// <summary>

/// Перегруженный метод OLoad с одним параметром типа double

/// </summary>

/// <param name="par"></param>

void OLoad(double par)

{

Console.WriteLine("double value {0}", par);

}

/// <summary>

/// Перегруженный метод OLoad с двумя параметрами типа long и long

/// </summary>

/// <param name="par1"></param>

/// <param name="par2"></param>

void OLoad(long par1, long par2)

{

Console.WriteLine("long par1 {0}, long par2 {1}",

par1, par2);

}

/// <summary>

/// Перегруженный метод OLoad с двумя параметрами типа double и double

/// </summary>

/// <param name="par1"></param>

/// <param name="par2"></param>

void OLoad(double par1, double par2)

{

Console.WriteLine("double par1 {0}, double par2 {1}",

par1, par2);

}

/// <summary>

/// Перегруженный метод OLoad с двумя параметрами типа int и float

/// </summary>

/// <param name="par1"></param>

/// <param name="par2"></param>

void OLoad(int par1, float par2)

{

Console.WriteLine("int par1 {0}, float par2 {1}",

par1, par2);

}

Все эти методы устроены достаточно просто. Они сообщают информацию о типе и значении переданных аргументов. Вот тестирующая процедура, вызывающая метод OLoad с разным числом и типами аргументов:

/// <summary>

/// Вызов перегруженного метода OLoad.

/// В зависимости от типа и числа аргументов

/// вызывается один из методов группы.

/// </summary>

public void OLoadTest()

{

OLoad(x);

OLoad(ux);

OLoad(y);

OLoad(dy);

//OLoad(x,ux); //conflict: (int, float) и (long, long)

OLoad(x,(float)ux);

OLoad(y,dy);

OLoad(x,dy);

}

Заметьте, один из вызовов закомментирован, так как он приводит к конфликту на этапе трансляции. Для устранения конфликта при вызове метода пришлось задать явное преобразование аргумента, что показано в строке, следующей за строкой-комментарием.

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

Рис. 4.2. Вывод на печать результатов теста OLoadTest

Приведу все-таки некоторые комментарии. При первом вызове метода тип источника – int, а тип аргумента у четырех возможных реализаций соответственно float, long, ulong, double. Явного соответствия нет, поэтому нужно искать самый короткий путь на схеме. Так как не существует неявного преобразование из типа int в тип ulong (на диаграмме нет пути), то остаются возможными три реализации. Но путь из int в long короче, чем остальные пути, поэтому будет выбрана long-реализация метода.

Следующий вызов демонстрирует еще одну возможную ситуацию. Для типа источника uint существуют две возможные реализации и пути преобразований для них имеют одинаковую длину. В этом случае выбирается та реализация, для которой на диаграмме путь показан сплошной, а не пунктирной стрелкой, потому будет выбрана реализация с параметром long.




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


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


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



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




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