Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка_программирование!!!!!!!.doc
Скачиваний:
1
Добавлен:
09.11.2019
Размер:
1.59 Mб
Скачать

Системные типы данных и их сокращенное обозначение в с#

Как и в любом языке программирования, в С# поставляется свой собственный набор типов данных, применяемых для представления локальных переменных, переменных экземпляра, возвращаемых значений и входных параметров. В отличие от других языков программирования, в С# эти ключевые слова представляют собой нечто гораздо большее, чем просто распознаваемые компилятором знаки. Они, по сути, представляют собой сокращенные варианты обозначения полноценных типов из пространства имен System. В табл. 1.1 перечислены эти системные типы данных вместе с охватываемыми диапазонами значений, ключевыми словами С# и соответствием требованиям CLS (Common Language Specification— общеязыковая спецификация).

Таблица 1. 1 – Внутренние типы данных C#

Сокращенный вариант обозначения в C#

Соответствует ли требованиям CLS

Системный тип

Диапазон значений

Описание

bool

Да

System.Boolean

true или false

Свидетельствует об истинности или ложности

sbyte

Нет

System.SByte

от -128 до 127

8-битное число со знаком

byte

Да

System.Byte

от 0 до 255

8-битное число без знака

short

Да

System.Int16

от -32768 до 32767

16-битное число со знаком

ushort

Нет

System.UInt16

от 0 до 65535

16-битное число без знака

int

Да

System.Int32

от -2147483648 до 2147483647

32-битное число со знаком

uint

Нет

System.UInt32

от 0 до 4294967295

32-битное число без знака

long

Да

System.Int64

от -9223372036854775808 до 9223372036854775807

64-битное число со знаком

ulong

Нет

System.UInt64

от 0 до 18446744073709551615

64-битное число без знака

char

Да

System.Char

от U+0000 до U+ffff

Отдельный 16-битный символ Unicode

float

Да

System.Single

от до

32-битное число с плавающей запятой

double

Да

System.Double

от до

64-битное число с плавающей запятой

decimal

Да

System.Decimal

от до

96-битное число со знаком

string

Да

System.String

Ограничивается объемом системной памяти

Представляет ряд символов в формате Unicode

object

Да

System.Object

Позволяет сохранить любой тип в объектной переменной

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

Каждый из числовых типов (short, int и т.д.) отображается на соответствующую структуру в пространстве имен System. Структуры, по сути, представляют собой "типы значений" и размещаются в стеке. A string и object, с другой стороны, представляют собой "ссылочные типы" и, следовательно, размещаются в управляемой куче.

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

Объявление и инициализация переменных

Объявлять тип данных как локальную переменную (например, переменную, действующую в пределах какого-нибудь члена) можно путем указания типа данных и следом за ним имени самой переменной. Чтобы увидеть, как это делается, рассмотрим несколько примеров. Сначала создадим новый проект типа Console Application по имени BasicDataTypes и обновим класс Program так, чтобы в нем использовался следующий вспомогательный метод, вызываемый из Main():

static void LocalVarDeclarations()

{

Console.WriteLine("=> Data Declarations:");

// Объявлять локальные переменные нужно так:

// типДанных имяПеременной;

int myInt;

string myString;

Console.WriteLine ();

}

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

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

static void LocalVarDeclarations()

{

Console.WriteLine("=> Data Declarations:");

// Объявлять и инициализировать локальные переменные нужно так:

// типДанных имяПеременной = началъноеЗначение;

int myInt = 0;

// Еще можно объявлять локальные переменные и присваивать //им начальное значение в двух отдельных строках.

string myString;

myString = "This is my character data";

Console.WriteLine();

}

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

static void LocalVarDeclarations()

{

Console.WriteLine("=> Data Declarations:");

int myInt = 0;

string myString;

myString = "This is my character data";

// Объявление трех переменных типа bool в одной строке

bool b1 = true, b2 = false, b3 = b1;

Console.WriteLine();

}

Помимо этого, поскольку ключевое слово bool в С# является сокращенным вариантом обозначения такой структуры из пространства имен System, как Boolean, существует возможность размещать любой тип данных с использованием его полного имени (разумеется, то же самое касается и всех остальных ключевых слов, представляющих типы данных в С#). Ниже приведена окончательная версия реализации LocalVarDeclarations ():

static void LocalVarDeclarations()

{

Console.WriteLine("=> Data Declarations:");

// Объявлять и инициализировать локальные переменные нужно так:

// типДанных имяПеременной = началъноеЗначение;

int myInt = 0;

string myString;

myString = "This is my character data";

// Объявление трех переменных типа bool в одной строке

bool b1 = true, b2 = false, b3 = b1;

// Объявление переменной типа bool, путем указания полного имени bool.

System.Boolean b4 = false;

Console.WriteLine("Your data: {0}, {1}, {2}, {3}, {4}, {5}",

myInt, myString, b1, b2, b3, b4);

Console.WriteLine();

}

Область видимости переменных

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

  • Поле, также известное как переменная-член класса, находится в области видимости до тех пор, пока в этой области находится содержащий поле класс (это так же, как и в C++, Java и VB).

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

  • Локальная переменная, объявленная в операторах for, while или подобных им, видима в пределах тела цикла. (Разработчики на C++ отметят, что это такое же поведение, как и регламентированное стандартом ANSI C++. Ранние версии компилятора Microsoft C++ не придерживались этого стандарта, оставляя эти переменные видимыми и после завершения цикла.)

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

int х = 20;

// какой-то код

int х = 30;

Пример:

public static int Main()

{

for (int i = 0; i < 10; i++)

{

Console.WriteLine(i);

} // i покидает область видимости

// Мы можем объявить переменную i снова, потому что

// не существует второй переменной i в данной области видимости

for (int i = 9; i >= 0; i--)

{

Console.WriteLine(i);

} // i покидает область видимости

return 0;

}

Этот код просто печатает числа от 0 до 9, а затем обратно от 9 до 0, используя цикл for. Важно отметить, что переменная i объявляется в этом коде два раза в пределах одного и того же метода. Это можно делать, поскольку переменные i объявляются в двух отдельных циклах, поэтому каждая из них локальна в пределах своего собственного цикла.

Вот другой пример:

class ScopeTest2

{

static int j = 20;

public static void Main()

{

int j = 30;

Console.WriteLine(j);

return;

}

}

