Структурные и ссылочные типы

ЛЕКЦИЯ 1

Введение. Платформа Microsoft .NET

Язык C# появился в 2000 году и стал основой новой стратегии развития компании Microsoft. Главным архитектором этого языка является Андерс Хейлсберг (он же в 1980-х годах был автором Turbo Pascal).

C# стал потомком языков C, C++ и Java. Можно даже сказать, что он стал их эволюционным продолжением, объединив в себе основные преимущества и доработав положительные стороны описанных языков. Поскольку язык C# позиционируется в семействе С-языков, он перенял основу синтаксиса языка С++. Поэтому С++ программисту будет достаточно легко «перейти» на C#. Язык С# является частью платформы, называемой платформой .NET.

Среди преимуществ технологии .NET можно выделить следующие:

• межплатформенная переносимость;

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

• среда исполнения программных решений, которая предоставляет режим безопасного исполнения потенциально опасных программных продуктов, контроль за распределением ресурсов.

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

Одним из самых весомых достижений платформы .NET Framework является создание интегрированной среды времени выполнения приложений (Common Language Runtime), благодаря которой стало возможным использование безопасного (или управляемого) кода (Managed Code). Одно из преимуществ безопасного кода состоит в том, что среда исполнения сама управляет выделением памяти и различного рода ресурсов, а так же руководит доступом к ним. Это позволяет разработчику, с одной стороны, избежать утечки памяти, а, с другой, сконцентрироваться на концептуальной части кода, обособившись от вопросов, связанных с управлением памятью и ресурсами, в самом широком смысле.

Однако, наряду с преимуществами, которые нам даёт платформа .NET, она имеет и несколько серьёзных недостатков:

Во-первых, из-за того, что компиляция происходит после запуска приложения, мы получаем задержки при первом запуске приложения. Этот недостаток является весьма относительным: поскольку получившийся в результате компиляции исполняемый файл сохраняется во временной директории, то при последующих запусках приложения не будет происходить повторной компиляции. Однако, если будет запущена новая версия приложения (например если изменить что-то в сборке и запустить), то произойдёт перекомпиляция приложения и старый исполняемый файл будет заменён на новый. Так же повторная компиляция будет происходить всякий раз, как получившийся временный исполняемый файл будет удалён (например: в результате действия служебных приложений, которые удаляют временные файлы).

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

Последний недостаток, который мы отметим, – это межплатформенная непереносимость .NET-приложений. Теоретически, при наличии в операционной системе .NET Framework, .NET-приложение должно корректно работать в любой операционной системе, однако .NET Framework существует только для операционных систем Microsoft Windows. Существует аналог Framework для операционных систем Linux (проект Mono), однако в силу концептуальных отличий архитектуры Linux от Windows, приложение придётся модифицировать с учётом этих отличий.

Платформа .NET базируется на двух основных компонентах: Common Language Runtime и .NET Framework Class Library.

CLR (Common Language Runtime, общеязыковая среда исполнения) – является основой, исполняющей приложения .NET, обычно написанные на CIL (Common Intermediate Language – общий промежуточный язык). Среда берет на себя работу по компиляции и выполнению кода, управлению памятью, работе с потоками, обеспечению безопасности и удалённого взаимодействия.

NET Framework Class Library – универсальный набор классов, для использования в разработке программ. Она, во-первых, упрощает взаимодействие программ, написанных на разных языках, за счет стандартизации среды выполнения. Во-вторых, FCL позволяет компилятору генерировать более компактный код, что актуально при распространении программ через интернет. В терминологии .NET Framework FCL так же называют BCL (англ. Base Class Library – Базовая Библиотека Классов).

Типы Данных

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

Технология .NET Framework определяет две группы типов данных: значащие типы или типы значений (value-types) и ссылочные типы(reference-types). Экземпляры значащих типов располагаются в стеке, тогда как ссылочные – в управляемой оперативной памяти (куче), в терминологии .NET Framework называемой managed heap.

В C# определены девять целочисленных типов: char, byte, sbyte, short, ushort, int, uint, long и ulong. Но тип char применяется, главным образом, для представления символов и поэтому рассматривается отдельно. Остальные восемь целочисленных типов предназначены для числовых расчетов. Ниже представлены их диапазон представления чисел и разрядность в битах:

Пример работы с целочисленными типами:

int param = 170;

int paramBase16 = 0xaa;

 

