Создание структуры в C#. Пример.



Структура - это набор зависимых друг от друга переменных. Зависимость здесь исключительно логическая и определяется условиями задачи. Чтобы стало понятно, рассмотрим простой пример. Допустим, мы пишем программу, печатающую справки для студентов. Все справки имеют один и тот же вид и текст, кроме следующих полей: ФИО, форма обучения, курс, факультет. Это зависимые данные и их можно представить в виде структуры, например так:

struct STUDENT

{

   public string fio;

   public string FormOfEducation;

   public int course;

   public string faculty;

}

Наша структура называется STUDENT и имеет четыре поля. После того как структура объявлена, мы можем с ней работать. Вот небольшой пример:

static void Main(string[] args)

{

STUDENT stud;

stud.fio = "Нгуен Павел Зунгович";

stud.FormOfEducation = "очного";

stud.course = 3;

       stud.faculty = "электроэнергетического";

       Console.WriteLine("СПРАВКА подтверждает, что "+stud.fio

       +" является студентом "+stud.FormOfEducation+" отделения ВоГТУ "

       +stud.course+" курса "+stud.faculty+" факультета");

       Console.Read();

   }

Передавать такие данные удобно, т.к. они являются одним объектом. В частности можно передавать структуры в качестве параметров функциям. Усовершенствуем нашу программу, написав отдельную функцию для печатания справок:

   static void print(STUDENT stud)

   {

     Console.WriteLine("СПРАВКА подтверждает, что "+stud.fio

       +" является студентом "+stud.FormOfEducation+" отделения ВоГТУ "

       +stud.course+" курса "+stud.faculty+" факультета");

   }

В качестве параметра мы передаем структуру типа STUDENT. Уверен, что вы скажете: "Зачем нужны структуры, когда есть классы?". Дело в том, что структуры являются более простым языковым средством. В частности имеют ограничения, которые обезопасят программиста от ошибок.

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

struct STUDENT

{

  public string fio;

   public string FormOfEducation;

   public int course;

   public string faculty;

       

   STUDENT (string name)

