Жанр: Учеба
Си шарп: создание приложений для windows
...дим экземпляр класса в куче (heap) и возвращаем на него
ссылку.
Затем в строчках
wrkl.age=34;
wrkl.name="Иванов";
Console.WriteLine(wrkl.name+", "+wrkl.agei;
мы используем наш класс, присваивая некоторые значения для возраста
и имени и выводя их потом на экран.
СОСТАВ КЛАССОВ
В главе 5 были описаны типы, определенные в CTS (Common Type
System). Эти типы поддерживаются как члены классов С# и бывают следующих
видов:
• Поле. Так называется член-переменная, содержащий некоторое значение.
В ООП поля иногда называют данными объекта. К полю можно
применять несколько модификаторов в зависимости от того, как вы
собираетесь его использовать. В число модификаторов входят static,
readonly и const. Ниже мы познакомимся с их назначением и способами
применения.
• Метод. Это реальный код, воздействующий на данные объекта (или
поля). Здесь мы сосредоточимся на определении данных класса. Подробнее
о методах смотрите в главе 10.
• Свойства. Их иногда называют "разумными" полями (smart fields),
поскольку они на самом деле являются методами, которые клиенты
класса воспринимают как поля. Это обеспечивает клиентам большую
степень абстрагирования за счет того, что им не нужно знать, обращаются
ли они к полю напрямую или через вызов метода-аксессора.
Подробнее о свойствах смотрите в главе 11.
• Константы. Как можно предположить, исходя из имени, константа -
это поле, значение которого изменить нельзя. В главе 4 уже рассматривались
константные типы данных. Ниже мы обсудим константы и сравним
их с сущностью под названием "неизменяемые поля".
Классы 87
Индексаторы. Если свойства - это "разумные" поля, то индексаторы -
это "разумные" массивы, так как они позволяют индексировать объекты
методами-аксессорами get и set. С помощью индексатора легко проиндексировать
объект для установки или получения значений. Подробнее-
в главе 13.
События. Событие вызывает исполнение некоторого фрагмента кода.
События - неотъемлемая часть программирования для Microsoft Windows.
Например, события возникают при движении мыши, щелчке или изменении
размеров окна. Об использовании событий смотрите главу 16.
МОДИФИКАТОРЫ ДОСТУПА
Теперь, зная, что типы могут быть определены как члены класса С#,
познакомимся с модификаторами, используемыми для задания степени
доступа, или доступности данного члена для кода, лежащего за пределами
его собственного класса. Они называются модификаторами доступа
(access modifiers)
Модификаторы доступа в С#
Модификатор доступа
public
protected
private
Описание
Член доступен вне определения класса и иерархии
производных классов.
Член невидим за пределами класса, к нему могут
обращаться только производные классы.
Член недоступен за пределами области видимости
определяющего его класса. Поэтому доступа к этим
членам нет даже у производных классов.
internal Член видим только в пределах текущей единицы компиляции.
Модификатор доступа internal в плане ограничения
доступа является гибридом public и protected,
зависимым от местоположения кода.
Если вы не хотите оставить модификатор доступа для данного члена
по умолчанию (private), задайте для него явно модификатор доступа. Этим
С# отличается от C++, где член, для которого явно не указан модификатор
доступа, принимает на себя характеристики видимости, определяемые
модификатором доступа, заданным для предыдущего члена. Например,
в приведенном ниже коде на C++ видимость членов а, Ь и с
определена модификатором public, а члены d и е определены как protected:
class AccessCPlusPlus
{
public:
int a;
int b;
Раздел I I . Фундаментальные понятия
i n t с ;
protected:
int d;
int e;
А в результате выполнения этого кода на С# член b объявляется как
private. Для объявления членов на С# как public необходимо использовать
следующую инструкцию:
class AccessCSharp
{
public int a;
public int b;
public int c;
protected int d;
protectea in: e;
В результате выполнения следующего кода на С# член b объявляется
как private:
public DifAccessInCSharp
i'
public int г.;
int b;
МЕТОД Main
У каждого приложения на С# должен быть метод Main, определенный в
одном из его классов. Кроме того, этот метод должен быть определен как
public и static (ниже будет объяснено, что значит static). Для компилятора
С# не важно, в каком из классов определен метод Main, а класс, выбранный
для этого, не влияет на порядок компиляции. Здесь есть отличие от
C++, где зависимости должны тщательно отслеживаться при сборке приложения.
Компилятор С# достаточно "умен", чтобы самостоятельно просмотреть
ваши файлы исходного кода и отыскать метод Main. Между тем,
этот очень важный метод является точкой входа во все приложения на С#.
Вы можете поместить метод Main в любой класс, но для его размещения
рекомендуется создавать специальный класс. Это можно сделать,
используя простой класс MyClass.
class MyClass
private in r. MyClassId;
!
class AppClass
1
static public void Mainf)
Классы 89
MyClasa nyObj - new MyClass( );
Как видите, здесь два класса. Этот общий подход используется при
программировании на С# даже простейших приложений. MyClass представляет
собой класс предметной области, a AppClass содержит точку входа
в приложение (Main). В этом случае метод Main создает экземпляр объекта
MyClass, и, будь это настоящее приложение, оно использовало бы члены
объекта MyClass.
Аргументы командной строки
Вы можете обращаться к аргументам командной строки приложения,
объявив метод Main как принимающий аргументы типа массива строк.
Затем аргументы могут обрабатываться так же, как любой массив. Хотя
речь о массивах пойдет только в главе 12, ниже приводится простой код,
который по очереди выводит все аргументы командной строки на стандартное
устройство вывода.
using liystem;
class С ommandLineApp
{
public static void Main(string[| args)
\
foreach (string arg in args]
Console. WriteLine( "Аргумент: (О}", arg);
t
А вот пример вызова этого приложения с парой случайно выбранных
чисел:
e:"CommandLineApp 5 42
Аргумент: 5
Аргумент: 42
Аргументы командной строки передаются в виде массива строк. Если
это флаги или переключатели, их обработку вы должны запрограммировать
сами.
Возвращаемые значения
Чаще всего метод Main определяется так:
class SomeClass
{
public static void Mainf)
90 Раздел II, Фундаментальные понятия
j
Однако вы можете определить метод Main так, чтобы он возвращал
значения типа int. Хотя это не является общепринятым в приложениях с
графическим интерфейсом, такой подход может быть полезным в консольных
приложениях, предназначенных для пакетного исполнения. Оператор
return завершает исполнение метода, а возвращаемое при этом
значение применяется вызывающим приложением как код ошибки для
вывода определенного пользователем сообщения об успехе или неудаче.
Для этого служит следующий прототип:
public static int Main()
(
// Вернуть некоторое значение типа int,
// представляющее код завершения.
return 0;
Несколько методов Main
В С# разработчиками включен механизм, позволяющий определять
более одного класса с методом Main. Зачем это нужно? Одна из причин -
необходимость поместить в ваши классы тестовый код. Затем, используя
переключатель Лпат;"имя_Класса", компилятору С# можно задавать
класс, метод Main которого должен быть задействован. Вот пример, в
котором создано два класса, содержащих методы Main:
using System;
class Mainl
{
public static void Main()
{
Console.WriteLine("Mainl");
class Main2
{
public static void Main()
{
Console.WriteLine("Main2")
Чтобы скомпилировать это приложение так, чтобы в качестве точки
входа в приложение применялся метод Mainl.Main, нужно использовать
переключатель:
esc MultipleMain.es /raain:Mainl
Классы 91
При изменении переключателя на /main:Main2 будет использован
метод Main2.Main.
Следует соблюдать осторожность и задавать в указанном переключателе
имени класса верный регистр символов, так как С# чувствителен
регистру. Кроме того, попытка компиляции приложения, состоящего из
нескольких классов с определенными методами Main, без указания переключателя
/main, вызывает ошибку компилятора.
ИНИЦИАЛИЗАЦИЯ КЛАССОВ И КОНСТРУКТОРЫ
Одно из величайших преимуществ языков ООП, таких как С#, состоит
в том, что вы можете определять специальные методы, вызываемые всякий
раз при создании экземпляра класса. Эти методы называются конструкторами
(constructors). C# вводит в употребление новый тип конструкторов-
статические (static constructors), с которыми вы познакомитесь ниже
в подразделе "Константы и неизменные поля".
Гарантия инициализации объекта должным образом, прежде чем он
будет использован,- ключевая выгода от конструктора. Когда пользователь
создает экземпляр объекта, вызывается его конструктор, который
должен вернуть управление до того, как пользователь сможет выполнить
над объектом другое действие. Именно это помогает обеспечивать целостность
объекта и делать написание приложений на объектно-ориентированных
языках гораздо надежнее.
Но как назвать конструктор, чтобы компилятор знал, что его надо
вызывать при создании экземпляра объекта? Разработчики С# последовали
в этом вопросе за разработчиками C++ и провозгласили, что у конструкторов
в С# должно быть то же имя, что и у самого класса. Вот
простой класс с таким же простым конструктором:
using System;
class Constructor1Лрр
{
ConscrucrorlApp(]
{
Console. Wr±:eLine("a конструктор.") ;
public static void Main(]
(
ConstructorlAp app = new ConstruotorlApp();
\
}
Значений конструкторы не возвращают. При попытке использовать с
конструктором в качестве префикса имя типа компилятор сообщит об
ошибке, пояснив, что вы не можете определять члены с теми же именами,
что у включающего их типа.
92 Раздел I I . Фундаментальные понятия
Следует обратить внимание и на способ создания экземпляров объектов
в С#. Это делается при помощи ключевого слова new:
"класс" "объект" = new "класс"(аргументы конструктора)
Если раньше вы программировали на C++, обратите на это особое
внимание. В C++ вы могли создавать экземпляр объекта двумя способами:
объявлять его в стеке, скажем, так:
//Код на Си- Создает экземпляр CMyCleas в стеке
CKyClass rr.yClass;
или создавать копию объекта в свободной памяти (или в куче), используя
ключевое слово C++ new:
//Код на С+-. Создает экземпляр CmyCiass в куче.
CHyCloss myClass = new CmyClassf);
Экземпляры объектов на С# формируются иначе, что и сбивает с толку
новичков в разработке на С#. Причина путаницы в том, что для создания
объектов оба языка используют одни и те же ключевые слова. Хотя с помощью
ключевого слова new в C++ можно указать, где именно будет создаваться
объект, место его построения на С# зависит от типа объекта, экземпляр
которого создается. Как вы уже знаете, ссылочные типы создаются в
куче, а размерные - в стеке. Поэтому ключевое слово new позволяет делать
новые экземпляры класса, но не определяет место создания объекта.
Хотя можно сказать, что приведенный ниже код на С# не содержит
ошибок, он делает совсем не то, что может подумать разработчик на C++:
MyClass myClass;
На C++ он создаст в стеке экземпляр MyClass. Как сказано выше, на С#
вы можете создавать объекты, только используя ключевое слово new.
Поэтому на С# эта строка лишь объявляет переменную myClass как переменную
типа MyClass, но не создает экземпляр объекта.
Примером служит следующая программа, при компиляции которой
компилятор С# предупредит, что объявленная в приложении переменная
ни разу не используется:
using System;
class Construetor2App
~ructor2App()
i
Console.WriteLine("я конструктор");
}
public static void Hainf)
{
Construct.or2App app;
}
)
Поэтому, объявляя объект, создайте где-нибудь в программе его экземпляр
с помощью ключевого слова new:
Construotor2App app;
app -new Constructor2App() ;
Классы 93
Зачем объявлять объект, не создавая его экземпляров? Объекты объявляются
перед использованием или созданием их экземпляров с помощью
new, если вы объявляете один класс внутри другого. Такая вложенность
классов называется включение (containment) или агрегирование (aggregation).
СТАТИЧЕСКИЕ ЧЛЕНЫ КЛАССА
Вы можете определить член класса как статический (static member)
или член экземпляра (instance member). По умолчанию каждый член
определен как член экземпляра. Это значит, что для каждого экземпляра
класса делается своя копия этого члена. Когда член объявлен как статический,
имеется лишь одна его копия. Статический член создается при
загрузке содержащего класс приложения и существует в течение жизни
приложения. Поэтому вы можете обращаться к члену, даже если экземпляр
класса еще не создан. Хотя зачем вам это?
Один из примеров - метод Main. CLR (Common Language Runtime)
нужна универсальная точка входа в ваше приложение. Поскольку CLR не
должна создавать экземпляры ваших объектов, существуют правила,
требующие определить в одном из ваших классов статический метод Main.
Вы можете захотеть использовать статические члены при наличии метода,
который формально принадлежит классу, но не требует реального
объекта. Скажем, если вам нужно отслеживать число экземпляров данного
объекта, которое создается во время жизни приложения. Поскольку
статические члены "живут" на протяжении существования всех экземпляров
объекта, должен работать такой код:
using System;
class InstCount
{
public InstCount IS
ins~anceCount++;
static public int instanceCount;
//instanceCount - 0;
class AppClass
t
public static void Main()
t
Console.WriteLine(InstCount.instanceCount)
InstCount icl = new InstCount ();
94 Раздел I I . Фундаментальные понятия
Console.WriteLine(InstCount.instanceCount);
InstCount ic2 = new InstCount ();
Console.WriteLine(InstCount.instanceCount] ;
В этом примере выходная информация будет следующая:
О
И последнее замечание по статическим членам: у них должно быть некоторое
допустимое значение. Его можно задать при определении члена:
static public int instanceCountt = 10;
Если вы не инициализируете переменную, это сделает CLR после запуска
приложения, установив значение по умолчанию, равное 0. Поэтому
следующие строки эквивалентны:
static public int instanceCount2;
static public int instanceCount2=0;
КОНСТАНТЫ И НЕИЗМЕНЯЕМЫЕ ПОЛЯ
В главе 5 уже упоминалось о константах. Здесь лишь еще раз будет
описана их специфика и дана сравнительная характеристика с неизменяемыми
полями. Можно с уверенностью сказать, что возникнут ситуации,
когда изменение некоторых полей при выполнении приложения будет
нежелательно, например, это могут быть файлы данных, от которых
зависит ваше приложение, значение pi для математического класса или
любое другое используемое в приложении значение, о котором вы знаете,
что оно никогда не изменится. В этих ситуациях С# позволяет определять
члены тесно связанных типов: константы и неизменяемые поля.
Константы
Из названия легко догадаться, что константы (constants), представленные
ключевым словом const,- это поля, остающиеся постоянными в течение
всего времени жизни приложения. Определяя что-либо как const, достаточно
помнить два правила. Во-первых, константа - это член, значение
которого устанавливается в период компиляции программистом или компилятором
(в последнем случае это значение по умолчанию). Во-вторых,
значение члена-константы должно быть записано в виде литерала.
Чтобы определить поле как константу, укажите перед определяемым
членом ключевое слово const:
using System;
class MagicNumbers
public const double pi = 3.1415;
public const int ig] = (10) ;
class ConstApp
{
public static void Main()
/
Console.WriteLine("pi = {0}, g = {!}",
MagicNurrbers . pi, MagicKumbers . [g] ) ;
)
)
Обратите внимание на один важный момент, связанный с этим кодом.
Клиенту нет нужды создавать экземпляр класса MagicNumbers, поскольку
по умолчанию члены const являются статическими.
Неизменяемые поля
Поле, определенное как const, ясно указывает, что программист намерен
поместить в него постоянное значение. Это плюс. Но оно работает,
только если известно значение подобного поля в период компиляции. А
что же делать программисту, когда возникает потребность в поле, чье
значение не известно до периода выполнения, но после инициализации
не должно меняться? Эта проблема (которая обычно остается нерешенной
в большинстве других языков) разрешена разработчиками языка С#
с помощью неизменяемого поля (read-only field).
Определяя поле с помощью ключевого слова readonly, вы можете установить
значение поля лишь в одном месте - в конструкторе. После этого
поле не могут изменить ни сам класс, ни его клиенты. Допустим, для
графического приложения нужно отслеживать разрешение экрана. Справиться
с этой проблемой с помощью const нельзя, так как до периода
выполнения приложение не может определить разрешение экрана у
пользователя. Поэтому лучше всего использовать такой код:
using System;
class GraphicsPackage
{
public readonlу int ScreenWidth;
public readonly int ScreenHeighz;
public GraphicsPackage(]
f
th^s.ScreenWidth = 1024;
th-S.ScreenHeight = 768;
96 Раздел II. Фундаментальные понятия
class ReadOnlyApp
public static void Main()
GraphicsPackage graphics = new GraphicsPackage()
Console.WriceLine("Ширина = I 0 j, Высота = {1}",
graphics.ScreenWidth,
graphics.ScreenHeight);
На первый взгляд кажется: это то, что нужно. Но есть одна маленькая
проблема: определенные нами неизменяемые поля являются полями
экземпляра, а значит, чтобы задействовать их, пользователю придется
создавать экземпляры класса. Может, это и не проблема, и код даже
пригодится, когда значение неизменяемого поля определяется способом
создания экземпляра класса. Однако если вам нужна константа, по
определению статическая, но инициализируемая в период выполнения?
Тогда нужно определить поле с обоими модификаторами - static и readonly,
а затем создать особый, статический тип конструктора. Статические конструкторы
(static constructor) используются для инициализации статических,
неизменяемых и других полей. Здесь был изменен предыдущий
пример так, чтобы сделать поля, определяющие разрешение экрана, статическими
и неизменяемыми, а также добавлен статический конструктор.
Обратите внимание на ключевое слово static, добавленное к определению
конструктора:
using System;
class ScreenResolution
public static readonly int ScreenWidth;
public static readonly int ScreenHeight;
static ScreenResolution(1
i
// code would be here to
// calculate resolution
ScreenWidth = 1024;
ScreenHeight = 768;
lass ReadOnlyApp
public static void MainO
Классы 97
Console.WriteLine("Ширина = {0}, Высота = {1}",
ScreenResolution.ScreenMidth,
ScreenBesolution . ScreenHeight) ;
ВЛОЖЕННЫЕ КЛАССЫ
Иногда некоторый класс играет чисто вспомогательную роль для другого
класса и используется только внутри него. В этом случае логично
описать его внутри существующего класса. Вот пример такого описания:
using System;
namespace test
(
class ClassA
{
//Вложенный класс
private class ClassB
{
public int z;
}
//Переменная типа вложенного класса
private ClassB w;
//Конструктор
public ClassAO
{
w=new ClassB ();
w.z=35;
}
//Некоторый метод
public int SomeMethod()
{
return w.z;
class Test
{
static void Main(string[] args)
!
ClassA v=new ClassAO;
int k=v.SomeMethod() ;
Console.WriteLine(k) ;
98 Раздел I I . Фундаментальные понятия
После запуска программа выведет результат:
Здесь класс ClassB объявлен внутри класса ClassA,
причем со словом private, так что его экземпляры мы
можем создавать только внутри класса ClassA (что
мы и делаем в конструкторе класса ClassA). Методы
класса ClassA имеют доступ к экземпляру класса
ClassB (как, например, метод SomeMethod).
Вложенный класс имеет смысл использовать
тогда, когда его экземпляр используется только в
определенном классе. Кроме того, с вложением классов
улучшается читаемость кода - если нас не интересует
устройство основного класса, то разбирать
работу вложенного класса нет необходимости.
Обратите также внимание, как вложенные классы показываются на вкладке
ClassView (см. рис. 9.1).
i LonsolcAppli cil ion1.
{) tcs:
|+i *ij Bases and Interfaces
[•! - * Bases and Interfaces
•••••• ClassAQ
•-"SomeMethodO
--X/ ^
i+! •& Test
Рис. 9.1. Отображение
вложенных классов
НАСЛЕДОВАНИЕ
Чтобы построить (в терминах данных или поведения) класс на основе
другого класса, применяется наследование. Оно принадлежит к правилам
заменяемости, а именно к тому, которое гласит, что производный класс
может быть заменен базовым. Примером этого может служить написание
иерархии классов базы данных. Допустим, вам был нужен класс для обработки
баз данных Microsoft SQL Server и Oracle. Поскольку эти базы
различны, вам понадобится по классу для каждой из БД. Однако в обеих
БД достаточная доля общей функциональности, которую можно поместить
в базовый класс, а два других класса сделать его производными, при
необходимости подменяя или изменяя поведение, унаследованное от базового
класса.
Чтобы унаследовать один класс от другого, используется синтаксис:
class "производный_класс": "базовый_класс"
Вот пример того, на что может быть похожа такая программа с базами
данных:
using System;
class Database
{
public Database))
CommonField = 42,-
public int CommonField;
Классы 99
public void ConmonMethod()
Console.WriteLine("Database.Common Method");
class SQLServer: Database
public void SomeMethodSpecificToSQLServer()
f
Console.WriteLine("SQLServer.SomeMethodSpecificToSQLServer");
)
}
class Oracle: Database
public void SomeMethodSpecificToQracle()
Console.WriteLine("Oracle.SomeMethodSpecificToOracle");
)
class InheritanceApp
public static void Main()
SQLServer sqlserver = new SQLServer();
sqlserver.SomeMethodSpecificToSQLServer();
sqlserver.CommonMethod();
Console.WriteLine("Inherited common field = {0 } ", sqlserver.CommonFieldI;
После компиляции и исполнения этого приложения получится следующий
результат:
SQLServer.SomeMethodSpecificToSQLServer Database.Common Method
Inherited common field =42
Заметьте: методы Data base. Common Method и Database.CommonField теперь
являются частью определения класса SQLServer. Так как классы SQLServer
и Oracle происходят от базового класса Database, оба наследуют практически
все его члены, определенные как public, protected или internal. Единственное
исключение - конструктор, который не может быть унаследован.
У каждого класса должен быть реализован собственный конструктор,
независимый от базового класса.
Подмена методов описана в главе 10. Однако следует заметить, что
подмена методов позволяет изменять реализацию унаследованных от
100 Раздел I I . Фундаментальные понятия
базового класса методов. Абстрактные классы интенсивно используют
подмену методов (см. ниже).
Для наглядности давайте рассмотрим еще один пример. Представьте,
что у нас есть некоторый класс (быть может, весьма большой), который
почти подходит для нашей конкретной задачи. Почти, но не совсем. Чтото
(некоторые методы) в этом классе надо изменить, что-то - добавить.
Наследование как раз и служит для этого. При наследовании мы объявляем
наш класс потомком другого класса. И наш класс-потомок (или, как
его еще называют, дочерний класс) автоматически приобретает все, что
было в родительском классе. Это мы делаем буквально парой строчек.
Затем в дочерний класс мы можем что-нибудь добавить (чего не было в
классе родительском). Кроме того, в дочернем классе мы можем чтонибудь
из родительского класса переопределить - так, что оно уже будет
работать по-другому.
Например, у нас будет класс Worker и производный от него класс Boss.
Класс Boss будет отличаться от класса Worker, во-первых, наличием переменной
numOfWorkers (для количества подчиненных) и, во-вторых, другой
работой в Boss метода для задания возраста (setAge):
using System;
namespace test
(
//Класс Worker
class Worker
{
protected int age=0;
public void setAge (int age)
{
if(age"0 && age"100)
this.age=age;
else
this.age=0;
public int getAge()
{
return age;
//Класс Boss
class Boss: Worker
{
public int numOfWorkers; //Количество подчиненных
public new void setAge(int age)
i
if(age"0 SS age"45)
this.age=age;
Классы 101
else
this.age=0;
clas? Test.
{
static void Main(string[] args)
{
Worker wrkl = new Worker ();
Eoss boss = new Boss () ;
wrkl.setAge(50) ;
boss.setAge(50);
boss.numOfWorkers-4;
Console.WriteLine("Возраст работника "+wrkl.getAge())
Console.WriteLine("Возраст босса "+boss.getAge()+
".ХпКоличество подчиненных "+boss.numOfWorkers);
Обратите внимание, как мы вводим новый класс Boss:
class Boss: Worker{
Такая запись и означает, что новый класс Boss будет потомком другого
класса (Worker в данном случае) и, следовательно, будет автоматически
уметь все то же самое, что и родительский класс. В частности, в
нем есть переменная age для хранения возраста. Далее, в класс Boss мы
вводим переменную numOfWorkers для хранения количества подчиненных
у нашего босса. В родительском классе такой переменной не было.
Кроме того, нас не устроила реализация метода setAge из родительского
класса Worker (по каким-то внутренним инструкциям фирмы возраст
босса не может превышать 45 лет, тогда как возраст работника не может
превышать 100), поэтому мы просто написали в классе Boss метод
с таким же именем. Обратите внимание на слово new при объявлении
метода:
public new void setAge(int age)
Таким образом, в производном классе мы можем что-нибудь добавлять
и что-нибудь изменять по отношению к классу родительскому. При
этом убирать что-нибудь полученное в наследство от родительского класса
мы не можем.
После запуска наша программа выдаст вполне ожидаемый результат:
Возраст работника 50
Возраст босса О
Количество подчиненных 4
102 Раздел I I . Фундаментальные понятия
ИНИЦИАЛИЗАТОРЫ КОНСТРУКТОРОВ
Во всех конструкторах С#, кроме System.Object, конструкторы базового
класса вызываются прямо перед исполнением первой строки конструктора.
Эти инициализаторы конструкторов позволяют задавать класс и подлежащий
вызову конструктор. Они бывают двух видов.
Инициализатор в виде base(...) активизирует конструктор базового
класса текущего класса.
Инициализатор в вид
...Закладка в соц.сетях