Опубликован: 25.09.2008 | Доступ: свободный | Студентов: 3197 / 511 | Оценка: 4.32 / 3.98 | Длительность: 18:50:00
ISBN: 978-5-94774-991-5
Лекция 3:

Основы языка C#

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

Цель лекции: познакомиться с основными отличительными особенностями языка C#, рассмотреть примеры использования новых средств и операторов языка, типов данных и их преобразований в объеме, необходимом для дальнейшего изучения материала.

Основные операторы языка C#

Состав и синтаксис операторов C# унаследован от языка С++. Тем не менее существует ряд отличий, которые улучшают некоторые характеристики C++, делая его более легким в использовании. Предполагая, что читатель уже знаком с основными операторами языка C++ и имеет минимальный опыт программирования, остановимся на рассмотрении только наиболее значимых операторов C#, а также операторов, специфических для данного языка программирования.

Цикл foreach

Новым видом цикла, который появился в C# и отсутствует в C++, является цикл foreach. Он удобен при работе с массивами, коллекциями и другими контейнерами данных. Синтаксис оператора выглядит следующим образом:

foreach (тип идентификатор in контейнер) оператор

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

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

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

public void SumMinMax()
{
  int[,] myArray = new int[10, 10];
  Random rnd = new Random();
  for (int i = 0; i < 10; i++)
  {
    for (int j = 0; j < 10; j++)
    {
      myArray[i, j] = rnd.Next(100);
      Response.Write(myArray[i, j] + " ");
    }
    Response.Write("<br/>");
  }
  long sum = 0;
  int min = myArray[0, 0];
  int max = myArray[0, 0];
  foreach (int i in myArray)
  {
    sum += i;
    if (i > max) max = i;
    if (i < min) min = i;
  }
  Response.Write("Sum=" + sum.ToString() + " Min=" +
  min.ToString() + " Max=" + max.ToString());
}

Типы данных. Преобразования типов

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

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

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

Для большинства процедурных языков, реально используемых программистами - Паскаль, C++, Java, Visual Basic, C#, — система встроенных типов более или менее одинакова. Всегда в языке присутствуют арифметический, логический (булев), символьный типы. Арифметический тип всегда разбивается на подтипы. Всегда допускается организация данных в виде массивов и записей (структур). Внутри арифметического типа всегда допускаются преобразования, всегда есть функции, преобразующие строку в число и обратно.

Поскольку язык C# является непосредственным потомком языка C++, то и системы типов этих двух языков близки и совпадают вплоть до названий типов и областей их определения. Но отличия, в том числе принципиального характера, есть и здесь.

Система типов

Давайте рассмотрим, как устроена система типов в языке C#, но вначале для сравнения приведем классификацию типов в стандарте языка C++.

Стандарт языка C++ включает следующий набор фундаментальных типов.

  1. Логический типbool.
  2. Символьный типchar.
  3. Целые типы. Целые типы могут быть одного из трех размеров: short, int, long, сопровождаемые описателем signed или unsigned, который указывает, как интерпретируется значение — со знаком или без оного.
  4. Типы с плавающей точкой. Эти типы также могут быть одного из трех размеров — float, double, long double.
  5. Тип void, используемый для указания на отсутствие информации.
  6. Указатели (например, int* — типизированный указатель на переменную типа int ).
  7. Ссылки (например, double& — типизированная ссылка на переменную типа double ).
  8. Массивы (например, char[] — массив элементов типа char ).
  9. Перечислимые типы enum для представления значений из конкретного множества.
  10. Структурыstruct.
  11. Классы.

Первые три вида типов называются интегральными, или счетными. Значения их перечислимы и упорядочены. Целые типы и типы с плавающей точкой относятся к арифметическому типу. Типы подразделяются также на встроенные и типы, определенные пользователем.

Эта схема типов сохранена и в языке C#. Однако здесь на верхнем уровне используется и другая классификация, имеющая для C# принципиальный характер. Согласно этой классификации, все типы можно разделить на четыре категории:

  • Типы-значенияvalue, или значимые типы.
  • Ссылочныеreference.
  • Указателиpointer.
  • Тип void.

Эта классификация основана на том, где и как хранятся значения типов. Для ссылочного типа значение задает ссылку на область памяти в " куче ", где расположен соответствующий объект. Для значимого типа используется прямая адресация, значение хранит собственно данные, и память для них отводится, как правило, в стеке.

В отдельную категорию выделены указатели, что подчеркивает их особую роль в языке. Указатели имеют ограниченную область действия и могут использоваться только в небезопасных блоках, помеченных как unsafe.

Особый статус имеет и тип void, указывающий на отсутствие какого-либо значения.

В языке C# жестко определено, какие типы относятся к ссылочным, а какие к значимым. К значимым типам относятся: логический, арифметический, структуры, перечисление. Массивы, строки и классы относятся к ссылочным типам. На первый взгляд, такая классификация может вызывать некоторое недоумение: почему это структуры, которые в C++ близки к классам, относятся к значимым типам, а массивы и строки — к ссылочным. Однако ничего удивительного здесь нет. В C# массивы рассматриваются как динамические, их размер может определяться на этапе вычислений, а не в момент трансляции. Строки в C# также рассматриваются как динамические переменные, длина которых может изменяться. Поэтому строки и массивы относятся к ссылочным типам, требующим распределения памяти в " куче ".