   {

       this.fio = name;

       this.FormOfEducation = "Неопределено";

       this.course = 0;

       this.faculty = "Неопределено";

 

Создание класса в C#. Пример.

C#: классы

Система классов играет важную роль в современных языках программирования. Как же они реализованы в новом языке C#, созданном корпорацией Microsoft, и зачем нужно изучать С#?Ответы на эти вопросы зависят от того, как вы собираетесь работать дальше. Если вы хотите создавать приложения для платформы .NET, то вам, скорее всего, не удастся избежать изучения C#. Конечно, можно использовать и Си++, и Visual Basic или любой язык программирования, тем более что независимыми разработчиками создаются трансляторы с APL, Кобола, Eiffel, Haskell, Оберона, Smalltalk, Perl, Python, Паскаля и др. Однако для компилятора, способного генерировать приложения среды .NET CLR (Common Language Runtime), только C# является «родным» языком. Он полностью соответствует идеологии .NET и позволяет наиболее продуктивно работать в среде CLR. В свое время для использования виртуальной машины Java было создано множество так называемых «переходников» (bridges) c различных языков программирования, в частности PERCobol, JPython, Eiffel-to-JavaVM System, Tcl/Java и т.д. Подобные разработки так и не получили должного распространения. Практика показала, что значительно проще изучить новый язык, чем вводить дополнительные расширения в менее подходящую для данных целей систему программирования. И не надо быть провидцем, чтобы утверждать, что бо,льшая часть программистов, создающих приложения для платформы .NET, отдаст предпочтение именно языку C#.

C# является языком объектно-ориентированного программирования, поэтому классы играют в нем основополагающую роль. Более того, все типы данных C#, как встроенные, так и определенные пользователем, порождены от базового класса object. Иными словами, в отличие от Java, где примитивные типы данных отделены от объектных типов, все типы данных в C# являются классами и могут быть разделены на две группы:

ссылочные (reference types);

обычные (value types).

Внешне ссылочные и обычные типы очень похожи, так как аналогично Cи++ в них можно объявлять конструкторы, поля, методы, операторы и т.д. Однако, в отличие от Cи++, обычные типы в C# не позволяют определять классы и не поддерживают наследования. Они описываются с помощью ключевого слова struct и в основном используются для создания небольших объектов. Ссылочные же типы описываются с помощью ключевого слова class и являются указателями, а экземпляры таких типов ссылаются на объект, находящийся в куче (heap). Продемонстрируем сказанное на примере:

using System;

 

class CValue

{

 

public int val;

public CValue(int x) {val = x;}

}

 

class Example_1

{

public static void Main()

{

CValue p1 = new CValue(1);

CValue p2 = p1;

Console.WriteLine(”p1 = {0}, p2 = {1}”,

p1.val, p2.val);

p2.val = 2;

Console.WriteLine(”p1 = {0}, p2 = {1}”,

p1.val, p2.val);

}

}

Откомпилировав и выполнив программу, получим следующий результат:

p1 = 1, p2 = 1

p1 = 2, p2 = 2

Как нетрудно видеть, p2 является всего лишь ссылкой на p1. Тем самым становится очевидно, что при изменении поля val экземпляра класса p2 в действительности изменяется значение соответствующего поля p1. Подобный подход не очень удобен при работе с примитивными типами данных, которые должны содержать само значение, а не ссылку на него (Complex, Point, Rect, FileInfo и т.д.). Для описания таких объектов и предназначены типы значений:

using System;

struct SValue

{

public int val;

public SValue(int x) {val = x;}

}

class Example_2

{

public static void Main()

{

SValue p1 = new SValue(1);

SValue p2 = p1;

Console.WriteLine(”p1 = {0}, p2 = {1}”,

p1.val, p2.val);

p2.val = 2;

Console.WriteLine(”p1 = {0}, p2 = {1}”,

p1.val, p2.val);

}

}

Вот что получится после запуска вышеприведенной программы:

p1 = 1, p2 = 1

p1 = 1, p2 = 2

Из этого следует, что экземпляр класса p2 является самостоятельным объектом, который содержит собственное поле val, не связанное с p1. Использование обычных типов позволяет избежать дополнительного расходования памяти, поскольку не создаются дополнительные ссылки, как в случае с экземплярами классов. Конечно, экономия невелика, если у вас имеется всего несколько небольших объектов типа Complex или Point. Зато для массива, содержащего несколько тысяч таких элементов, картина может в корне измениться. В таблице приведены основные отличия типов class и struct.

Интерфейсы

Классы в языке C# претерпели довольно серьезные изменения по сравнению с языком программирования Cи++, который и был взят за основу. Первое, что бросается в глаза, это невозможность множественного наследования. Такой подход уже знаком тем, кто пишет на языках Object Pascal и Java, а вот программисты Cи++ могут быть несколько озадачены. Хотя при более близком рассмотрении данное ограничение уже не кажется сколь-нибудь серьезным или непродуманным. Во-первых, множественное наследование, реализованное в Cи++, нередко являлось причиной нетривиальных ошибок. (При том что не так уж часто приходится описывать классы с помощью множественного наследования.) Во-вторых, в C#, как и в диалекте Object Pascal фирмы Borland, разрешено наследование от нескольких интерфейсов.

 

Интерфейсом в C# является тип ссылок, содержащий только абстрактные элементы, не имеющие реализации. Непосредственно реализация этих элементов должна содержаться в классе, производном от данного интерфейса (вы не можете напрямую создавать экземпляры интерфейсов). Интерфейсы C# могут содержать методы, свойства и индексаторы, но в отличие, например, от Java, они не могут содержать константных значений. Рассмотрим простейший пример использования интерфейсов:

using System;

class CShape

{

bool IsShape() {return true;}

}

interface IShape

{

double Square();

}

class CRectangle: CShape, IShape

{

double width;

double height;

public CRectangle(double width, double height)

{

this.width = width;

this.height = height;

}

public double Square()

{

return (width * height);

}

}

class CCircle: CShape, IShape

{

double radius;

public CCircle(double radius)

{

this.radius = radius;

}

public double Square()

{

return (Math.PI * radius * radius);

}

}

class Example_3

{

public static void Main()

{

CRectangle rect = new CRectangle(3, 4);

CCircle circ = new CCircle(5);

Console.WriteLine(rect.Square());

Console.WriteLine(circ.Square());

}

}

 

Оба объекта, rect и circ, являются производными от базового класса CShape и тем самым они наследуют единственный метод IsShape(). Задав имя интерфейса IShape в объявлениях CRectangle и CCircle, мы указываем на то, что в данных классах содержится реализация всех методов интерфейса IShape. Кроме того, члены интерфейсов не имеют модификаторов доступа. Их область видимости определяется непосредственно реализующим классом.

Свойства

Рассматривая классы языка C#, просто нельзя обойти такое «новшество», как свойства (properties). Надо сказать, что здесь чувствуется влияние языков Object Pascal и Java, в которых свойства всегда являлись неотъемлемой частью классов. Что же представляют собой эти самые свойства? С точки зрения пользователя, свойства выглядят практически так же, как и обычные поля класса. Им можно присваивать некоторые значения и получать их обратно. В то же время свойства имеют бо,льшую функциональность, так как чтение и изменение их значений выполняется с помощью специальных методов класса. Такой подход позволяет изолировать пользовательскую модель класса от ее реализации. Поясним данное определение на конкретном примере:

using System;

using System.Runtime.InteropServices;

class Screen

{

[DllImport(”kernel32.dll”)]

static extern bool SetConsoleTextAttribute(

int hConsoleOutput, ushort wAttributes

);

[DllImport(”kernel32.dll”)]

static extern int GetStdHandle(

uint nStdHandle

);

const uint STD_OUTPUT_HANDLE = 0x0FFFFFFF5;

static Screen()

{

output_handle = GetStdHandle(STD_OUTPUT_HANDLE);

m_attributes = 7;

}

public static void PrintString(string str)

{

Console.Write(str);

}

public static ushort Attributes

{

get

{

return m_attributes;

}

set

{

m_attributes = value;

SetConsoleTextAttribute(output_handle, value);

}

}

private static ushort m_attributes;

private static int output_handle;

}

class Example_4

{

public static void Main()

{

for (ushort i = 1; i < 8; i++)

{

Screen.Attributes = i;

Screen.PrintString(”Property Demo\n”);

}

}

}

Программа выводит сообщение «Property Demo», используя различные цвета символов (от темно-синего до белого). Давайте попробуем разобраться в том, как она работает. Итак, сначала мы импортируем важные для нас функции API-интерфейса Windows: SetConsoleTextAttribute и GetStdHandle. К сожалению, стандартный класс среды .NET под названием Console не имеет средств управления цветом вывода текстовой информации. Надо полагать, что корпорация Microsoft в будущем все-таки решит эту проблему. Пока же для этих целей придется воспользоваться службой вызова платформы PInvoke (обратите внимание на использование атрибута DllImport). Далее, в конструкторе класса Screen мы получаем стандартный дескриптор потока вывода консольного приложения и помещаем его значение в закрытую переменную output_handle для дальнейшего использования функцией SetConsoleTextAttribute. Кроме этого, мы присваиваем другой переменной m_attributes начальное значение атрибутов экрана (7 соответствует белому цвету символов на черном фоне). Заметим, что в реальных условиях стоило бы получить текущие атрибуты экрана с помощью функции GetConsoleScreenBufferInfo из набора API-интерфейса Windows. В нашем же случае это несколько усложнило бы пример и отвлекло от основной темы.

В классе Screen мы объявили свойство Attributes, для которого определили функцию чтения (getter) и функцию записи (setter). Функция чтения не выполняет каких-либо специфических действий и просто возвращает значение поля m_attributes (в реальной программе она должна бы возвращать значение атрибутов, полученное с помощью все той же GetConsoleScreenBufferInfo). Функция записи несколько сложнее, так как кроме тривиального обновления значения m_attributes она вызывает функцию SetConsoleTextAttribute, устанавливая заданные атрибуты функций вывода текста. Значение устанавливаемых атрибутов передается специальной переменной value. Обратите внимание на то, что поле m_attributes является закрытым, а стало быть, оно не может быть доступно вне класса Screen. Единственным способом чтения и/или изменения этого метода является свойство Attributes.

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

В языке C# свойства реализованы на уровне синтаксиса. Более того, рекомендуется вообще не использовать открытых полей классов. На первый взгляд, при таком подходе теряется эффективность из-за того, что операции присваивания будут заменены вызовами функций getter и setter. Отнюдь! Среда .NET сгенерирует для них соответствующий inline-код.

Делегаты

Язык программирования C# хотя и допускает, но все же не поощряет использование указателей. В некоторых ситуациях бывает особенно трудно обойтись без указателей на функции. Для этих целей в C# реализованы так называемые делегаты (delegates), которые иногда еще называют безопасными аналогами указателей на функцию. Ниже приведен простейший пример использования метода-делегата:

using System;

delegate void MyDelegate();

class Example_5

{

static void Func()

{

System.Console.WriteLine(«MyDelegate.Func()»);

}

public static void Main()

{

MyDelegate f = new MyDelegate(Func);

f();

}

}Помимо того что делегаты обеспечивают типовую защищенность, а следовательно, и повышают безопасность кода, они отличаются от обычных указателей на функции еще и тем, что являются объектами, производными от базового типа System.Delegate. Таким образом, если мы используем делегат для указания на статический метод класса, то он просто связывается с соответствующим методом данного класса. Если же делегат указывает на нестатический метод класса, он связывается уже с методом экземпляра такого класса. Это позволяет избежать нарушения принципов ООП, поскольку методы не могут быть использованы отдельно от класса (объекта), в котором они определены.

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

using System;

delegate void MyDelegate(string message);

class Example_6

{

public static void Func1(string message)

{

Console.WriteLine(”{0}: MyDelegate.Func1”, message);

}

public static void Func2(string message)

{

Console.WriteLine(”{0}: MyDelegate.Func2”, message);

}

public static void Main()

{

MyDelegate f1, f2, f3;

f1 = new MyDelegate(Func1);

f2 = new MyDelegate(Func2);

f3 = f1 + f2;

f1(”Calling delegate f1”);

f2(”Calling delegate f2”);

f3(”Calling delegate f3”);

}

}

Откомпилировав и выполнив вышеприведенную программу, получим следующий результат:

Calling delegate f1: MyDelegate.Func1

Calling delegate f2: MyDelegate.Func2

Calling delegate f3: MyDelegate.Func1

Calling delegate f3: MyDelegate.Func2

 

Из этого следует, что вызов метода-делегата f3, полученного с помощью операции сложения f1 + f2, приводит к последовательному выполнению обоих этих методов. Подобно применению операции сложения с целью объединения делегатов, можно использовать и операцию вычитания, которая, как нетрудно догадаться, выполняет обратное действие.

Способы передачи параметров

 

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

using System;

class Example_7

{

static int GetRoots(double a, double b, double c,

out double x1, out double x2)

{

double d = b * b - 4 * a * c;

if (d > 0)

{

x1 = -(b + Math.Sqrt(d)) / (2 * a);

x2 = -(b - Math.Sqrt(d)) / (2 * a);

return 2;

} else

if (d == 0)

{

x1 = x2 = -b / (2 * a);

return 1;

} else

{

x1 = x2 = 0;

return 0;

}

}

public static void Main()

{

double x1, x2;

int roots = GetRoots(3, -2, -5, out x1, out x2);

Console.WriteLine(”roots #: {0}”, roots);

if (roots == 2)

Console.WriteLine(”x1 = {0}, x2 = {1}”, x1, x2);

else

if (roots == 1)

Console.WriteLine(”x = {0}”, x1);

}

}

Чтобы функция GetRoots возвращала оба корня уравнения (x1 и x2), мы указали транслятору, что переменные x1 и x2 должны быть переданы по ссылке, применив для этого параметр out. Обратите внимание на то, что нам не обязательно инициализировать переменные x1 и x2 перед вызовом функции GetRoots. Обозначив функцию ключевым словом out, мы добьемся того, что ее аргументы могут использоваться только для возврата какого-то значения, но не для его передачи внутрь функции. Таким образом, подразумевается, что переменная будет инициализирована в теле самой функции. В случае же, если нам по какой-то причине потребуется передать в параметре функции некоторое значение с возможностью его последующего изменения, можно воспользоваться параметром ref. Действие этого параметра очень похоже на действие out, но он позволяет еще и передавать значение параметра телу функции. Второе отличие ключевого слова ref состоит в том, что передаваемый параметр функции должен быть инициализирован предварительно.

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

1) Класс Math:

Кроме переменных и констант, первичным материалом для построения выражений являются функции. Большинство их в проекте будут созданы самим программистом (мы будем с этим фактом разбираться в следующих статьях), но не обойтись и без встроенных функций. Умение работать в среде Visual Studio.Net предполагает знание встроенных возможностей этой среды, знание возможностей каркаса Framework.Net, пространств имён, доступных при программировании на языке C#, а также соответствующих встроенных классов и функций этих классов. Продолжим знакомство с возможностями, предоставляемыми пространством имён System. Давайте рассмотрим ещё один класс – класс Math, содержащий стандартные математические функции, без которых трудно обойтись при построении многих выражений. Этот класс содержит два статических поля E (число е) и PI (число ПИ), а также 23 статических метода. Методы задают:

тригонометрические функции – Sin, Cos, Tan;

обратные тригонометрические функции – ASin, ACos, ATan, ATan2 (sinx, cosx);

гиперболические функции – Tanh, Sinh, Cosh;

экспоненту и логарифмические функции – Exp, Log, Log10;

модуль, корень, знак – Abs, Sqrt, Sign;

функции округления – Ceiling, Floor, Round;

минимум, максимум, степень, остаток – Min, Max, Pow, IEEERemainder.

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

2) Класс Random:

 

Умение генерировать случайные числа требуется во многих приложениях. Класс Random содержит все необходимые для этого средства. Класс Random имеет конструктор класса: для того, чтобы вызы-вать методы класса, нужно вначале создавать экземпляр (объект) класса. Созданный объект обеспечи-вает получение псевдослучайных (почти случайных…) чисел. Этим Random отличается от класса Math, у которого все поля и методы – статические, что позволяет обойтись без создания экземпляров класса Math.

Как всякий "настоящий" класс, класс Random является наследником класса Object, а, следовательно, имеет в своём составе и методы родителя. Рассмотрим часть оригинальных методов класса Random, используемых для генерирования последовательностей случайных чисел.

Начнём рассмотрение с конструктора класса. Конструктор обеспечивает создание объектов класса. Он имеет две реализации. Одна из них позволяет генерировать неповторяющиеся при каждом запуске серии случайных чисел. Начальный элемент такой серии строится на основе текущей даты и времени, что гарантирует уникальность серии. Этот конструктор вызывается без параметров. Он описан как Random(). Другой конструктор с параметром целого типа – Random(int) обеспечивает важную возмож-ность генерирования повторяющейся серии случайных чисел. Параметр конструктора используется для построения начального элемента серии, поэтому при задании одного и того же значения параметра се-рия будет повторяться.

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

Next () – метод без параметров выдаёт целые положительные числа во всём положительном диапазоне типа int;

Next (max) – выдаёт целые положительные числа в диапазоне [0, max];

Next (min, max) – выдаёт целые положительные числа в диапазоне [min, max].

Метод NextDouble() имеет одну реализацию. При каждом вызове этого метода выдаётся новое слу-чайное число, равномерно распределённое в интервале [0, 1].

Ещё один полезный метод класса Random позволяет при одном обращении получать целую серию случайных чисел. Метод имеет параметр – массив, который и будет заполнен случайными числами.

 

Метод описан как NextBytes(buffer). Так как параметр buffer представляет массив байтов, то, естест-венно, генерированные случайные числа находятся в диапазоне [0, 255].


Дата добавления: 2018-05-12; просмотров: 645; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!