...
...

Linux и C#. Типы данных

Linux и C#. Типы данных

В одной и предыдущих заметок мы рассказывали о языке программирования C# и его связи с Linux в контексте Mono, которая является свободнораспространяемой реализацией платформы Microsoft.NET. В этой заметке мы рассмотрим некоторые особенности типов данных, поддерживаемых языком программирования C#.

В ходе обсуждения мы будем использовать следующие диаграммы для представления переменных и объектов:



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

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

  • значение;
  • ссылка;
  • указатель.

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

    int x = 5;

    объявляет переменную — целую со знаком размером 32 бита, с именем x и начальным значением, равным 5. Ниже приведена соответствующая этому объявлению диаграмма:



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

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

    object y = new object();

    Соответствующая диаграмма выглядит так:



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

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

    int a = x;
    object b = y;

    Результат такого объявления показан ниже:



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

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

    y = null;
    b = null;

    Теперь переменные y и b больше не ссылаются на какой-либо объект:



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

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

    static unsafe void FastCopy (byte[] src, byte[] dst, int count)

    {

    // здесь допустимо использовать указатели

    }


    Предопределенные типы.
    C# имеет ряд предопределенных типов, которые можно использовать в своих программах. Рисунок, приведенный ниже, показывает иерархию предопределенных типов в языке C#:



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

    Тип Размер в байтах Описание
    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") создается одна переменная-значение и одна переменная-ссылка. На рисунке ниже показаны эти переменные после их создания.



    Переменная-значение v1 представляет собой структуру ValType. Оператор new в строке

    v1 = new ValType(3, 4.2);

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

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

    r1 = new RefType(4, 5.1);

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

    Теперь перейдем ко второй части программы (которая начинается с комментария PART 2) и посмотрим на получившиеся результаты. Во второй части вводятся в действие две новые переменные, которые инициализируются простым присваиванием значений первых двух. Затем для каждого поля новых переменных выполняется операция увеличения на единицу (++).



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

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

    По материалам Ariel Ortiz Ramirez и Андрея Киселева
    Подготовил X-Stranger



    © Компьютерная газета

    полезные ссылки
    Аренда ноутбуков