Со структурами дело обстоит иначе. Структуры C# представляют частный случай класса. Определив свой класс как структуру, программист получает возможность отнести класс к значимым типам, что иногда бывает крайне полезно. В C# только благодаря структурам появляется возможность управлять отнесением класса к значимым или ссылочным типам. Правда, это неполноценное средство, поскольку на структуры накладываются дополнительные ограничения по сравнению с обычными классами.

Согласно принятой классификации все типы делятся на встроенные и определенные пользователем. Все встроенные типы C# однозначно отображаются, а фактически совпадают с системными типами каркаса .NET Framework, размещенными в пространстве имен System. Поэтому всюду, где можно использовать имя, например int, с тем же успехом можно использовать и имя System.Int32.

Система встроенных типов языка C# не только содержит практически все встроенные типы (за исключением long double ) стандарта языка C++, но и перекрывает его разумным образом. В частности, тип String является встроенным в язык, что вполне естественно. В области совпадения сохранены имена типов, принятые в C++, что облегчает жизнь тем, кто привык работать на C++, но собирается по тем или иным причинам перейти на язык C#.

Язык C# в большей степени, чем язык C++, является языком объектного программирования. В языке C# сглажено различие между типом и классом. Все типы — встроенные и пользовательские — одновременно являются классами, связанными отношением наследования. Родительским, базовым классом является класс Object. Все остальные типы или, точнее, классы являются его потомками, наследуя методы этого класса. У класса Object есть четыре наследуемых метода:

  1. bool Equals (object obj) — проверяет эквивалентность текущего объекта и объекта, переданного в качестве аргумента;
  2. System.Type GetType() — возвращает системный тип текущего объекта;
  3. String ToString() — возвращает строку, связанную с объектом. Для арифметических типов возвращается значение, преобразованное в строку;
  4. int GetHashCode() — служит как хэш-функция в соответствующих алгоритмах поиска по ключу при хранении данных в хэш-таблицах.

Естественно, что все встроенные типы нужным образом переопределяют методы родителя и добавляют собственные методы и свойства. Учитывая, что и типы, создаваемые пользователем, также являются потомками класса Object, для них необходимо переопределить методы родителя, если предполагается использование этих методов ; реализация родителя, предоставляемая по умолчанию, не обеспечивает нужного эффекта.

Рассмотрим несколько примеров. Начнем с вполне корректного в языке C# примера объявления переменных и присваивания им значений:

int x=11;
int v = new Int32();
v = 007;
String s1 = "Agent";
s1 = s1 + v.ToString() +x.ToString();

В этом примере переменная x объявляется как обычная переменная типа int. В то же время для объявления переменной v того же типа int используется стиль, принятый для объектов. В объявлении применяется конструкция new и вызов конструктора класса. В операторе присваивания, записанном в последней строке фрагмента, для обеих переменных вызывается метод ToString, как это делается при работе с объектами. Этот метод, наследуемый от родительского класса Object и переопределенный в классе int, возвращает строку с записью целого. Отметим еще, что класс int не только наследует методы родителя - класса Object, — но и дополнительно определяет метод CompareTo, выполняющий сравнение целых, и метод GetTypeCode, возвращающий системный код типа. Для класса Int определены также статические методы и поля, о которых поговорим чуть позже.

Так что же такое после этого int, спросите вы: тип или класс? Ведь ранее говорилось, что int относится к value-типам, следовательно, он хранит в стеке значения своих переменных, в то время как объекты должны задаваться ссылками. С другой стороны, создание экземпляра с помощью конструктора, вызов методов, наконец, существование родительского класса Object, — все это указывает на то, что int — это настоящий класс. Правильный ответ состоит в том, что int — это и тип, и класс. В зависимости от контекста x может восприниматься как переменная типа int или как объект класса int. Это же верно и для всех остальных значимых типов. Стоит отметить, что все значимые типы фактически реализованы как структуры, представляющие частный случай класса.

Такая двойственность в языке C# обусловлена тем, что значимые типы эффективнее в реализации, им проще отводить память, так что именно соображения эффективности реализации заставили авторов языка сохранить значимые типы. Более важно, что зачастую необходимо оперировать значениями, а не ссылками на них, хотя бы из-за различий в семантике присваивания для переменных ссылочных и значимых типов.

С другой стороны, в определенном контексте крайне полезно рассматривать переменные типа int как настоящие объекты и обращаться с ними как с объектами. В частности, полезно иметь возможность создавать и работать со списками, чьи элементы являются разнородными объектами, в том числе и принадлежащими к значимым типам.

Семантика присваивания

Рассмотрим присваивание: x = e.

Чтобы присваивание было допустимым, типы переменной x и выражения e должны быть согласованными. Пусть сущность x согласно объявлению принадлежит классу T. Будем говорить, что тип T основан на классе T и является базовым типом x, так что базовый тип определяется классом объявления. Пусть теперь в рассматриваемом нами присваивании выражение e связано с объектом типа T1.

Определение: тип T1 согласован по присваиванию с базовым типом T переменной x, если класс T1 является потомком класса T.

Присваивание допустимо, если и только если имеет место согласование типов. Так как все классы в языке C# — встроенные и определенные пользователем — по определению являются потомками класса Object, то отсюда и следует наш частный случай: переменным класса Object можно присваивать выражения любого типа.

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

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

class Child:Parent {...}

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

Parent p1 = new Parent(), p2 = new Parent();
Child ch1 = new Child(), ch2 = new Child();

Тогда допустимы присваивания р1 = 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, но ее тип — тип того объекта, с которым она связана в текущий момент, — может динамически изменяться.