Если попытаемся скомпилировать этот код, то получим сообщение об ошибке.

Дело в том, что переменная j, которая определена перед началом цикла for, внутри цикла все еще находится в области видимости и не может из нее выйти до завершения метода Main(). Хотя вторая переменная j (недопустимая) объявлена в контексте цикла, этот контекст вложен в контекст метода Main (). Компилятор не может различить эти две переменных, поэтому не допустит объявления второй из них. Опять-таки, это отличается от поведения C++, где допустимо сокрытие переменных.

Рисунок 1. 2 – Объявление и инициализация переменных

Члены числовых типов данных

Продолжая обсуждение встроенных типов данных С#, нельзя не упомянуть, что числовые типы в .NET поддерживают свойства MaxValue и MinValue, предоставляющие информацию о диапазоне значений, которые могут храниться в данном типе. Помимо свойств MinValue / MaxValue каждый числовой тип еще может иметь и другие полезные члены. Например, тип System.Double позволяет получать значения эпсилона и бесконечности (которые могут представлять интерес для занимающихся решением математических задач). Ниже приведена иллюстрирующая это вспомогательная функция:

static void DataTypeFunctionality()

{

Console.WriteLine("=> Data type Functionality:");

// максимальное значение типа int

Console.WriteLine("Max of int: {0}", int.MaxValue);

// минимальное значение типа int

Console.WriteLine("Min of int: {0}", int.MinValue);

// максимальное значение типа double

Console.WriteLine("Max of double: {0}", double.MaxValue);

// минимальное значение типа double

Console.WriteLine("Min of double: {0}", double.MinValue);

// эпсилон значение типа double

Console.WriteLine("double.Epsilon: {0}", double.Epsilon);

// значение + бесконечности типа double

Console.WriteLine("double.PositiveInfinity: {0}",

double.PositiveInfinity);

// значение - бесконечности типа double

Console.WriteLine("double.NegativeInfinity: {0}",

double.NegativeInfinity);

Console.WriteLine();

}

Запустим приложение и рассмотрим вывод (рисунок 1. 3).

Рисунок 1. 3 – Члены числовых типов данных

Члены System.Boolean

Рассмотрим тип данных System.Boolean. Единственными значениями, которые могут присваиваться типу bool в С#, являются true и false. Из этого должно быть понятно, что System.Boolean не поддерживает свойств MinValue/MaxValue, но зато поддерживает свойства TrueString/FalseString (которые делают строку, соответственно, "истинной" или "ложной"). Давайте попробуем добавить в вспомогательный метод DataTypeFunctionality() следующие операторы кода (на рисунке 1. 4 представлен результат вывода):

Console.WriteLine("bool.FalseString: {0}", bool.FalseString);

Console.WriteLine("bool.TrueString: {0}", bool.TrueString);

Рисунок 1. 4 – Члены типа System. Boolean

Члены System.Char

Текстовые данные в С# представляются с помощью ключевых слов string и char, которые являются сокращенными вариантами обозначения типов System.String и System.Char (в основе которых лежит Unicode). Как вам уже наверняка известно, string позволяет представлять непрерывный набор символов (например, "Hello"), a char — только какой-то конкретный символ в типе string (например, 'Н').

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

static void CharFunctionality()

{

Console.WriteLine("=> char type Functionality:");

char myChar = 'a';

Console.WriteLine("char.IsDigit('a'): {0}", char.IsDigit(myChar));

Console.WriteLine("char.IsLetter('a'): {0}", char.IsLetter(myChar));

Console.WriteLine("char.IsWhiteSpace('Hello There', 5): {0}",

char.IsWhiteSpace("Hello There", 5));

Console.WriteLine("char.IsWhiteSpace('Hello There', 6): {0}",

char.IsWhiteSpace("Hello There", 6));

Console.WriteLine("char.IsPunctuation('?'): {0}",

char.IsPunctuation('?'));

Console.WriteLine();

}

Результат выполнения кода представлен на рисунке 1. 5.

Рисунок 1. 5 – Члены типа System.Char

Выполнение синтаксического анализа значений из строковых данных

Типы данных .NET позволяют генерировать переменную лежащего в основе типа, имея текстовый эквивалент (выполняя синтаксический анализ). Эта возможность может оказаться чрезвычайно полезной при возникновении необходимости в преобразовании некоторых из предоставляемых пользователем данных (например, за счет выбора им того или иного варианта в раскрывающемся списке внутри графического пользовательского интерфейса). Ниже показано, как может выглядеть логика синтаксического анализа на примере метода ParseFromStrings() (рисунок 1. 6):

static void ParseFromStrings()

{

Console.WriteLine("=> Data type parsing:");

bool b = bool.Parse("True");

Console.WriteLine("Value of b: {0}", b);

double d = double.Parse("99,884");

Console.WriteLine("Value of d: {0}", d);

int i = int.Parse("8");

Console.WriteLine("Value of i: {0}", i);

char c = Char.Parse("w");

Console.WriteLine("Value of c: {0}", c);

Console.WriteLine();

}

Рисунок 1. 6 – Выполнение синтаксического анализа значений из строковых данных

Управляющие последовательности символов

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

Таблица 1. 2 – Управляющие последовательности, которые могут применяться со строковыми литералами

Управляющая

последовательность

Описание

\'

Позволяет вставлять в строковый литерал символ одинарной кавычки

\''

Позволяет вставлять в строковый литерал символ двойной кавычки

\\

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

\a

Заставляет систему выдать звуковой сигнал, который в консольных приложениях может служить своего рода звуковой подсказкой пользователю

\n

Позволяет вставлять символ новой строки (на платформах Win32)

\r

Позволяет вставлять символ возврата каретки

\t

Позволяет вставлять в строковый литерал символ горизонтальной табуляции

За счет добавления к строковому литералу префикса @ можно создавать так называемые дословные строки (verabtim string). Использование дословных строк позволяет отключать обработку управляющих последовательностей в литералах и выводить объекты string в том виде, как они есть (например, задавать путь к директориям).

Код с применением управляющих символов:

static void EscapeChars()