Типы с плавающей точкой позволяют представлять числа с дробной частью. В C# имеются две разновидности типов данных с плавающей точкой: float и double. Они представляют числовые значения с одинарной и двойной точностью соответственно. Так, разрядность типа float составляет 32 бита, что приближенно соответствует диапазону представления чисел от 5E-45 до 3,4E+38. А разрядность типа double составляет 64 бита, что приближенно соответствует диапазону представления чисел от 5E-324 до 1,7Е+308.

Тип данных float предназначен для меньших значений с плавающей точкой, для которых требуется меньшая точность. Тип данных double больше, чем float, и предлагает более высокую степень точности (15 разрядов).

Пример:

float f = 12.3F;

Для представления чисел с плавающей точкой высокой точности предусмотрен также десятичный тип decimal, который предназначен для применения в финансовых расчетах. Этот тип имеет разрядность 128 бит для представления числовых значений в пределах от 1Е-28 до 7,9Е+28. Для обычных арифметических вычислений с плавающей точкой характерны ошибки округления десятичных значений. Эти ошибки исключаются при использовании типа decimal, который позволяет представить числа с точностью до 28 десятичных разрядов. Благодаря тому что этот тип данных способен представлять десятичные значения без ошибок округления, он особенно удобен для расчетов, связанных с финансами.

Пример:

decimal totalMoney = 2005m, workedHours = 263m;

decimal ratePerHour = totalMoney / workedHours;

// 7,6235741444866920152091254753

В .NET Framework тип данных char используется для выражения символьной

информации и представляет символ в формате Unicode. Формат Unicode предусматривает, что каждый символ идентифицируется 21-битным скалярным значением, называемым “code point” («кодовая точка» или «кодовый пункт»), и предусматривает кодовую форму UTF-16, которая определяет, как кодовая точка декодируется в последовательность из одного или более 16-битного значения.

Таким образом, значение объекта типа char – это его 16-битное числовое положительное значение.

Пример:

char char1 = 'A';

char char2 = 'V';

bool compare = 'A' > 'V'; //false

 

Среди перечисленных выше базовых типов данных в .NET Framework предусмотрены такие типы как object и string .

Тип данных string (.NET Class: System.String) представляет собой множество символов.

Класс String содержит большое количество методов для работы со строками, например:

Compare – сравнивает указанные подстроки двух переданных в качестве аргументов строк

Concat – возвращает результат объединения переданного массива строк.

Copy – создаёт новый экземпляр строки.

Equals – сравнивает значение двух строк.

Remove – удаляет из текущей строки все вхождения указанной подстроки.

Replace – заменяет указанную подстроку новой подстрокой.

Split – делит строку на массив строк, основываясь на переданном массиве

разделителей.

Литералы

Литералы в С# - это фиксированные значения, которые представлены в понятной форме. Следуя традиции языков семейства «С» литералы так же можно называть константными значениями. Так, например, 100 – это целочисленный литерал (константное целое число).

Так как С# - строго типизированный язык, то все литералы должны иметь тип.

Для явной спецификации типа данных литерала в C# предусмотрены специальные суффиксы. Таким образом литера:

• объявленный с суффиксом «L» будет иметь тип long;

• с суффиксом «F» будет иметь тип float;

• с суффиксом «D» будет иметь тип double;

• с суффиксом «M» будет иметь тип decimal;

• суффикс «U» делает число беззнаковым (суффикс «U» может быть объединён с суффиксами, специфицирующими тип данных).

Пример:

10D // double

10F // float

10M // decimal

10 // int

10L // long

10UL // unsigned long

В отдельную группу литералов выделены управляющие символьные последовательности, которые не имеют символьного эквивалента, а преимущественно используются для форматирования текста:

\a Звуковой сигнал

\b Возврат на одну позицию

\f Переход к началу следующей страницы

\n Новая строка

\r Возврат каретки

\t Горизонтальная табуляция

\v Вертикальная табуляция

\0 Нуль-символ (символ конца строки)

\ Экранирование таких символов, как " ' \

Пример:

"Hello \"World\"!\n" // Hello "World"!


 

ЛЕКЦИЯ 2

Переменные

