До сих пор я описывал основные моменты работы в C# — знакомство с Visual Studio, арифметические и логические операторы, работы с массивами и при этом не затрагивал вопросов практического применения тех или иных знаний по языку. На мой взгляд, изучение языка программирования без практики — это всё равно, что учиться водить автомобиль исключительно по книгам, то есть занятие бесполезное. Вот поэтому я и решил немного потренироваться и написать свой первый класс в C# для дальнейшего его практического применения.
В качестве объекта изучения я выбрал класс TGeodesy из Delphi, который я писал для работы с KML в Delphi. Этот класс не содержит чего-то продвинутого в плане программирования типа дженериков, работы с JSON и XML и так далее — сугубо расчёты с использованием арифметических операторов и немного функций из Math. Чем не объект для изучения работы с классами в C# для начинающего?
Объявление класса в C#
Определение класса в C# в общем случае можно представить следующим образом:
//[access modifier] - [class] - [identifier] public class Customer { // Fields, properties, methods and events go here... }
Здесь представлен публичных класс с идентификатором (именем) Customer. Перед ключевым словом class указывается уровень доступа. В наем случае используется открытый класс (public), что означает, что любой пользователь может создавать его экземпляры. После ключевого слова class указывается идентификатор класса. Оставшаяся часть определения (в фигурных скобках) — это тело класса, в котором задаются данные и поведение. Поля, свойства, методы и события в классе собирательно называются членами класса.
Раз уж речь зашла об уровнях доступа, то…
Модификаторы доступа в C#
Все типы и члены типов имеют уровень доступности. Уровень доступности определяет возможность их использования из другого кода в вашей или в других сборках.
В C# возможно использование следующих модификаторов доступа:
public: доступ к типу или члену возможен из любого другого кода в той же сборке или другой сборке, ссылающейся на него.private: доступ к типу или члену возможен только из кода в том же объектеclassилиstruct.protected: доступ к типу или члену возможен только из кода в том же объектеclassлибо вclass, производном от этогоclass.internal: Доступ к типу или члену возможен из любого кода в той же сборке, но не из другой сборки.protected internal: доступ к типу или члену возможен из любого кода в той сборке, где он был объявлен, или из производногоclassв другой сборке.private protected: доступ к типу или члену возможен только из его объявляющей сборки из кода в том жеclassлибо в типе, производном от этогоclass.
Что будет делать наш первый класс C#?
Разрабатываемый класс будет выполнять преобразование декартовых координат (X,Y) в географические в системе WGS-84 (широта, долгота) и обратно. При разработке я постараюсь пока использовать только известные мне типы данных. Не исключено, что по прошествии некоторого времени я буду возвращаться к коды и вносить в него коррективы, исходя из полученных новых знаний по работе в C#.
Итак, пошаговый алгоритм разработки класса.
Создаем «скелет» будущего класса
Чтобы создать заготовку для будущего класса в Visual Studio можно выполнить следующие действия:
1) В окне «Обозреватель решений» выбрать проект в котором мы будем создавать наш класс. У меня — это проект «HelloWorld»:
2) Кликаем правой кнопкой мыши на названии проекта и в меню выбираем пункт «Добавить — Класс»:
3) Откроется новое окно «Добавление нового элемента» в котором необходимо выбрать интересующий нас элемент (Класс), а также задать название файла в котором будет этот класс храниться:
Я назвал файл «Wgs84.cs»
В результате этого Visual Studio создаст пустую заготовку для класса с названием Wgs84, которая будет выглядеть следующим образом:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace HelloWorld { class Wgs84 { } }
Все определения using можно убрать (они нам сегодня не понадобятся) и оставить только одно:
using System;
Далее, после using идёт определение пространства имен (namespace). Название пространства имён должно отражать суть того, что в этом пространстве имён располагается. А так как я планирую также добавлять в свой проект и другие классы и методы для работы с географическими системами координат, то я решил назвать пространство имен Geodesy. В результате у меня получилась вот такая структура файла с будущим классом Wgs84:
using System; namespace Geodesy { public class Wgs84 { } }
Задаем константы и поля класса
В C# константы можно охарактеризовать следующими признаками:
- Константа должна быть инициализирована при определении;
- После определения значение константы не может быть изменено.
Задается константа также, как и в Delphi с использованием ключевого слова const, например:
const double Radius = 6378137
При этом следует помнить, что пространство имен не может напрямую содержать константы, поля или методы. Что это значит? Например, в Delphi мы могли спокойно сделать вот так:
unit Geodesy; interface uses System.Math, System.Classes, System.SysUtils; const R = 6378137; // Экваториальный радиус Земли по WGS-84
И потом использовать константу в любых своих классах, методах и так далее. В C# же вот такая запись:
using System; namespace Geodesy { public const double EartRadius = 6378137; }
Приведет сразу к ошибке: «Ошибка CS0116 Пространство имен не может напрямую включать в себя такие члены, как поля или методы.»
То есть, в нашем случае, мы должны константу «завернуть» в наш класс и написать код следующим образом:
using System; namespace Geodesy //пространство имен { public class Wgs84 //класс { /* Константы для системы WGS-84 */ protected const double Radius = 6378137; } }
Соответственно, так как я не планирую давать доступ к этой константе извне другим классам и методам, то константа была помечена модификатором доступа protected. Всего же, для дальнейшей работы нам потребуется три константы:
public class Wgs84 { /* Константы для системы WGS-84 */ protected const double Radius = 6378137; //экваториальный радиус Земли в WGS-84 protected const double recF = 298.257223563; //сжатие элипсоида в WGS-84 protected const double f = 1.0 / recF; }
Что касается полей класса, то они определяются аналогично, например:
protected double Bo;
При этом, если в Delphi правилом хорошего тона считается начинать название поля класса с буквы «F», например, «FName«, «FOrder» и так далее, то в C#, на сколько я понимаю, такого делать не требуется и используется следующее правило: для именования полей, доступных вне класса (например, public), используется стиль pascal case (первая буква каждого слова пишется с большой буквы), для private полей — camel case (первая буква первого слова — с маленькой буквы, остальные — с большой).
Для работы нашего класса я определил следующие поля:
/* Поля класса, используемые для хранения вспомогательных значений */ protected double fe = Math.Sqrt(f * (2 - f)); protected double bo; protected double lo; protected double w; /* Поля, используемые в свойствах класса */ private double baseLatitude; //широта точки привязки private double baseLongitude;//долгота точки привязки
Свойства класса
Для начала немного справки Microsoft:
- Свойства позволяют классу предоставлять общий способ получения и задания значений, скрывая при этом код реализации или проверки.
- Метод доступа
getиспользуется для возврата значения свойства, а метод доступаset— для присвоения нового значения. Эти методы также могут иметь различные уровни доступа. - Ключевое слово
valueиспользуется для определения значения, присваиваемого методом доступаset. - Свойства могут быть доступны для чтения и записи (они имеют оба метода доступа —
getиset), только для чтения (они имеют метод доступаget, но не имеют метода доступаset) или только для записи (они имеют метод доступаset, но не имеют метода доступаget). - Простые свойства, не требующие пользовательского кода метода доступа, можно реализовать как определения текста выражений или как автоматически реализуемые свойства.
Теперь попробуем разобраться с тем, что написано выше. Итак, для свойства можно определить два метода — get и set, что, по-сути, является аналогом read и write в Delphi. В самом общем случае, свойство класса в C# можно описать следующим образом:
private double prop; public double Prop { get { return prop; } set { prop = value; } }
Вначале определено поле с именем prop, которое помечено модификатором доступа private. Далее определено публично (public) свойство с идентификатором Prop (с большой буквы). У это свойства определены два метода — get и set, то есть наше свойство можно и записывать и читать. Метод get у нас возвращает (return) значение поля prop, а метод set — присваивает полю prop передаваемое значение (value). Переводя этот код на язык Delphi мы бы получили вот такой код:
private Fprop: double; procedure SetProp(const Value: double); function GetProp: double; public property Prop: double read GetProp write SetProp; end; implementation { TMyCalss } function TMyCalss.GetProp: double; begin Result:=Fprop end; procedure TMyCalss.SetProp(const Value: double); begin FProp := Value; end;
Знающий человек сказал бы, что этот код Delphi можно сократить вот до такого без каких либо потерь:
private Fprop: double; public property Prop: double read Fprop write Fprop; end;
А как быть в C#? Каждый раз писать get/set? Конечно же нет. В C# можно определить так называемые автоматически реализуемые свойства. Синтаксис которых выглядит следующим образом:
public double TotalPurchases { get; set; }
Как видите, таким свойствам, во-первых, не требуется описание поля, а, во-вторых, get/set задаются без какого-либо кода. При объявлении свойства, как показано выше, компилятор создает закрытое анонимное резервное поле, которое может быть доступно только через методы доступа get и set свойства.
Так что, для описания свойств класса мы можем с вами руководствоваться следующими правилами:
- Если для свойства необходимо описание какой-либо дополнительной логики, например, значение заданное пользователем должно каким-либо образом пересчитываться и храниться, то используем полное описание с get/set
- Если дополнительная логика не требуется, то создаем автоматически реализуемые свойства.
Для своего класса я определил два автоматически реализуемых свойства:
/*Координаты базовой точки в декартовой системе координат*/ public double X0 { get; set; } public double Y0 { get; set; }
И два свойства с так называемыми резервными полями:
/*Координаты базовой точки в системе Wgs-84*/ public double BaseLatitude { get { return baseLatitude; } set { baseLatitude = value; bo = DegToRad(baseLatitude); w = Math.Sqrt(1 - Math.Pow(fe * Math.Sin(bo), 2)); } } public double BaseLongitude { get { return baseLongitude; } set { baseLongitude = value; lo = DegToRad(baseLongitude); } }
При задании этих свойств пользователем происходит преобразование координат из градусов в радианы, а также рассчитывается вспомогательное поле w, которое будет нам необходимо в методах класса. Так как я пока не знаю, потребуются ли мне значения географических координат в форме градусов, а не радиан, то пока решил сохранять введенное пользователем значение в поле класса. Возможно, что в дальнейшем код методов get set у свойств изменится и в классе будет на два поля меньше.
Методы класса
Методы в C# описываются следующим образом:
protected double MethodName(double arg)
то есть, в начале идет модификатор доступа, затем — тип возвращаемого значения, затем — имя метода и в круглых скобках аргументы (передаваемые и возвращаемые данные).
Был немного удивлен, когда не обнаружил в классе Math (аналог модуля Math в Delphi) методов перевода градусов в радианы и наоборот (DegToRad / RadToDeg). Поэтому пришлось их быстренько написать самому. Согласен, что по-хорошему, эти методы прямого отношения к системе Wgs-84 не имеют и их бы следовало вынести в отдельный класс, но пока, ради экономии времени, решал их оставить в своем классе. Выглядят эти методы следующим образом:
/*Вспомогательные методы*/
protected double DegToRad(double deg) { return deg * (Math.PI / 180.0); } protected double RadToDeg(double rad) { return rad * (180.0 / Math.PI); }
Теперь осталось дописать ещё два метода — для перевода координат из одной системы в другую:
/*Перевод декартовых координат в географические double X, Y - координаты точки, которые необходимо перевести в широту и долготу ref double B - широта ref double L - долгота */ public void XY2BL( double X, double Y, ref double B, ref double L) { double dB = (Y - Y0) /(radius * (1 - Math.Pow(fe, 2)) / Math.Pow(w, 3)); double dL = (X - X0) / (radius * Math.Cos(bo) / w); B = baseLatitude + RadToDeg(dB); L = baseLongitude + RadToDeg(dL); } /* Перевод географических координат в декартовы double B, L - широта и долгота точки ref double X, ref double Y - координаты точки в декартовой системе */ public void BL2XY(double B, double L, ref double X, ref double Y) { double dY = (DegToRad(B) - bo) * (radius * (1 - Math.Pow(fe, 2)) / Math.Pow(w, 3)); double dX = (DegToRad(L) - lo) * ((radius * Math.Cos(bo)) / w); X = X0 + dX; Y = Y0 + dY; }
Теперь наш класс может получать от пользователя координаты базовых точек и переводить любые координаты из одной системы в другую. Полный код класса представлен ниже.
Код класса Wgs84
using System; namespace Geodesy { // Сводка: // Предоставляет методы для преобразования географических координат в декартовы и наоборот public class Wgs84 { /* Константы для системы WGS-84 */ protected const double radius = 6378137; //экваториальный радиус Земли в WGS-84 protected const double recF = 298.257223563; //сжатие элипсоида в WGS-84 protected const double f = 1.0 / recF; /* Поля класса для хранение вспомогательных значений */ protected double fe = Math.Sqrt(f * (2 - f)); protected double bo; protected double lo; protected double w; /*Вспомогательные методы*/ protected double DegToRad(double deg) { return deg * (Math.PI / 180.0); } protected double RadToDeg(double rad) { return rad * (180.0 / Math.PI); } /* Поля, используемые в свойствах */ private double baseLatitude; //широта точки привязки private double baseLongitude;//долгота точки привязки /* Свойства */ public double BaseLatitude { get { return baseLatitude; } set { baseLatitude = value; bo = DegToRad(baseLatitude); w = Math.Sqrt(1 - Math.Pow(fe * Math.Sin(bo), 2)); } } public double BaseLongitude { get { return baseLongitude; } set { baseLongitude = value; lo = DegToRad(baseLongitude); } } public double X0 { get; set; } public double Y0 { get; set; } /* Методы */ /*Перевод декартовых координат в географические double X, Y - координаты точки, которые необходимо перевести в широту и долготу ref double B - широта ref double L - долгота */ public void XY2BL( double X, double Y, ref double B, ref double L) { double dB = (Y - Y0) /(radius * (1 - Math.Pow(fe, 2)) / Math.Pow(w, 3)); double dL = (X - X0) / (radius * Math.Cos(bo) / w); B = baseLatitude + RadToDeg(dB); L = baseLongitude + RadToDeg(dL); } /* Перевод географических координат в декартовы double B, L - широта и долгота точки ref double X, ref double Y - координаты точки в декартовой системе */ public void BL2XY(double B, double L, ref double X, ref double Y) { double dY = (DegToRad(B) - bo) * (radius * (1 - Math.Pow(fe, 2)) / Math.Pow(w, 3)); double dX = (DegToRad(L) - lo) * ((radius * Math.Cos(bo)) / w); X = X0 + dX; Y = Y0 + dY; } }; }
Использование класса в приложении
Попробуем использовать разработанный класс в приложении C#. Для этого мы должны включить пространство имен Geodesy в наше приложение, используя директиву using.
using System; using Geodesy; //используем Geodesy в нашем приложении namespace HelloWorld { class Program { static void Main(string[] args) { } } }
Теперь мы можем создать объект, реализующий наш класс Wgs84 и задать ему необходимые свойства. Сделать это можно, опять же несколькими способами.
Первый способ:
//создаем объект Wgs84 ConvertCoord = new Wgs84(); //задаем свойства ConvertCoord.BaseLatitude = 55.55; ConvertCoord.BaseLongitude = 74.75; ConvertCoord.X0 = 0; ConvertCoord.Y0 = 0;
А можем воспользоваться более лаконичной формой записи:
Wgs84 Coord = new Wgs84 { BaseLatitude = 55.55, BaseLongitude = 74.75, X0 = 0, Y0 = 0 };
Теперь, чтобы воспользоваться переводом координат из одной системы в другую мы можем написать вот так:
double B = 0; double L = 0; Coord.XY2BL(100, 100, ref B, ref L); Console.WriteLine("Широта {0}; Долгота {1} ", B, L);
Полный код программы, показывающей работу с классом представлен ниже:
using System; using Geodesy; namespace HelloWorld { class Program { static void Main(string[] args) { Wgs84 Coord = new Wgs84 { BaseLatitude = 55.55, BaseLongitude = 74.75, X0 = 0, Y0 = 0 }; double B = 0; double L = 0; Coord.XY2BL(100, 100, ref B, ref L); Console.WriteLine("Широта {0}; Долгота {1} ", B, L); Console.ReadLine(); } } }
Подведем итог
Сегодня мы создали первый наш класс в C#. Да, возможно он не идеален и профессиональный разработчик в C# найдет в нем массу мест для улучшения, но, тем не менее общее представление о работе с классами в C# должно сложиться.
Конечно, эта статья не охватывает абсолютно все аспекты работы с классами в C#. Например, я не рассмотрел работу с событиями и другие моменты,которые в дальнейшем нам пригодятся в работе, однако,при этом стоит помнить, что всё в .NET так или иначе завязано на понятиях «класс» и «объект» поэтому к работе с классами я ещё буду возвращаться ни один раз.