{

Console.WriteLine("=> Escape characters:\a");

string strWithTabs = "Model\tColor\tSpeed\tPet Name\a";

Console.WriteLine(strWithTabs);

Console.WriteLine("Everyone loves \"Hello World\"\a");

// The following string is printed verbatim

// thus, all escape characters are displayed.

Console.WriteLine(@"C:\MyApp\bin\Debug\a");

// White space is preserved with verbatim strings.

string myLongString = @"This is a very

very

very

long string";

Console.WriteLine(myLongString);

Console.WriteLine(@"Cerebus said ""Darrr! Pret-ty sun-sets""");

// Adds a total of 4 blank lines (then beep again!).

Console.WriteLine("All finished.\n\n\n\a");

Console.WriteLine();

}

Результат выполнения кода представлен на рисунке 1. 7.

Рисунок 1. 7 – Применение управляющей последовательности символов в строках

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

Пусть имеется определение типа класса:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with type conversions *****");

// Добавление двух переменных типа short и отображение результата.

short numbl = 9, numb2 = 10;

Console.WriteLine("{0} + {1} = {2}", numbl, numb2, Add(numbl, numb2));

Console.ReadLine();

}

static int Add(int x, int y)

{ return x + y; }

}

Обратите внимание, что метод Add() ожидает двух параметров int. Однако метод Main() на самом деле передает две переменных short. Хотя может показаться, что здесь присутствует полное несоответствие типов, программа компилируется и выполняется без ошибок, возвращая в результате ожидаемое значение 19.

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

Поскольку максимальное значение, которое может содержать тип short (32767), вполне вписывается в рамки диапазона типа int (максимальное значение которого составляет 2 147 483 647), компилятор неявно расширяет (widens) каждую переменную типа short до типа int. Официально термин "расширение" применяется для обозначения неявного "восходящего приведения" (upward cast), которое не приводит к потере данных.

Хотя возможность подобного неявного расширения типов в предыдущем примере и сыграла нам на руку, в других случаях она может стать источником ошибок на этапе компиляции. Например, предположим, что для numb1 и numb2 установлены значения, которые (при сложении вместе) превышают максимальное значение short, и сделано так, чтобы значение, возвращаемое методом Add(), сохранялось в новой локальной переменной short, а не просто напрямую выводилось на консоль:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with type conversions *****");

// Следующий код приведет к генерации ошибки на этапе компиляции!

short numbl = 30000, numb2 = 30000;

short answer = Add(numbl, numb2);

Console.WriteLine("{0} + {1} = {2}", numbl, numb2, answer);

Console.ReadLine();

}

В таком случае компилятор выдаст ошибку.

Проблема связана с тем, что хотя метод Add() и способен вернуть переменную int со значением 60000 (поскольку это значение вполне вписывается в диапазон допустимых значений типа System.Int32), сохранение этого значения в переменной типа short невозможно (поскольку оно выходит за рамки диапазона значений этого типа данных).

Формально это означает, что CLR-среде не удалось применить операцию сужения (narrowing operation). Нетрудно догадаться, что операция сужения является логической противоположностью операции расширения, поскольку предусматривает сохранение большего значения внутри меньшей переменной.

При желании проинформировать компилятор о необходимости смириться с возможной потерей данных из-за операции сужения, нужно обязательно применить операцию явного приведения типов (explicit casting), которая в С# выглядит как (). Удостовериться в этом можно на примере следующего обновленного кода Program (его вывод показан на рис. 1.8):

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Fun with type conversions *****");

// Declare two shorts to add.

short numb1 = 30000, numb2 = 30000;

short answer = (short)Add(numb1, numb2);

Console.WriteLine("{0} + {1} = {2}",

numb1, numb2, answer);

NarrowingAttempt();

Console.ReadLine();

}

static int Add(int x, int y)

{ return x + y; }

static void NarrowingAttempt()

{

byte myByte = 0;

int myInt = 200;

myByte = (byte)myInt;

Console.WriteLine("Value of myByte: {0}", myByte);

}

}

Рисунок 1. 8 – Результат выполнения кода с преобразованиями типов данных

Методы

Методы могут быть реализованы в рамках классов или структур (и прототипироваться внутри типов интерфейсов), а также снабжаться различными ключевыми словами (наподобие internal, virtual, public, new и т.д.), уточняющими их поведение.

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

Таблица 1. 3 – Модификаторы параметров в C#

Модификатор параметра

Описание

(отсутствует)

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

out

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

ref

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

params

Этот модификатор позволяет передавать в виде одного логического параметра любое количество аргументов. В каждом методе может присутствовать только один модификатор params и он должен обязательно идти последним в списке параметров

Передача параметров в метод

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

public static int Add(int x, int y)

{

int ans = x + y;

// Вызывающий код не будет видеть этих изменений,

// поскольку модифицируется копия исходных данных.

x = 10000;

y = 88888;

return ans;

}

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

static void Main(string[] args)

{

Console.WriteLine ("***** Fun with Methods *****");

// Передаем две переменные по значению.

int x = 9, y = 10;

Console.WriteLine("Before call: X: {0}, Y: {1}", x, y);

Console.WriteLine("Answer is: {0}", Add(x, y));

Console.WriteLine("After call: X: {0}, Y: {1}", x, y);

Console.ReadLine();

// до вызова

// ответ

// после вызова

}

Как и следовало ожидать, значения х и у будут выглядеть до и после вызова Add () совершенно идентично, как видно на рис. 1.9.

Рисунок 1. 9 – По умолчанию параметры передаются по значению

Модификатор out

Методы, которым при определении (с помощью ключевого слова out) указывается принимать выходные параметры, должны при выходе обязательно присваивать им соответствующее значение (в случае не соблюдения этого условия, компилятор будет выдавать ошибку).

Для иллюстрации ниже приведена альтернативная версия метода Add(), в которой предусматривается возврат суммы двух целых чисел за счет использования С#-модификатора out (важно обратить внимание на то, что физическим возвращаемым значением этого метода теперь является void).

// Выходные параметры должны предоставляться вызываемым методом.

public static void Add(int x, int y, out int ans)

{

ans = x + y;

}

Перепишем метод Main() следующим образом :

static void Main(string[] args)

{

Console.WriteLine ("***** Fun with Methods *****");

// Нет необходимости присваивать первоначальное значение

// локальным переменным, используемым в качестве выходных параметров.

int ans;

Add(90, 90, out ans);

Console.WriteLine("90 + 90 = {0}", ans);

Console.ReadLine();

}

Результат выполнения кода представлен на рисунке 1. 10.

Рисунок 1. 10 – Использования модификатор out в параметрах методов

Пример был приведен здесь исключительно в демонстративных целях; на самом деле нет совершенно никаких оснований возвращать значение операции суммирования в выходном параметре. Но сам модификатор out в С# действительно является очень полезным: он позволяет вызывающему коду получать в результате одного вызова метода сразу несколько возвращаемых значений. Например:

// Возвращение множества выходных параметров.

public static void FillTheseValues(out int a, out string b, out bool c)

{

a = 9;

b = "Enjoy your string.";

b = "Наслаждайтесь своей строкой.";

c = true;

}

Метод Main() перепишем следующим образом:

static void Main(string[] args)

{

Console.WriteLine ("***** Fun with Methods *****");

int i; string str; bool b;

FillTheseValues(out i, out str, out b);

Console.WriteLine("Int is: {0}", i); // целое число

Console.WriteLine("String is: {0}", str); // строка

Console.WriteLine("Boolean is: {0}", b); // булевское значение

Console.ReadLine();

}

Результат выполнения кода представлен на рисунке 1.11.

Рисунок 1. 11 – Использования модификатор out в параметрах методов для указания нескольких возвращаемых значений

Модификатор ref

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

  • Выходные параметры не нужно инициализировать перед передачей методу. Почему? Потому что метод сам должен присваивать значения выходным параметрам перед выходом.

  • Ссылочные параметры нужно обязательно инициализировать перед передачей методу. Почему? Потому что они подразумевают передачу ссылки на уже существующую переменную. Если не присвоить ей первоначальное значение, это равнозначно выполнению операции над не присвоенной локальной переменной.

Рассмотрим применение ключевого слова ref на примере метода, меняющего две строки местами:

// Ссылочные параметры.

public static void SwapStrings(ref string s1, ref string s2)

{

string tempStr = s1;

s1 = s2;

s2 = tempStr;

}

Этот метод может вызваться следующим образом:

static void Main(string[] args)

{

Console.WriteLine ("***** Fun with Methods *****");

string s1 = "Flip";

string s2 = "Flop";

Console.WriteLine("Before: {0}, {1} ", s1, s2); //до

SwapStrings(ref s1, ref s2);

Console.WriteLine("After: {0}, {1} ", s1, s2); //после

Console.ReadLine();

}

Результат выполнения кода представлен на рисунке 1.12.

Рисунок 1. 12 – Использование модификатора ref при передаче параметров

Модификаторы доступа С#

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

В табл. 1.4 описаны роли и применение модификаторов доступа.

Таблица 1. 4 – Модификаторы доступа C#

Модификатор доступа C#

Может быть применен к…

Назначение

public

типам или членам типов

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

private

членам типов или вложенным типам

Приватные (private) элементы могут быть доступны только в классе (или структуре), в котором они определены

protected

членам типов или вложенным типам

Защищенные (protected) элементы не доступны напрямую через переменную-объект; однако они доступны в определяющем типе, а также всех производных классах

internal

членам типов или

Внутренние (internal) элементы доступны только в пределах текущей сборки. Поэтому если вы определили набор внутренних типов внутри библиотеки классов .NET, другие сборки не могут их использовать.

protected

internal

вложенным типам

Когда ключевые слова protected и internal комбинируются в объявлении элемента, такой элемент доступен внутри определяющей его сборки, определяющего класса и всех его наследников

Модификаторы доступа по умолчанию

По умолчанию члены типов являются неявно приватными (private) и неявно внутренними (internal). Таким образом, следующее определение класса автоматически установлено internal, в то время как конструктор по умолчанию этого типа автоматически является private:

// Внутренний класс с приватным конструктором по умолчанию.

class Radio

{

Radio()

{ }

}

Таким образом, чтобы позволить другим типам вызывать члены объекта, их потребуется пометить как общедоступные (public). К тому же, если необходимо открыть Radio внешним сборкам (опять же, это удобно при построении библиотек кода .NET;), следует добавить к нему модификатор public.

// Внутренний класс с приватным конструктором по умолчанию.

public class Radio

{

Radio()

{ }

}

Другие модификаторы (virtual, abstract, override, sealed, extern)

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

Таблица 1. 5 - Модификатор virtual, abstract, override, sealed, extern и т.д.

Модификатор

К чему относится

Описание

new

Функциям-членам

Член скрывает унаследованный член с той же сигнатурой

static

Ко всем членам

Член не связан с конкретным экземпляром класса

virtual

Только к классам и функциям-членам

Член может быть переопределен в классах-наследниках

abstract

Только к функциям-членам

Виртуальный член, определяющий сигнатуру, но не представляющий реализации

override

Только к функциям-членам

Член переопределяет унаследованный виртуальный или абстрактный член базового класса

sealed

К классам, методам и свойствам

Для классов означает, что класс не может быть наследован. Для свойств и методов – член переопределяет унаследованный виртуальный член, но не может быть переопределен ни одним членом производных классов. Должен применяться в сочетании с override.

extern

Только к статическим методам

Член реализован внешне на другом языке

Из всех приведенных выше модификаторов internal и protected internal являются новыми в языке С# и среде .NET. Модификатор internal ведет себя в основном так же, как public, но ограничивает доступ пределами текущей сборки — т.е. пределами кода, скомпилированного в одно и то же время, в одну и ту же программу. Вы можете использовать internal для обеспечения того, чтобы все другие классы, которые вы пишете, имели доступ к определенному члену, в то же время скрывая его от кода, написанного в других организациях. Модификатор protected internal комбинирует protected и internal, но комбинирует в смысле логического "ИЛИ", а не "И". Член protected internal может быть видимым любому коду в той же сборке. Он также видим для всех производных классов, даже из других сборок.

Класс System.Console

Троелсен – ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2008 И ПЛАТФОРМА .NET 3.5 4-еиздание. Стр. 119.

Форматирование консольного вывода.

Троелсен – ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2008 И ПЛАТФОРМА .NET 3.5 4-еиздание. Стр. 121.

Форматирование числовых данных.

Троелсен – ЯЗЫК ПРОГРАММИРОВАНИЯ С# 2008 И ПЛАТФОРМА .NET 3.5 4-еиздание. Стр. 122.

Пример программы, вычисляющей функцию: (a+b) / 2

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace Lab1

{

class Program

{

static void Main(string[] args)

{

//выводим сообщение

Console.WriteLine("Укажите число a");

//считываем введенные пользователем данные и преобразуем в double

double a = double.Parse(Console.ReadLine());

Console.WriteLine("Укажите число b");

double b = double.Parse(Console.ReadLine());

Console.WriteLine("(a+b)/2 = "+Sum(a,b));

Console.ReadLine();

}

//видимая для других классов функция,

//static - чтобы можно было вызывать без создания экземпляров класса Program

//возвращаемый результат - double

public static double Sum(double a, double b)

{

//вычисляем значение и сразу возвращаем

return (a + b) / 2;

}

}

}

Результат работы программы представлен на рисунке 1. 13.

Рисунок 1. 13 – Пример программы, вычисляющей функцию (a+b)/2

Задание

Написать C# программу, реализующую функцию согласно варианту задания. Исходные данные вводятся с клавиатуры.

Варианты:

1. Реализовать функцию вычисления суммы двух целых чисел

2. Реализовать функцию вычисления разности двух целых чисел

3. Реализовать функцию вычисления произведения двух целых чисел

4. Реализовать функцию вычисления частного двух целых чисел

5. Реализовать функцию вычисления суммы двух вещественных чисел

6. Реализовать функцию вычисления разности двух вещественных

чисел

7. Реализовать функцию вычисления произведения двух вещественных

чисел

8. Реализовать функцию вычисления частного двух вещественных

чисел

9. Реализовать функцию возведения целого числа в квадрат

10. Реализовать функцию возведения в квадрат суммы двух целых

Чисел

11. Реализовать функцию возведения в квадрат разности двух целых

чисел

12. Реализовать функцию возведения в квадрат произведения двух

целых чисел

13. Реализовать функцию возведения в квадрат частного двух целых

чисел

14. Реализовать функцию возведения в квадрат суммы двух

вещественных чисел

15. Реализовать функцию возведения в квадрат разности двух

вещественных чисел

16. Реализовать функцию возведения в квадрат произведения двух

вещественных чисел

17. Реализовать функцию возведения в квадрат частного двух

вещественных чисел

18. Реализовать функцию возведения в куб целого числа

28. Реализовать функцию возведения в куб суммы двух целых чисел

20. Реализовать функцию возведения в куб разности двух целых чисел

21. Реализовать функцию возведения в куб произведения двух целых

чисел

22. Реализовать функцию возведения в куб частного двух целых чисел

23. Реализовать функцию возведения в куб суммы двух вещественных

чисел

24. Реализовать функцию возведения в куб разности двух

вещественных чисел

25. Реализовать функцию возведения в куб произведения двух

вещественных чисел

26. Реализовать функцию возведения в куб частного двух

вещественных чисел

Лабораторная работа 2: Объекты и классы (наследование, конструкторы, деструкторы)

Теоретическая часть

Существует два типа наследования:

  1. Наследование реализации: наследник получает от базового класса все поля-члены и все функции-члены и расширяет (изменяет) его функциональность.

  2. Наследование интерфейса: наследник обязуется выполнить контракт - реализовать определенную в наследуемом интерфейсе функциональность.

Множественное наследование:

  1. Классы поддерживают одиночное наследование реализации и множественное наследование интерфейсов.

  2. Структуры не поддерживают наследование реализации и поддерживают множественное наследование интерфейсов.

Синтаксис наследования:

public class DerivClass: BaseCl,IInt1,IInt2 {

...

}

public struct DerivStruct: IInt1,IInt2 {

...

}

Методы и свойства, объявленные в базовом классе виртуальными (virtual), могут быть переопределены их новыми версиями (override) в классах-наследниках. По умолчанию все методы и свойства класса не виртуальны.

Если методы с одинаковой сигнатурой объявлены и в базовом, и в унаследованном классе без описателей virtual и override, то метод класса-наследника скрывает метод базового класса.

Сокрытие нежелательно, но если оно необходимо, то для новой версии метода (т.е. для метода класса-наследника) используют описатель new.

Пример:

public class B{

public int Hf();

}

public class D: B{

public new int Hf(){

...

}

}

Функция базового класса, не имеющая реализации, называется абстрактной (abstract) функцией, а класс, содержащий абстрактную функцию – абстрактным классом.

Абстрактная функция автоматически является виртуальной и обязательно должна быть переопределена в классе-наследнике.

От запечатанного класса нельзя наследовать (он последний в иерархии), а запечатанную функцию невозможно переопределить, например:

public sealed class B{

...

}

public class D: B{

...

} //ошибка компиляции

При создании экземпляра любого класса A последовательно «работают» конструкторы всех классов-предков (вышележащих по иерархии), начиная с класса System.Object и завершая конструктором класса A. Каждый конструктор инициализирует поля собственного класса.

Интерфейсы не предназначены для создания своих экземпляров.

Интерфейс – это контракт, который «добровольно» принимает на себя какой-либо класс и который обязывает его предоставить полную реализацию функциональности, декларированной в интерфейсе.

Объявление интерфейса синтаксически похоже на объявление абстрактного класса. Отличия:

  1. Может содержать только объявления (сигнатуры) методов, свойств, индексов и событий.

  2. Не может содержать конструкторов и перегруженных операций.

  3. Для членов интерфейса нельзя указывать модификаторы (они являются public по умолчанию).

Интерфейсы могут быть унаследованы от других интерфейсов.

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

Пример программы

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace lab2

{

class Program

{

static void Main(string[] args)

{

Rectangle MyRect = new Rectangle(10, 20);

Console.WriteLine(MyRect.ToString());

Console.WriteLine("Площадь: "+ MyRect.Area());

Triangle MyTriangle = new Triangle(10, 20,30,40);

Console.WriteLine(MyTriangle.ToString());

Console.WriteLine("Площадь: " + MyTriangle.Area());

Console.ReadLine();

}

}

public class Shape

{

private int LocX;

public int X

{

get { return LocX; }

set { LocX = value; }

}

private int LocY;

public int Y

{

get { return LocY; }

set { LocY = value; }

}

public virtual int Area()

{

return 0;

}

public override string ToString()

{

return "Базовый класс Shape!";

}

}

public class Rectangle : Shape

{

private int w;

public int Width

{

get { return w; }

set { w = value; }

}

public Rectangle(int width, int height)

{

this.w = width;

this.h = height;

}

private int h;

public int Height

{

get { return h; }

set { h = value; }

}

public override int Area()

{

return Height*Width;

}

public override string ToString()

{

return "Класс Rectangle, унаследованный от Shape";

}

}

public class Triangle : Shape

{

public Triangle(int a, int b, int c, int h)

{

this.a = a;

this.b = b;

this.c = c;

this.h = h;

}

private int a;

public int A

{

get { return a; }

set { a = value; }

}

private int b;

public int B

{

get { return b; }

set { b = value; }

}

private int c;

public int C

{

get { return c; }

set { c = value; }

}

private int h;

public int H

{

get { return h; }

set { h = value; }

}

public override int Area()

{

return (a*b*c)/h;

}

public override string ToString()

{

return "Класс Triangle, унаследованный от Shape";

}

}

}

Результат выполнения кода представлен на рисунке 2.1.

Рисунок 2. 1 – Пример программы, иллюстрирующей правила наследования

Задание

1) Разработать методы (не менее 3-х) и свойства (не менее 3-х) для каждого из определяемых классов.

2) Один из методов в классе родителе объявить как virtual и переопределить его в одном из классов - потомков.

3) Реализовать программу на C# в соответствии с вариантом исполнения.

Построить иерархию классов в соответствии с вариантом задания:

1) Студент, преподаватель, персона, заведующий кафедрой

2) Служащий, персона, рабочий, инженер

3) Рабочий, кадры, инженер, администрация

4) Деталь, механизм, изделие, узел

5) Организация, страховая компания, нефтегазовая компания, завод

6) Журнал, книга, печатное издание, учебник

7) Тест, экзамен, выпускной экзамен, испытание

8) Место, область, город, мегаполис

9) Игрушка, продукт, товар, молочный продукт

10) Квитанция, накладная, документ, счет

11) Автомобиль, поезд, транспортное средство, экспресс

12) Двигатель, двигатель внутреннего сгорания, дизель, реактивный

двигатель

13) Республика, монархия, королевство, государство

14) Млекопитающее, парнокопытное, птица, животное

15) Корабль, пароход, парусник, корвет

Лабораторная работа 3: «Массивы»

Теоретическая часть

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

Синтаксис объявления массива:

int[] v; // объявление ссылки

v = new int[4]; // размещение блока данных

int[] v = new int[4]; // объявление ссылки

// с размещением

int[] v = new int[4] {4, 7, 14, 2}; // инициализация массива

int[] v = new int[] {4, 7, 14, 2}; // инициализация без указания размера

int[] v = {4, 7, 14, 2}; // короткая форма

Обращение к элементам массива производится с помощью индексатора. Индекс массива – целое число. Первый элемент массива имеет индекс 0. Для всех массивов определено свойство Length.

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

Многомерный массив регулярной структуры (матрица, трехмерная матрица и пр.) индексируется двумя или более целыми индексами:

int[,] m; // объявление ссылки для матрицы

m = new int[2,3]; // размещение блока данных

int[,] m = new int[2,3]; // объявление ссылки

// с размещением

int[,] m = { // короткая форма

{4, 7, 14},

{2, 5, 10}

};

Зубчатые массивы более гибкие в отношении размерности: каждая строка может иметь свой размер.

Объявление зубчатого массива и работа с ним:

int[][] q = new int[3][];

q[0] = new int[] {1, 2};

q[1] = new int[] {5, 3, 4, 7, 9, 2};

q[2] = new int[] {3, 8, 5};

for (int i=0; i < q.Length; i++)

for (int j=0; j < q[i].Length; j++)

Console.WriteLine(“q[{0},{1}]={2}”, i, j, q[i][j]);

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

public abstract class Array : ICloneable, IList, ICollection, IEnumerable

Поэтому методы и свойства класса Array можно использовать с любым массивом.

Использование свойств и методов System.Array при работе с массивами позволяет значительно расширить базовые возможности языка.

Например, для создания массивов можно использовать статический метод Array.CreateInstance(), имеющий 6 перегрузок. Например, можно записать так:

int [] n = {2, 3, 4};

int [] r = {1, 4, 9};

Array q = Array.CreateInstance(typeof(int),n,r);

int[,,] x = (int[,,])q; // приведение

x[2, 6, 12] = 999;

Console.WriteLine(q.Rank); // 3

Console.WriteLine(q.Length); // 24

Console.WriteLine(x[2, 6, 12]); // 999

Для копирования массивов предусмотрена реализация массивами интерфейса ICloneable, содержащего методы Clone() и CopyTo() для неглубокого (shallow) копирования массивов.

В классе Array реализована пузырьковая сортировка элементов массива с помощью статического метода Array.Sort(). Для этого элементы массива должны поддерживать интерфейс IComparable.

class Pair : IComparable {

public string a, b;

public Pair(string a, string b) {this.a=a; this.b=b; }

public override string ToString()

{ return "( " + a + " , " + b + " )"; }

public int CompareTo(object x) {

Pair y = x as Pair;

int r = this.a.CompareTo(y.a);

if (r != 0) return r;

return this.b.CompareTo(y.b);

}

}

...

Pair[] z = { new Pair("Petrova", "M."), new Pair("Petrov", "A."), new Pair("Ivanov", "A."), new Pair("Petrova","T."),};

Array.Sort(z);

foreach (Pair v in z) Console.WriteLine(v);

В версии C# 2005 введен альтернативный способ создания типов, работающих с циклом foreach посредством итераторов (перечислителей) – членов класса, определяющих, как должны возвращаться внутренние элементы контейнера при обработке оператора foreach. Хотя метод итератора по-прежнему должен называться GetEnumerator() и возвращать значение типа IEnumerator, пользовательскому классу не нужно реализовывать никакие интерфейсы.

Пример программы

//Содержит информацию о имеющихся машинах

public class Garaj

{

public string[] Avtomobili = { "Lambordgini", "Aston Martin", "Ferrari", "Ford", "Lexus" };

public int[] Vajnost = { 100,50 , 90, 40,60 };

public string Tip;//"машины","важность"

public Garaj(string tipperebora)

{

Tip = tipperebora;

}

public IEnumerator GetEnumerator()

{

// если хотим перечислить машины, имеющиеся в гараже

if (this.Tip.ToLower() == "машины")

{

for (int i = 0; i <= Avtomobili.Length-1; i++)

{ yield return Avtomobili[i]; }

}

else

// если хотим перечислить важность машин

if (this.Tip.ToLower() == "важность")

{

for (int i = 0; i <= Vajnost.Length-1; i++)

{ yield return Vajnost[i]; }

}

else

{

yield return "Не верно указан тип перебора элементов!!!";

yield break;

}

}

}