Переменная – это именованный объект, хранящий значение некоторого типа данных. Язык C# относиться к «type-safe» языкам. Иными словами, компилятор C# (C# compiler) гарантирует нам, что расположенное в переменной значение всегда будет одного и того же типа. Так как C# является строго типизированным (strongly typed) языком программирования, то при объявлении переменной обязательно нужно указывать её тип данных. В общем виде объявление переменной выглядит следующим образом:

тип_переменной имя_переменной;

тип_переменной имя_переменной = значение;

 

Важным моментом является начальное значение (initial value) переменной. Дело в том, что согласно синтаксису языка C# переменная должна быть обязательно инициализирована перед использованием.

Пример:

double a, b = 5;

double result = a / b; //ошибка использование переменной "а", которой не присвоено значение

Согласно правилам языка С#, при объявлении идентификаторов можно использовать алфавитно-цифровые символы и символ подчёркивания (нижний слеш). Начинаться идентификатор должен с буквы или символа подчёркивания. Не допустимо начинать идентификатор с цифры. Ниже приведены примеры нескольких идентификаторов:

int SomeVar; //допустимый идентификатор

int somevar; //допустимый идентификатор

int _SomeVar; //допустимый идентификатор

int SomeVar2; //допустимый идентификатор

int 3_SomeVar; //не допустимый идентификатор

Язык C# регистро-зависим. Поэтому «SomeVar» и «somevar», будут восприниматься как различные идентификаторы. Само собой разуметься, что не разрешается использовать ключевые слова в качестве идентификаторов, однако можно использовать ключевые и зарезервированные слова, предварив их символом «@».Однако это считается плохой практикой и не является хорошим примером для повторения.

Пример:

int @int;

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

Pascal case convention (или просто нотация Паскаля) предлагает начинать каждое отдельное слово в идентификаторе с символа в Верхнем регистре. Разделители между словами не используются. Начинаться идентификатор так же должен с символа в верхнем регистре. В идентификаторах предлагается использовать только алфавитные (буквенные) символы латинского алфавита.

Пример:

double SomeVariable;

int EveryWordStartsWithUpperCharacter;

Camel case convention идентичен Pascal case, но с одним отличием – начинается идентификатор с символа в нижнем регистре, а все последующие отдельные, составляющие идентификатор, слова начинаются с символа в верхнем регистре.

Пример:

int someVariable;

double totalCount;

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

Использование стилей именования в значительной мере упрощает работу с кодом, понимание незнакомых ранее конструкций. Также существует практика применения различных стилей к различным типа (например классы - именовать в стиле Паскаля, методы и переменные в Camel, константы - Uppercase convertion).

Область видимости переменной – это та часть кода, в пределах которой переменная доступна для использования. Областью видимости переменной является блок, в котором она объявлена. Блок начинается открывающей, а заканчивается закрывающей фигурными скобками.

Пример:

{

//first block

}

 

if (a > 5) {

//another block

}

 

int someFunction() {

//one more block

}

 

Блок может находиться внутри другого блока. В таком случае вводятся понятия

внешнего и внутреннего блока соответственно. Пример вложенного блока приведён ниже:

int someFunc() {

//block A

int a = 5;

if (a > 2) {

//block B

a += 2;

int b = a;

if (b == 7) {

//block C

int c = 1;

a = a + b + c;

}

//c += 2; ошибка

}

//b = 0; ошибка

return a;

}

В примере есть области видимости: A, B, C. В блоке А доступна только переменная "а". В блоке B доступны переменные a, b. В блоке C доступны переменные a,b,c.

«Время жизни переменной» начинается с момента её объявления и заканчивается закрывающей фигурной скобкой блока, в котором она была объявлена.

Структурные и ссылочные типы

В С# определены две категориитипов данных:

•Структурные типы данных или типы значений (иногда употребляется название значащие типы) (value-types)

Ссылочные (referenced – types).

К типам значений относятся все объекты структур, а к ссылочным – объекты классов. Переменная значащего типа возвращает дубликат объекта, с которым связана, тогда как переменная ссылочного типа возвращает ссылку на объект, с которым связана. Отличие состоит и в размещении объектов в памяти: объекты значащих типов размещаются в стеке целиком, тогда как переменная ссылочного типа, размещённая в стеке, хранит только ссылку на объект, который в действительности расположен в «куче» (основной массе оперативной памяти, выделяемой для хранения относительно больших объёмов данных).

Таким образом, если мы объявим переменную значащего типа “X” со стартовым значением 10 и переменную “Y” – равную “X”, а потом изменим значение “X“, то значение “Y” останется неизменным, поскольку при присваивании значение переменной “X” было сдублировано. Этот процесс демонстрируется на следующем примере:

int x = 10;

int y = x;

x = x – 5;

//x = 5, y = 10

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

SomeObject a = new SomeObject(); //создание нового объекта

SomeObject b = a; //ссылка на объект скопирована, a,b ссылаются на один объект

//a.changeState(1); изменяет и объект, доступный спомощью переменной b

SomeObject c = new SomeObject()

c.changeState(2);

b = c;

// a ссылается на объект с состоянием 1

// b,c ссылаются на объект с состоянием 2

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

Преобразование типов (приведение типов) – это процесс перевода значения объекта из одного типа данных в другой. Отличают две формы приведения типов:

неявное (implicit) приведение (компилятор самостоятельно определяет – к какому типу данных необходимо привести значение).

явное (explicit) приведение (тип, к которому нужно привести значение, «явно» указан разработчиком).

Существуют правила приведения типов. Не все типы данных приводимы друг к другу.

Необходимо понимать, что при неявном преобразовании, значение будет приведено к более точному типу данных. То есть неявное приведение между совместимыми типами возможно только в том случае, когда оно происходит без потери информации. Например, тип double является более точными, нежели тип float (у типа double большее число разрядов дробной части). Таким образом, приведение типа float к типу double заключается в отсечении той части дробного числа, хранение которой не позволяет тип данных float. Конечно, возможна ситуация, при которой в переменной типа double храниться число c нулевой дробной частью. В таком случае никакой потери информации (даже при приведении к типу int) не произойдёт. Однако, мы не можем знать на этапе компиляции, какие значения будут принимать переменные на этапе выполнения программы. Поэтому компилятор запрещает такое неявное приведение, при котором потенциально возможна потеря информации. Использование неявного приведения типов показано на приведённом примере:

float a = 3.5f;

int b = 5;

float c = a + b; // c = 8.5

int d = a + b; // ошибка

Неявно допустимо приводить другу к другу следующие типы данных:

byte: short, ushort, int, uint, long, ulong, float, double, decimal

sbyte: short, int, long, float, double, decimal

short: int, long, float, double, decimal

ushort: int, uint, long, ulong, float, double, decimal

int: long, float, double, decimal

uint: long, ulong, float, double, decimal

long: float, double, decimal

ulong: float, double, decimal

char: ushort, int, uint, long, ulong, float, double, decimal

float :double

Для явного приведения типа необходимо указать этот тип данных в скобках

непосредственно перед переменной или выражением. Из предыдущего примера:

int d =(int) a + b; //d = 8

Также важно соблюдать порядок:

float a = 4.9, b = 4.9;

int result1 = (int)a + (int)b; //a->4, b->4; result1 = 4+4 = 8

int result2 = (int)(a + b); //a+b=9.8; 9.8->9; result2 = 9

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

byte: Sbyte или char

sbyte: byte, ushort, uint, ulong, char

short: sbyte, byte, ushort, uint, ulong, char

ushort: sbyte, byte, short, char

int: sbyte, byte, short, ushort, uint, ulong, char

uint: sbyte, byte, short, ushort, int, char

long: sbyte, byte, short, ushort, int, uint, ulong, char

ulong: sbyte, byte, short, ushort, int, uint, long, char

char: sbyte, byte, short

float: sbyte, byte, short, ushort, int, uint, long, ulong, char, decimal

double: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, decimal

decimal: sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double

ЛЕКЦИЯ 3

Операторы

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

Все операторы С# делятся на классы:

1. Арифметические операторы.

2. Операторы отношений.

3. Логические операторы.

4. Битовые операторы.

5. Операторы присваивания.

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

