Программирование: C# -- типы данных
Автор (C): Ariel Ortiz Ramirez
Перевод (C): Андрей Киселев


В своей предыдущей статье (http://gazette.linux.ru.net/lg84/ortiz.html) я дал начальные сведения о языке программирования C# в контексте Mono, которая является свободно-распространяемой реализацией платформы Microsoft .NET. В этой статье я расскажу о некоторых особенностях типов данных, поддерживаемых языком программирования C#.

В ходе обсуждения я буду использовать следующие диаграммы для представления переменных и объектов: (http://ga zette.linux.ru.net/lg85/ortiz/misc/ortiz/notation.png).

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

Категории типов

В языке программирования C# типы данных подразделяются на три категории:

К первой категории относятся участки памяти, распределенные под переменные и предназначенные для хранения значений этих переменных. Например, следующий код

int x = 5;

объявляет переменную - целую со знаком, размером 32 бита, с именем x и начальным значением, равным 5. Ниже приведена соответствующая этому объявлению диаграмма: (http://gaze tte.linux.ru.net/lg85/ortiz/misc/ortiz/intvar.png)

Обратите внимание на то, как число 5 размещено на диаграмме.

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

object y = new object();

Соотвтетствующаяљдиаграмма выглядит так: (http://g azette.linux.ru.net/lg85/ortiz/misc/ortiz/objectvar.png)

В данном случае мы видим, что "значением" переменной является адрес объекта, размещенного в динамической памяти.

А теперь посмотрим, что произойдет, если мы создадим две новых переменных и инициализируем их значениями исходных переменных. Пусть имеется следующий код:

int a = x;
object b = y;

Результат такого объявления показан ниже: (http://gaz ette.linux.ru.net/lg85/ortiz/misc/ortiz/copying.png).

Как и следовало ожидать, значение переменной x было скопировано в переменную a. Если изменить одну из этих переменных, то это никак не повлияет на другую. В случае переменных y и b оказывается так, что обе они ссылаются на один и тот же объект. Если изменить состояние объекта, используя переменную y, то эти изменения можно будет наблюдать используя переменную b, и наоборот.

Кроме того, переменные-ссылки могут иметь специальное значение null, которое подразумевает ссылку на несуществующий объект, в пустоту. Продолжим наш пример следующими строками:

y = null;
b = null;

Теперь переменные y и b больше не ссылаются на какой-либо объект: (http://gaz ette.linux.ru.net/lg85/ortiz/misc/ortiz/nullvar.png)

А сам объект становится "мусором". Как я уже упоминал ранее, в C# встроена поддержка "сборщика мусора", это означает, что система автоматически освобождает память, занимаемую такими "мертвыми" объектами. Другие языки, такие как C++ и Pascal, не поддерживают автоматическую "сборку мусора". Поэтому программисты, пишущие на этих языках, вынуждены явно освобождать блоки динамической памяти по мере необходимости. Недостатком такого способа управления памятью является возможность появления "утечек" памяти. Как показывает опыт -- управление памятью "вручную" слишком громоздко и является потенциальным источником ошибок. Вот почему многие современные языки программирования (такие как Java, Python, Scheme, Smalltalk) имеют встроенную в окружение времени исполнения поддержку автоматической "сборки мусора".

И наконец -- переменные-указатели. Они похожи на указатели в языках программирования C и C++. Очень важно понимать, что и переменные-ссылки и переменные-указатели, фактически, представляют собой адрес в памяти, но на этом их сходство заканчивается. Переменные-ссылки отслеживаются сборщиком "мусора", указатели -- нет. Переменные-указатели допускают выполнение арифметических операций над ними, ссылки -- нет. Бесконтрольное использование указателей в программе в принципе небезопасно, поэтому в языке C# допускается работать с только внутуказателямири unsafe (небезопасных) блоков (небезопасный код должен помечаться зарезервированным словом unsafe, например:

static unsafe void FastCopy ( byte[] src, byte[] dst, int count )
{
   // здесь допустимо использовать указатели
}

(кроме того, для компиляции такого модуля, компилятор должен вызываться с ключом --unsafe+ прим. перев.). Однако это тема для отдельной статьи и я не буду углубляться в нее сейчас.

Предопределенные типы

C# имеет ряд предопределенных типов, которые можно использовать в своих программах. Рисунок, приведенный ниже, показывает иерархию предопределенных типов в языке C#: (http://gazet te.linux.ru.net/lg85/ortiz/misc/ortiz/types.png)

В таблице приводится краткое описание каждого из них:

Тип Размер в
байтах
Описание
bool 1 Логический тип. Может иметь только два значения -- true или false.
sbyte 1 Целое со знаком, размером в 1 байт.
byte 1 Целое без знака, размером в 1 байт.
short 2 Кортокое целое со знаком.
ushort 2 Короткое целое без знака.
int 4 Целое со знаком. Для записи целых чисел в тексте программы можно использовать десятичную (по-умолчанию) или шестнадцатеричную (префикс 0x) нотацию. Например: 26, 0x1A
uint 4 Целое без знака. Например: 26U, 0x1AU (суффикс U обязателен)
long 8 Длинное целое со знаком. Например: 26L, 0x1AL (суффикс L обязателен)
ulong 8 Длинное целое без знака. Например: 26UL, 0x1AUL (суффикс UL обязателен)
char 2 Символ юникода (unicode character). Например: 'A' (символ в одиночных кавычках)
float 4 Число с плавающей точкой одинарной точности (IEEE 754). Например: 1.2F, 1E10F (суффикс F обязателен)
double 8 Число с плавающей точкой двойной точности (IEEE 754). Например 1.2, 1E10, 1D (суффикс D НЕ обязателен)
decimal 16 Числовой тип данных, который обычно используется в финансовых расчетах и расчетах с участием денежных единиц, обеспечивает точность - до 28-го знака. Например: 123.45M (суффикс M обязателен)
object 8+ Элементарный базовый тип как для переменных-значений, так и для переменных-ссылок. Не может быть представлен в виде литерала.
string 20+ Непрерывная последовательность символов в юникоде (unicode characters). Например: "hello world!\n" (строка должна заключаться в двойные кавычки)

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

Классы и структуры

Язык программирования C# предоставляет программисту возможность создания новых типов данных, как ссылочных, так и типов-значений. Ссылочные типы создаются с помощью зарезервированного слова class, а типы-значения - struct. Рассмотрим порядок определения новых типов более подробно на конкретном примере:

struct ValType {
    public int i;
    public double d;

    public ValType(int i, double d)
    {
        this.i = i;
        this.d = d;
    }

    public override string ToString()
    {
        return "(" + i + ", " + d + ")";
    }
}

class RefType {
    public int i;
    public double d;

    public RefType(int i, double d)
    {
        this.i = i;
        this.d = d;
    }

    public override string ToString()
    {
        return "(" + i + ", " + d + ")";
    }
}

public class Test
{
    public static void Main (string[] args)
    {
    // PART  1
        ValType v1;
        RefType r1;
        v1 = new ValType(3,  4.2);
        r1 = new RefType(4, 5.1);
        System.Console.WriteLine("PART 1");
        System.Console.WriteLine("v1 = " + v1);
        System.Console.WriteLine("r1 = " + r1);

    // PART 2
        ValType v2;
        RefType r2;
        v2 = v1;
        r2 = r1;
        v2.i++; v2.d++;
        r2.i++; r2.d++;
        System.Console.WriteLine("PART 2");
        System.Console.WriteLine("v1 = "  + v1);
        System.Console.WriteLine("r1 = " + r1);
    }
}

Первой, в этом примере, объявляется структура ValType. Она имеет два поля -- i и d типа int и double соответственно. Область видимости полей задана как public, это означает, что к переменным можно обратиться из любого места в программе, где доступна сама структура. Структура имеет конструктор, с именем, совпадающим с именем самой структуры. В данном случае на конструктор возложена обязанность по инициализации полей структуры. Зарезервированное слово this используется для получения ссылки на экземпляр структуры, во-избежание неоднозначности, которая возникает из-за совпадения имен полей структуры и имен параметров, передаваемых конструктору. В структуре также имеется метод ToString, который возвращает значения переменных i и d в виде строки. Этот метод перекрывает стандартный метод ToString класса object (это объясняет наличие модификатора override). Результатом работы метода является строка в виде - "(i, d)", которая создается с помощью оператора (+) конкатенации (слияния) строк, где вместо i и d подставляются фактические значения этих переменных.

Код класса RefType практически эквивалентен коду структуры ValType. А теперь рассмотрим как работают переменные-значения и переменные-ссылки, чтобы более глубоко понять отличия между ними. Метод Main класса Test является точкой входа в программу. В первой части программы (которая начинается с комментария "PART 1") создается одна переменная-значение и одна переменная-ссылка. На рисунке ниже показаны эти переменные после их создания (http://gazett e.linux.ru.net/lg85/ortiz/misc/ortiz/v1r1.png).
Переменная-значение v1 представляет собой структуру ValType. Оператор new, в строке

v1 = new ValType(3, 4.2);

не выделяет дополнительной памяти в "куче", поскольку тип ValType представляет собой тип-значение, и нужен лишь для того, чтобы вызвать конструктор и таким образом инициализировать структуру. Поскольку переменная v1 фактически является локальной переменной метода Main, то она размещается на стеке.

Объекты, вызываемые по ссылке, создаются аналогичным образом

r1 = new RefType(4, 5.1);

где оператор new выделяет блок динамической памяти, необходимый для размещения объекта, поскольку переменная типа RefType является переменной ссылочного типа. После выделения памяти вызывается соответствующий конструктор. Переменная r1 так же размещается на стеке, поскольку она, как и v1, является локальной переменной. Для нее резервируется места на стеке ровно столько, сколько необходимо для хранения ссылки (адреса) на экземпляр объекта. Сам же объект размещается в динамической памяти (в "куче").

Теперь перейдем ко второй части программы (которая начинается с комментария "PART 2") и посмотрим на получившиеся результаты. Во второй части вводятся в действие две новых переменных, которые инициализируются простым присваиванием значений первых двух. Затем для каждого поля новых переменных выполняется операция увеличения на единицу (++) (http://gazett e.linux.ru.net/lg85/ortiz/misc/ortiz/v2r2.png).

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

Если взглянуть на диаграмму иерархии типов, которая приведена выше, то можно заметить, что простые типы, такие как int, bool, char -- являются структурами (struct - тип-значение), в то время как object и string - классами (class), т.е. ссылочными типами.

Полный исходный код примера вы можете взять здесь: htt p://gazette.linux.ru.net/lg85/ortiz/misc/ortiz/varsexample.cs.txt, на случай, если у вас возникнет желание поэксперементировать с этой программой. Чтобы скомпилировать и запустить его -- выполните следующие команды:

mcs varsexample.cs
mono varsexample.exe

Программа должна выдать:

PART 1
v1 = (3, 4.2)
r1 = (4, 5.1)
PART 2
v1 = (3, 4.2)
r1 = (5, 6.1)

Ссылки

http://www.go-mono.com/
Официальная домашняя страничка проекта Mono. Здесь вы можете скачать исходные тексты платформы Mono и найти инструкции по установке, включая компилятор с языка C#,библиотеку времени исполнения и библиотеку классов.
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cscon/html/vco riCStartPage.asp
MSDN. Информация о языке программирования C#

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

http://www.dotsite.spb.ru/
Сайт целиком посвящен технологии .NET
http://www.javapower.ru/net/index. htm
Сайт посвященный Java, JavaScript, .Net и сопутствующим им технологиям.

Copyright (C) 2002, Ariel Ortiz Ramirez. Copying license http://www.linuxgazette.com/copying.html
Published in Issue 85 of Linux Gazette, December 2002

Команда переводчиков:
Владимир Меренков, Александр Михайлов, Иван Песин, Сергей Скороходов, Александр Саввин, Роман Шумихин, Александр Куприн, Андрей Киселев

Со всеми предложениями, идеями и комментариями обращайтесь к Сергею Скороходову ([email protected]). Убедительная просьба: указывайте сразу, не возражаете ли Вы против публикации Ваших отзывов в рассылке.

Сайт рассылки: http://gazette.linux.ru.net
Эту статью можно взять здесь: http://gazette.linux.ru.net/lg85/ortiz.html