static void Main()

{

Garaj Garaj1 = new Garaj("машины");

Console.WriteLine("Перебор по машинам: ");

foreach (string x1 in Garaj1)

{ Console.WriteLine(x1); }

Console.Read();

Garaj1.Tip = "важность";

Console.WriteLine("Перебор по важности машин: ");

foreach (int x1 in Garaj1)

{ Console.WriteLine(x1); }

Console.Read();

}

Результат работы программ представлен на рисунке 3.1.

Рисунок 3.1 – Пример класса коллекции

Задание

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

Лабораторная работа 4: «Перегрузка операций»

Теоретическая часть

Большинство операций C# имеют тот же смысл, что и в C++.

Новые операции:

  1. checked, unchecked – контроль переполнения.

  2. is, as, typeof – информация о типе.

  3. ?? – операция поглощения null.

  4. + – конкатенация строк.

Операции checked, unchecked применяются для работы с проблемным кодом, в котором может произойти переполнение, например:

byte b = 255;

b++;

Console.WriteLine(b.ToString());

Данный код приведет к неправильной работе программы, т. к. произойдет переполнение переменной b и она будет иметь уже не точное значение. Чтобы отследить данную ситуацию, применим следующую запись:

byte b = 255;

checked { b++; } // OverflowException

Console.WriteLine(b.ToString());

В тоже время можно отменить контроль переполнения:

byte b = 255;

unchecked { b++; } // b=0 ???

Console.WriteLine(b.ToString());

Оператор is проверяет совместимость объекта с указанным типом, as выполняет явное безопасное преобразование ссылочного типа, например:

int i = 10;

if (i is object){

Console.WriteLine(“i является объектом”);

object a = “abcdef”;

object b = 5;

string s1 = a as string; // s1=“abcdef”

string s2 = b as string; // s2=null

Функция typeof возвращает объект класса System.Type, содержащий характеристики указанного типа, например, typeof(string) вернет объект класса Type с характеристикой типа System.String.

В C# можно определять типы, допускающие null значения, например:

byte b = null; // err!

byte? b1 = null;

int? x = 10;

int? arr = new int?[];

string? s1 = “abc”; // err!

int? a1 = null;

int? a2 = a1 + 5; // err!

При работе с типами, допускающими null значения, применяется операция поглощения нулевого значения (??):

int? a = null;

int b;

b = a ?? 10; // b=10

a = 3;

b = a ?? 10; // b=3

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

Если первый операнд не-null, то его значение является результатом выражения, если же он null, то результатом выражения является значение второго операнда.

Перегрузка операций в равной мере применима и к классам, и к структурам. Перегрузка операций в C# в основном выполняется так же, как и в C++.

Важное отличие: в C# не существует внешних функций, поэтому все перегруженные операции являются членами классов или структур. Кроме того, они должны иметь обязательные модификаторы public и static. Следовательно, они не имеют доступа к членам класса уровня экземпляра (нестатическим). Так же в C# не допускается перегрузка операции = (присваивание).

Особенности перегрузки операций сравнения:

  1. Должны перегружаться попарно.

  2. Должны возвращать результат типа bool.

  3. При перегрузке пары (==, !=) необходимо также переопределить виртуальные методы Equals() и GetHashCode() класса System.Object (т.к. Equals() должен реализовывать ту же логику сравнения, что и операция ==).

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

Перегрузка операций

Пусть имеется структура:

struct Vector

{

public double x, y, z;

public Vector(double x, double y, double z)

{

this.x = x;

this.y = y;

this.z = z;

}

public Vector(Vector rhs)

{

x = rhs.x;

y = rhs.y;

z = rhs.z;

}

public override string ToString()

{

return " ( " + x + " , " + y + " , " + z + " ) ";

}

}

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

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

Самая интересная часть структуры Vector – перегрузка операции сложения:

public static Vector operator + (Vector lhs, Vector rhs)

{

Vector result = new Vector(lhs);

result.x += rhs.x;

result.y += rhs.y;

result.z += rhs.z;

return result;

}

Перегрузка операции объявляется почти так же, как метод, за исключением того, что ключевое слово operator сообщает компилятору, что это на самом деле определение перегрузки операции. За ключевым словом operator следует символ операции, в данном случае— знак сложения (+). Возвращаемый тип — это тип, который получим, применив эту операцию. Сложение двух векторов дает в результате вектор, поэтому здесь возвращаемым типом является Vector. В данном конкретном случае перегрузки операции сложения возвращаемый тип совпадает с включающим его классом, но это вовсе не обязательно, как будет показано позже. Два параметра — это те операнды, с которыми операция будет работать. Для бинарных операций (которые принимают два параметра) вроде сложения и вычитания первый параметр — это значение, которое записывается слева от знака операции, а второй — значение, записываемое справа.

C# требует чтобы перегруженные операции объявлялись как public и static; это значит, что они ассоциированы с классом или структурой, а не с их экземплярами. По этой причине тело перегруженной операции не имеет доступа к нестатическим членам, а также к идентификатору this. И это нормально, потому что параметры представляют все входные данные, необходимые операции для решения своей задачи.

Запишем пример кода с использованием операции сложения (рисунок 4. 1):

static void Main(string[] args)

{

Vector vectl, vect2, vect3;

vectl = new Vector (3.0, 3.0, 1.0);

vect2 = new Vector (2.0, -4.0, -4.0);

//здесь будет запущена перегруженная операция сложения

vect3 = vectl + vect2;

Console.WriteLine("vectl = " + vectl.ToString ());

Console.WriteLine("vect2 = " + vect2.ToString());

Console.WriteLine("vect3 = " + vect3.ToString());

Console.ReadLine();

}

Рисунок 4. 1 – Перегрузка операций сложения

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

public static Vector operator *(double lhs, Vector rhs)

{

return new Vector(lhs * rhs.x, lhs * rhs.y, lhs * rhs.z);

}

public static Vector operator *(Vector lhs, double rhs)

{

return new Vector(rhs * lhs.x, rhs * lhs.y, rhs * lhs.z);

}

Перегрузка операций сравнения

В С# имеется шесть операций сравнения и их можно разделить на три пары:

1. == и !=

2. > и <

3. >= и <=

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

Другой пример касается базового класса .NET System. DateTime. Можно вычесть один экземпляр DateTime из другого, но результатом будет не DateTime, а System.TimeSpan. В отличие от этого, для операций сравнения не имеет смысла возвращать что-то, отличное от bool.

Если вы перегружаете == и !=, то должны также переопределить методы Equals() и GetHashCode() из System. Object, иначе получите предупреждение компилятора.

Причина в том, что метод Equals() должен реализовывать ту же логику сравнения, что и операция ==.

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

public static bool operator == (Vector lhs, Vector rhs)

{

if (lhs.x == rhs.x && lhs.y== rhs.y && lhs.z== rhs.z)

{ return true; }

else

{ return false; }

}

Реализацию операции «!=» можно выполнить следующим простым способом:

public static bool operator !=(Vector lhs, Vector rhs)

{

return !(lhs == rhs);

}

Проверим операции сравнения (результат представлен на рисунке 4. 2).

static void Main(string[] args)

{

Vector vect1, vect2, vect3;

vect1 = new Vector(3.0, 3.0, -10);

vect2 = new Vector (3.0, 3.0, -10);

vect3 = new Vector (2.0, 3.0, 6.0);

Console.WriteLine("vect1= " + vect1.ToString());

Console.WriteLine("vect2= " + vect2.ToString());

Console.WriteLine("vect3= " + vect3.ToString());

Console.WriteLine();

Console.WriteLine("vect1==vect2 возвращает "+(vect1==vect2));

Console.WriteLine ("vect1==vect3 возвращает "+(vect1==vect3));

Console.WriteLine("vect2==vect3 возвращает " + (vect2 == vect3));

Console.WriteLine();

Console.WriteLine("vect1!=vect2 возвращает " + (vect1 != vect2));

Console.WriteLine("vect1!=vect3 возвращает " + (vect1 != vect3));

Console.WriteLine("vect2!=vect3 возвращает " + (vect2 != vect3));

Console.ReadLine();

}

Рисунок 4. 2 – Перегрузка операций сравнения

Какие операции можно перегружать ?

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

Таблица 4. 1 – Операции, которые можно перегружать

К атегория

Операции

Ограничения

Арифметические бинарные

+, *, /, –, %

Нет

Арифметические унарные

+, –, ++, – –

Нет

Битовые бинарные

&, |, ^, <<, >>

Нет

Битовые унарные

!, ~, true, false

Операции true и false должны перегружаться в паре

Сравнение

= =, !=, >=, <=, >, <

Операции сравнения должны перегружаться попарно

Присваивание

+=, -=, *=, /=, >>=, <<=, %=, &=, |=, ^=

Эти операции нельзя перегрузить; их перегрузка осуществляется неявно при определении индивидуальных операций +, –, % и т.д.

Индексация

[ ]

Операцию индекса нельзя перегрузить явно. Тип индексирующего члена позволяет поддержать операцию индексации в пользовательских классах и структурах

Приведение

( )

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

Задание

Реализовать класс геометрической фигуры в соответствии с вариантом и перегрузить для данного класса операции >, < (сравнение введется по площади фигуры или объему, в зависимости от варианта), + (складываются члены класса).

  1. Прямоугольник (члены класса:x, y; площадь: x*y)

  2. Квадрат (члены класса: x; площадь: x*x)

  3. Ромб (члены класса: AC, BD - диагонали; площадь: (AC*BD)/2 )

  4. Дельтоид (члены класса: a, b – длины не равных сторон, - угол между ними; площадь: a*b*sin )

  5. Сегмент (члены класса: R, ; площадь: )

  6. Треугольник (члены класса: a,b,c, H ; площадь: (abc)/4H )

  7. Куб (члены класса: a ; объем: )

  8. Прямоугольная призма (члены класса: a, b, c ; объем: a*b*c)

  9. Цилиндр (члены класса: r, h ; объем: )

  10. Конус (члены класса: r, h ; объем: )

Лабораторная работа 5: «Делегаты и события» (Windows)

Теоретическая часть

В программировании для Windows широко применяется техника функций обратного вызова (callback functions). Функции обратного вызова – это указатели на функции, через которые могут быть вызваны функции. В .NET концепция указателей на функции реализована в форме делегатов (delegates), которые безопасны в отношении типов. Основное назначение делегатов – реализация механизма событий.

Рассмотрим пример объявления и использования делегата:

public delegate void D(int k);

class Program {

static void f(int n) {

Console.WriteLine("\nf(): n={0}", n);

}

static void Main(string[] args) {

D d = new D(f); // создание и инициализация делегата

d(5); // вызов функции f через делегат

Console.ReadKey();

}

}

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

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

Синтаксис языка C# 2.0 допускает инициализировать экземпляр делегата без явного вызова конструктора, непосредственно именем метода.

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

public delegate void D(int k); ... D[] md = { f, f1, A.fsD, a.fD };

int v = 4;

foreach (D x in md) x(v++);

Результат:

f(): n=4

f1(): n=5

fsD(): x=6

fD(): x=7

Экземпляр делегата можно связать с еще не существующей функцией, которая создается «на лету». Такая функция называется анонимным методом:

public delegate void D(int k); … D d = delegate(int n) {

Console.WriteLine("\nAnonym(): n={0}", n);

};

d(5);//Anonym():n=5 d = delegate(int n) {

Console.WriteLine("\nAnonym1(): n={0}", n);

};

d(9); // Anonym1(): n=9

В рассмотренных примерах экземпляр делегата являлся в одно и то же время «оболочкой» только для одного, конкретного метода. При необходимости можно было «переприкрепить» к нему другой метод. В языке C# существует возможность рассматривать экземпляр делегата как контейнер, в который помещены сразу несколько методов. Такие делегаты называются групповыми (multicast delegates). При вызове группового делегата последовательно вызываются все находящиеся в нем методы.

Ограничение: делегат может быть групповым, если он имеет тип возвращаемого значения void. К групповым делегатам применимы операции +, -, += и -=. Операндами могут являться как групповые делегаты, так и имена методов. Групповые делегаты рассматриваются как мультимножества, а операции + и – как операции «объединение» и «пересечение» для мультимножеств.

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

- отправитель генерирует событие, ничего не зная о получателях этого события; он определяет делегат, который будет использован получателем;

- получатель определяет обработчик события и «прикрепляет» его к событию (выполняет привязку события). Отправителем может быть объект, сборка или исполняющая система .NET (для системных событий).