Унарные – получают на вход один операнд. Бинарные – принимают два операнда и, наконец, тернарные (а такой в С# всего один) – принимают три операнда.

Некоторые операторы ведут себя по-разному в зависимости от типа или количества переданных им операндов.

Оператор вычитание (-) является одновременно унарным и бинарным оператором. Если оператор вычитание используется только с одним операндом, оператор меняет его знак и возвращает результат. Если оператор вычитания используется с двумя операндами, он возвращает разность между ними.

Арифметические операторы

Арифметические операторы – используются для вычислений так же как в алгебре, то есть для выполнения основных арифметических действий:

Как и в С++ операторы ++ и -- имеют две формы префиксную и постфиксную.

Пример:

int a = 5, b = -3;

int c = a > b ? 14 : 12; // c=14

int d = ++a * c; // a=6 d=84

int e = d-- / a; //e=14 d=83

e = d / a; //e=13

int firstDigitOfD = d % 10; //3

int secondDigitOfD = d / 10 % 10; //8

string message1 = "d = " + secondDigitOfD + firstDigitOfD; // "d = 83"

string message2 = "сумма цифр d =" + (secondDigitOfD + firstDigitOfD); //"сумма цифр d = 9"

string message3 = "d - " + (d % 2 == 0 ? "четное" : "нечетное") + " число"; //"d - нечетное число"

a = 7;

b = 2;

c = 4;

d += a / b * c < c * a / b ? 1 : -1; // a/b*c=12; c*a/b=14; 12<14 d = 84

int zero = 0;

zero = 5 / zero; // DivideByZeroException: Попытка деления на нуль.

double infinity = 1.0 / 0.0; //infinity=бесконечность

--infinity; //infinity=бесконечность

Операторы отношения

Операторы отношений – бинарные операторы, получают два операнда, сравнивают их значения, а затем возвращают логическое значение (True or False), используются для постановки условия в задаче.

Операторы отношения = = и != применимы к любому объекту, тогда как операторы >, <, >=, <= только к числовым типам и типам, которые поддерживают упорядывачивание.

Пример:

2 > 7 //false

2 != 4// true

8 < 10//true

my” == “my//true

false == true//false

// true > false //ошибка!!!

Логические операторы

Логические операторы – операторы, реализующие операции булевой алгебры – сравнивают два логических типа и возвращают результат сравнения (true or false). Логические операторы предназначены для выполнения самых распространенных логических операций и перечислены в следующей таблице.

Сокращенные(укороченные) формы операторов И и ИЛИ отличаются только тем, что при использовании сокращенной формы вычисление значения второго операнда происходит лишь в случае необходимости.

Пример:

int a = 1, b = 3, c;

c = a > b && a++ % 2 == 0 ? 1 : 0; // c=0;

bool even = a % 2 ==0; //false

a = 1;

b = 3;

c = a > b & a++ % 2 == 0 ? 1 : 0; // c=0; a=2

even = a % 2 ==0; //true

Битовые операторы

Битовые или поразрядные операторы – определены для целых числовых типов данных. С помощью битовых операторов можно проверять и модифицировать состояние отдельных битов соответствующих значений. В таблице приведена сводка таких операторов. Операторы битовой арифметики работают с каждым битом как с самостоятельной величиной.

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

Пример:

int a = 10;

int b = 1;

int result = a >> b;//деление на 2 в степени второго

//операнда, в данном случае в степени 1, то есть просто на 2

Операторы присвоения

Эта группа включает в себя оператор присваивания равно, а также так называемые «составные» или «укороченные» операторы присваивания.

Оператор присваивания обозначается одинарным знаком равенства (=). Его роль в языке С# во многом такая же, как и в других языках программирования – он позволяет присвоить результат выражения или значение одного операнда другому. В С# оператор присваивания = позволяет создавать целую цепочку присвоений. Например:

int а, b, c;

a = b = c = 58;

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

Приоритет операторов определяет порядок, в соответствии с которым они будут выполнены. В С# операторы имеют следующий приоритет:

++(постфиксный) - - (постфиксный)

! ~ + (унарный) – (унарный) ++ (префиксный) - - (префиксный)

* / %

+ -

>> <<

< > <= >=

= = !=

&

^

|

&&

| |

Условия

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

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

а < 34 или Sity ! = " "

Cоставное условное выражение - это–последовательность простых условий или других условных выражений, которые соединены между собой знакамилогических операций. Например:

a > 2 && a < 10 (значение переменной а больше 2 и меньше 10)

a > 5 || b > 5 (значение переменной а больше 5 или значение переменной b больше 5)

!(a < 0) (не верно, что значение переменной а меньше 0)

Условный оператор if

Условные операторы позволяют контролировать поток выполнения, исполняя те операции, которые согласуются с условием.

Условный оператор if используется для разветвления процесса обработки данных на два направления. Он может иметь одну из форм:

• сокращенную

• полную.

Форма сокращенного оператора if:

if (Условное Выражение) Действие;

Логика работы такая же как в С++: