Жанр: Учеба
Си шарп: создание приложений для windows
...ePay из базового класса. То, что здесь
происходит,-пример раннего связывания (early binding). Во время компиляции
этого кода компилятор С#, изучив вызов emp.CalculatePay, определил
адрес памяти, на который ему нужно перейти во время этого вызова.
В таком случае это будет адрес метода Employee.CalculatePay. Вызов
метода Employee.CalculatePay представляет собой проблему. Мы хотим,
чтобы вместо раннего связывания происходило динамическое (позднее)
связывание (late binding). Динамическое связывание означает, что компилятор
не выбирает метод для исполнения до периода выполнения. Чтобы
заставить компилятор выбрать правильную версию метода объекта, мы
используем два новых ключевых слова: virtual и override. С методом базового
класса применяется ключевое слово virtual, a override - с реализацией
метода в производном классе. Вот еще один пример, который на этот
раз работает должным образом,- Ро1уЗАрр.
124 Раздел II. Фундаментальные понятия
using System;
class Employee
{
public Employee(string name)
{
this.Name = name;
protected string Name;
public string name
i
get
{
return this.Name;
virtual public void CalculatePay()
Console.WriteLine("Employee.CalculatePay вызвана для (0 } ", name);
i
}
class ContractEmployee: Employee
public ContractEmployee(string name)
: base(name)
override public void CalculatePay()
Console.WriteLine("ContractEmployee.CalculatePay вызвана для (0}", name);
class SalariedEmployee: Employee
public SalariedEmployee (string name)
: base(name)
override public void CalculatePay()
Методы125
Console.WriteLineC'SalariedEmployee.CalculatePay вызвана для {0}", name);
class Poly2App
i
protected Employee!] employees;
public void LoadErr.ployees ()
{
// эмулируем загрузку из базы данных
employees = new Employee[2];
employees[0] = new ContractEmployee{"Алексей Рубинов")
employees[l] = new SslariedEmployee("Василий Лазерко")
public: void DoPayrollf)
{
foreach(Employee emp in employees)
{
enp.CalculatePay(];
}
}
public static void Main()
{
Poly2App poly2 = new Poly2App();
poiy2.LoadEmployees();
poly2.DoPayroll();
Выполнение такого кода приведет к следующим результатам:
ContractEmployee.CalculatePay вызвана для Алексей Рубинов
SalariedEmployee.CalculatePay вызвана для Василий Лазерко
СТАТИЧЕСКИЕ МЕТОДЫ
Статическим называется метод, который существует в классе как таковом,
а не в отдельных его экземплярах. Как и в случае других статических
членов, главное преимущество статических методов в том, что
они расположены вне конкретных экземпляров класса и не засоряют
глобальное пространство приложения. При этом они и не нарушают
принципов ООП, поскольку ассоциированы с определенным классом.
Примером может служить API баз данных, представленный ниже. В
126 Раздел II. Фундаментальные понятия
той иерархии классов есть класс SQLServerDb. Помимо базовых операций
для работы с БД (new, update, read и delete), класс содержит метод,
предназначенный для восстановления БД. В методе Repair не нужно
открывать саму БД. Следует использовать функцию ODBC SQLConfigDataSource,
которая предполагает, что БД закрыта. Однако конструктор
SQLServerDb открыл БД, указанную переданным ему именем. Поэтому
здесь очень удобно использовать статический метод. Это позволит поместить
метод в класс SQLServerDb, к которому он принадлежит, и даже
не обращаться к конструктору класса. Очевидно, выгода клиента в том,
что он также не должен создавать экземпляр класса SQLServerDb. В
следующем примере вы можете видеть вызов статического метода
(RepairDatabase) из метода Main. Заметьте, что для этого не создается
экземпляр SQLServerDB:
using System;
class SQLServerDb
{
public static void RepairDatabase()
{
Console.WriteLine("восстановление базы данных... " ] ;
class StaticMethodlApp
{
public static void Main()
{
SQLServerDb.RepairDatabase(] ;
Определить метод как статический позволяет ключевое слово static. Затем
для вызова метода пользователь применяет синтаксис вида Класс.Метод.
Этот синтаксис необходим, даже если у пользователя есть ссылка на
экземпляр класса. Данный момент можно проиллюстрировать кодом,
который не будет компилироваться:
using System;
class SQLServerDb
public static void RepairDatabase()
{
Console.WriteLine("восстановление базы данкых... " ) ;
class StacicMethod2App
Методы 127
public static void Main()
{
SQLServerDb db = new SQLServerDb();
db.RepairDatabase() ;
Последним моментом, касающимся статических методов, является
правило, определяющее, к каким членам класса можно обращаться из
статического метода. Как вы можете догадаться, статический метод может
обращаться к любому статическому члену в пределах класса, но не
может обращаться к члену экземпляра. Например:
using System;
class SQLServerDb
{
static string progressStringl = "repairing database...";
string progressString2 = "repairing database...";
public static void RepairDatabase()
{
Console.WriteLine(progressStringl); // все правильно
Console.WriteLine(progressString2); // не будет компилироваться
// compilation.
class StaticHethod3App
{
public static void Main()
t
SQLServerDb.RepairDatabase();
Компилятор не пропустит этот код, потому что он содержит ошибку.
Статический метод пытается обратиться к нестатической переменной
класса. Это недопустимо в С#.
РЕКУРСИЯ
Иногда возникают ситуации, когда в теле функции необходимо вызвать
экземпляр этой же функции. Такое явление называется рекурсией.
Использование рекурсии позволит вам в некоторых случаях избежать
сложных циклов и проверок. Типичным примером использования рекурсии
является функция нахождения факториала числа.
128 Раздел I I . Фундаментальные понятия
using System;
class Degrees
{
public static int Factorial(int x)
{
return (x ==1) ? 1: x * Factorial(x-1);
}
public static void Main])
for (int i = 1; i " 10; i
i
Console.WriteLine(Factorial(i) ) ;
Функция Factorial явяется рекурсивной. На первый взгляд она может
показаться вам сложной. Однако она совсем проста (всего одна строка).
Если функция приняла значение, отличное от единицы, то вызывает саму
себя и передает значение, на единицу меньшее. Так происходит до тех
пор, пока переданое значение не станет равным единице. После того как
функция примет значение 1, начнется выход из всех экземпляров вызванных
функций Factorial и возврат произведения параметра функции и возвращенного
значения.
Допустим, мы ищем факториал числа 5. Тогда передача параметров экземплярам
функции Factorial идет в такой последовательности: 5, 4, 3, 2,1.
А последовательность возвращаемых значений будет следующей: 1, 2, 6,
24, 120. Результатом работы функции станет число 120.
Однако рекурсия имеет и недостатки. При создании каждого экземпляра
функции выделяются дополнительные блоки памяти, что нежелательно
при работе программы.
11. СВОЙСТВА
ПРИМЕНЕНИЕ СВОЙСТВ
С# поддерживает такой тип данных, как свойства. Свойства позволяют
не только скрывать классам реализацию методов, но и запрещать членам
любой прямой доступ к полям класса. Для обеспечения корректной
работы с полями класса в С# введено такое понятие, как свойство метода.
Свойство выполняет работу по получению и установке значений полей,
действуя согласно прописанным требованиям.
Для примера рассмотрим класс, имеющий два поля: возраст и год
рождения. Поле возраст хранит число, определяющее количество полных
лет человека. Поле год рождения, соответственно, хранит год рождения
человека. Как вы понимаете, эти поля связаны между собой очень жестко.
Если предоставить программисту открытый доступ к полю возраст,
то может возникнуть рассогласование данных в ходе работы программы,
потому что изменение поля возраст не изменит значения поля
год рождения.
Рассмотрим пример описанной выше программы:
using System;
namespace Properties
class People
{
public int birthday;
public int age;
public People (int age)
t
this.age = age;
this.birthday = 2003-age;
class PeopleApp
(
static void Main(string[] args)
(
People pep = new People(22);
pep.age = 30;
130 Раздел II. Фундаментальные понятия
Console.WriteLine("Возраст: {0}, Год рождения: (1)",
pep.age, pep,birthday);
}
Результат работы программы будет следующий:
Возраст: 30, Год рождения: 1981
Это отнюдь не правильный результат. Если сейчас 2003 год, то при
возрасте сотрудника в 30 лет его год рождения должен быть 1973. Однако
значение birthday не изменилось после изменения значения age.
Один из способов избежать подобных недочетов - использование методов-аксессоров.
Методы-аксессоры позволяют закрепить за каждым
полем функцию, которая будет устанавливать значение поля или возвращать
данные о значении поля.
Вот как выглядит усовершенствованный вариант предыдущего примера
с использованием методов-аксессоров:
using System;
namespace Properties
(
class People
{
private int birthday;
privare int age;
public void SetAge(int age)
{
this.age = age;
this.birthday = 2003-age;
}
public int GetAgeO
(
return this.age;
1
public void SetBirthday(int birthday)
{
this.birthday = birthday;
this.age = 2003-birthday;
)
public int GetBirthdayO
E
return this.birthday;
public People(int age)
Свойства 131
this.age = age;
this.birthday = 2003-age;
class PeopleApp
{
static void Main(string[] args)
{
People pep = new People(22);
pep.SetAge(30);
Console.WriteLine("Возраст: {Of, Год рождения: {1
pep.GetAge(), pep.GetBirthday() ) ;
Результат работы программы будет следующий:
Возраст: 30, Год рождения: 1973
В этом примере для каждого поля определены функции Get и Set. Они
предназначены для управления доступом к полям age и birthday, которые
в данном случае являются скрытыми для внешнего использования. Функции
GetAge и GetBirthday просто возвращают значения соответствующих
полей. Функции SetAge и SetBirthday устанавливают значения соответствующих
переменных и, кроме того, производят дополнительные действия
по уравновешиванию зависимых переменных. Если вызывается функция
SetAge, то значение age изменяется напрямую, а значение birthday изменяется
по формуле 2003 - age. Такое же действие происходит и при вызове
функции SetBirthday.
Однако использование функций-аксессоров вызывает некоторые неудобства.
Функции-аксессоры имеют различные имена, заставляют использовать
передачу параметров и анализ возвращаемого значения. Применение
свойств помогает избежать этих неудобств. Свойства позволяют обращаться
к членам класса так, как будто вы обращаетесь к полям. И вам
даже не приходится задумываться, есть ли эти поля на самом деле. Вот
как будет выглядеть наш пример с использованием свойств:
using System;
namespace Properties
{
class People
{
private int birthday;
private int age;
public int Age
132 Раздел I I . Фундаментальные понятия
age = value;
birthday - 2003 -value;
}
get
{
return age;
}
t
public int Birthday
t
set
{
birthday = value;
age = 2003 -value;
)
get
{
return birthday;
public People(int age)
{
this.age = age;
this.birthday = 2003-sge;
class PeopleApp
static void Main(string[] args)
People pep = new People(22);
pep.Age = 30;
Console.WriteLine("Возраст: (0), Год рождения: {1}",
pep.Age, pep.Birthday);
Свойство на С# состоит из объявления поля и методов-аксесоров, применяемых
для изменения значения поля. Эти методы-аксессоры называются
получатель (getter) и установщик (setter). Методы-получатели используются
для получения значения поля, а установщики - для его изСвойства
менения. В нашем примере создано поле People.age и свойство People.Age.
Не стоит думать, что совпадение имен имеет какое-то значение - нет.
Разница между полями в регистре заглавной буквы призвана лишь улучшить
читабельность кода. People.Age - это не поле, а свойство, представляющее
собой универсальное средство определения аксессоров для членов
класса, что позволяет использовать более интуитивно понятный синтаксис
вида объект.поле. Свойство имеет два стандартных метода-аксессора: get
и set. Заметьте также, что свойство не принимает значения аргумента.
Аргумент имеет по умолчанию имя value.
На самом деле компилятор преобразует определение вашего свойства в
вызовы функций-аксессоров. То, какой из методов-аксессоров будет вызываться,
определяется местоположением свойства. Если свойство используется
слева от оператора присваивания, то вызывается функция установщик:
pep.Age - 30;
Если же свойство используется для получения значения, то вызывается
функция получатель:
Console.WriteLine("Возраст: {0}, Год рождения: {1}",
pep.Age, pep.Birthday);
СВОЙСТВА ТОЛЬКО ДЛЯ ЧТЕНИЯ
В нашем примере оба свойства считаются доступными и для чтения, и
для записи. Однако вы можете определить свойство как доступное только
для чтения. Определять свойство только для записи не имеет смысла,
поскольку им нельзя будет воспользоваться. Для того чтобы определить
свойство как доступное только для чтения, вам необходимо определить
для него только функцию получатель. Вот как можно сделать для нашего
примера свойство Birthday доступным только для чтения:
using System;
namespace Properties
{
class People
(
private int birthday;
private int age;
public int Age
age =• value;
birthday - 2003 -value;
\
get
134 Раздел I I . Фундаментальные понятия
return age;
}
public int Birthday
Посмотри в окно!
Чтобы сохранить великий дар природы — зрение,
врачи рекомендуют читать непрерывно не более 45–50 минут,
а потом делать перерыв для ослабления мышц глаза.
В перерывах между чтением полезны
гимнастические упражнения: переключение зрения с ближней точки на более дальнюю.
get
return birthday;
}
public People(int age)
this.age = age;
this.birthday = 2003-age;
class PeopleApp
static void Main(string[j args)
People pep = new People(22);
pep.Age - 30;
//pep.Birthday •- 1800;
Console.WriteLine("Возраст: {0}, Год рождения:
pep.Age, pep.Birthday);
Здесь свойство People.Birthday имеет только функцию-получатель. Если
вы попробуете использовать в функции Main код для изменения свойства
People.Birthday, то программа не скомпилируется. Раскомментируйте в программе
строку
//pep.Birthday = 1800;
Попробуйте скомпилировать программу. Компилятор выдаст ошибку с
сообщением, что данное свойство доступно только для чтения.
СВОЙСТВА И НАСЛЕДОВАНИЕ
Свойства, как и методы, могут быть перегружены в производных классах.
Для свойств также могут задаваться модификаторы virtual, override
или abstract. Подробно о наследовании говорилось в главе "Методы", сейСвойства
час я лишь хочу еще раз привести подробный пример использования
механизма виртуализации с применением свойств:
using System;
enum COLORS
!
RED,
GREEN,
BLUE
namespace Properties
(
abstract class Base
{
protected COLORS color;
public abstract COLORS Color
{
get;
set;
protected int size;
public virtual int Size
size - value;
//изменение размера объекта
Console.WriteLine("Изменение размера объекта {0}", value!
)
get
{
return size;
class Circle: Base
{
public override COLORS Color
get
return color;
136 Раздел I I . Фундаментальные понятия
color - value;
//код для изменения цвета окружности
Console.WriteLine("Изменение цвета окружности {0}", color.ToString())
private int radius,
public int Radius
{
get
!
return radius;
public override int Size
{
get:
(
return size;
}
set
i
size = value;
radius = value;
//код для перерисовки окружности с новым размером
Console.WriteLine("Изменение размера (радиуса) окружности {0) ", value!
class Bar: Base
{
public override COLORS Color
{
get
return color;
set
\
color = value;
Свойства 137
//код для изменения цвета квадрата
Console.WriteLine("Изменение цвета квадрата {0 } ", value.ToString ()
class PeopleApp
{
static void Main(string[] args)
Base baseObj;
Circle circle = new Circle!);
Bar bar = new Bar();
//работаем с объектом Circle через базовый класс
baseObj = circle;
//будет использовано перегруженное свойство класса Circle
circle.Color = COLORS.BLUE;
//будет использовано перегруженное свойство класса Circle
circle.Size = 10;
//работаем с объектом Ваг через базовый класс
baseObj = bar;
//будет использовано перегруженное свойство класса Ваг
baseObj.Color - COLORS.GREEN;
//будет использовано свойство базового класса
baseObj.Size = 50;
Условно говоря, данное приложение предназначено для рисования
окружностей и квадратов. Для наглядности объекты могут иметь только
три цвета, которые определены перечислением COLORS.
enum COLORS
{
RED,
GREEN,
BLUE
138 Раздел I I . Фундаментальные понятия
В качестве основы для построения объектов взят абстрактный базовый
класс Base.
abstract class Base
{
protected COLORS color;
public abstract COLORS Color
(
get;
set;
protected int size;
public virtual int Size
(
set { ... }
get { ... }
Этот класс имеет два защищенных поля color и size. Для доступа к
данным полям используются свойства Color и Size.
Свойство Color объявлено с модификатором abstract. Это означает, что
в базовом классе нет реализации данного свойства и все производные
классы обязательно должны перегрузить это свойство. О том, что свойство
Color в базовом классе не имеет реализации, свидетельствуют пустые
функции установщик и получатель.
Свойство Size объявлено со спецификатором virtual. Это означает, что
производные классы могут использовать данное свойство как в варианте
базового класса, так и определить свой вариант реализации свойства.
Поэтому свойство Size имеет реализацию в классе Base.
Код программы содержит два производных класса от класса Base: Circle
и Bar.
class Circle: Ease f ... j
class Bar: Base { ... }
Поскольку класс Base является абстрактным и содержит абстрактное
свойство Color, то оба производных класса обязаны реализовать у
себя свойство Color. Они перегружают свойство Color, устанавливая не
только значение цвета, но и реализуя код, для раскраски реальных
объектов. Заметьте, что свойство Color имеет тип перечисления COLORS.
Поэтому переменная value, видимая внутри метода-установщика или
метода-получателя, имеет тип перечисления COLORS. О том, что мы
перегружаем абстрактное свойство базового класса, свидетельствует
ключевое слово override, используемое при объявлении свойств в производных
классах.
public override COLORS Color
Свойство Size не является абстрактным, поэтому производные классы
могут не перегружать его. Именно так и сделано в классе Ваг. Он не
содержит перегруженного свойства Size.
Свойства 139
В свою очередь класс Circle содержит перегруженное свойство Size. Оно
перегружено таким образом, что, кроме установки значения поля size
базового класса, устанавливает значение поля radius.
public override inn Size
{
get { ... j
set
{
size = value;
radius = value;
//код для перерисовки окружности с новым размером
Console.WriteLine("Изменение размера (радиуса) окружности )0}", value);
Класс PeopleApp является своего рода клиентом, использующим созданные
нами классы.
В первой строке функции Main объявляется объект класса Base. Заметьте,
именно объявляется, а не создается. Объект абстрактного класса создать
нельзя.
Base baseObj;
Далее формируются два объекта производных классов.
Circle circle - new Circled;
Bar bar = new Bar();
Для наглядной демонстрации механизма виртуализации будем работать
со свойствами производных классов через объект базового класса.
Первым делом присваиваем переменной baseObj объект circle. Как уже
говорилось, объект производного класса может быть приведен к типу
базового класса, но не наоборот. При этом все виртуальные свойства и
методы базового класса заменятся перегруженными свойствами и методами
производного класса.
baseObj = circle;
Объект circle имеет тип Circle, который содержит два перегруженных
свойства Color и Size. Когда мы будем устанавливать значения этих свойств,
то будет происходить обращение к экземплярам свойств из производного
класса.
circle.Color = COLORS.BLUE;
circle.Size = 10;
При выполнении данных строк кода на экране появится:
Изменение цвета окружности BLUE
Изменение размера (радиуса) окружности 10
После этого переменной baseObj присваивается объект bar, имеющий
тип класса Ваг.
baseObj = bar;
Теперь baseObj при обращении к свойству Color будет использовать
экземпляр свойства Color класса Ваг. Со свойством Size дело обстоит иначе.
Класс Ваг не имеет перегруженного экземпляра свойства Size. Поэто140
Раздел I I . Фундаментальные понятия
му, при обращении к свойству Size, обращение будет происходить к свойству
базового класса.
baseObj.Color = COLORS.GREEN;
baseObj.Size = 50;
При выполнении этих строк кода на экран выведется:
Изменение цвета квадрата GREEN
Изменение размера объекта 50
ДОПОЛНИТЕЛЬНЫЕ ВОЗМОЖНОСТИ СВОЙСТВ
Одним из преимуществ использования свойств является реализация
методики отложенной инициализации. При этой методике некоторые
члены класса не инициализируются, пока не потребуется.
Отложенная инициализация дает преимущества, если у вас имеется
класс с членами, на которые редко ссылаются и на инициализацию которых
уходит много времени и ресурсов. Типичным примером могут
служить ситуации, в которых требуется считать значения из базы данных
или получить данные по сети. Если вам известно, что на эти члены
ссылаются редко, а на их инициализацию требуется много ресурсов, то
инициализацию полей можно отложить до вызова методов получателей.
Для примера давайте рассмотрим класс, имеющий свойство, значение
которого необходимо получать из базы данных. Допустим, у вас имеется
многопользовательская система. При поставке этой системы заказчику
вы продаете определенное количество лицензий. Количество лицензий
означает количество экземпляров программы, которые могут быть одновременно
запущены. Данные о количестве уже запущенных экземпляров
программы хранятся на удаленном сервере в базе данных. Получить это
значение возможно, лишь затратив большое количество ресурсов. Вашему
классу необходима проверка лицензии только в редких случаях. Поэтому
нет необходимости включать код для получения значения о количестве
лицензий в конструктор класса. Проще включить код для инициализации
поля в функцию получатель соответствующего свойства.
class Customer
t
private int licenseCount;
public int LicenseCount
{
get
{
// получить значение количества используемых
// лицензий с удаленного сервера и записать
// их в переменную licenseCount
return licenseCount;
12. МАССИВЫ
В главе 4 я уже рассказывал о применении массивов как типов данных.
Но понятия массивов в С# распространяются гораздо шире, нежели
просто объединение множества значений в переменной с одним имененм.
В С# массивы являются объектами, производными от базового класса
System.Array. Поэтому, несмотря на синтаксис определения массивов, аналогичный
языку C++, на самом деле происходит создание экземпляра
класса, унаследованного от System.Array.
ОДНОМЕРНЫЕ МАССИВЫ
Если вы объявляете массив как
int[] arr;
то объявляете объект класса, производный от System.Array.
Как уже отмечалось, для создания объекта массива необходимо использовать
оператор new. Приведу полный пример приложения, работающего
с массивами.
using System;
namespace C_Sharprogramming
{
class ArrClass
(
public int[] arr;
public ArrClassО
{
arr = new int [5];
for(int i = 0; i " 5; i++)
{
arr[i] = i;
class ArrApp
i
static void Main(string[] args)
{
ArrClass arrObj = new ArrClass ();
142 Раздел I I . Фундаментальные понятия
for(int i = 0; i " 5; i++)
{
Console.WriteLinef"arr[(0}]: = (If", i, arrObj.arr[i]);
]
Класс ArrClass содержит объявление массива arr. В конструкторе класса
массив arr инициализируется пятью элементами.
Функция Main создает экземпляр класса ArrClass. При создании экземпляра
класса вызывается конструктор ArrClass, который инициализирует
массив. После инициализации функция Main выводит значения элементов
массива на экран.
arrfO]: = О
arrfl]: = 1
arr[2]: = 2
arr[3J: = 3
arr[4]: = 4
МНОГОМЕРНЫЕ МАССИВЫ
О многомерных массивах я уже рассказывал в главе 4. Но далеко не
все было упомянуто. Начнем с того, что представляет собой многомерный
массив.
Многомерный массив - это массив, элементами которого являются
массивы. Если массив имеет порядок N, то элементами такого массива
являются массивы порядка N-1. Порядок еще называется рангом массива.
Массив может иметь любой ранг, хотя вряд ли вам пригодится массив
ранга 10, обычно используют одно-, двухранговые (двумерные) массивы.
В некоторых случаях применяются массивы с рангом три.
Рассмотрим двумерный массив.
Двумерный массив, представленный на
рис. 12.1, состоит из N строк и М столбцов.
Значит, его размерность составляет N на М.
Если вы захотите объявить такой массив, то
это необходимо сделать следующим образом:
i n t [ , ] arr = new inc[N, M];
Каждая из N строк содержит по М элементов
(столбцов). При обращении к элементу
массива следует указать номер строки и номер
столбца.
int n = a r r [Ni# M ]
Таким же способом можно работать с трех-,
четерых-, N-мерными массивами. Не утружм
л ( \
Рис. 12.1. Двумерный массив
Массивы 143
дайте себя представлением N-мерного пространства. Это ни к чему. Лучше
представьте себе использование многомерного массива. Например,
необходимо разработать программу, которая использует следующую модель
данных: имеется ограниченное число университетов, в каждом университете
существует несколько факультетов, на каждом факультете студенты
обучаются несколько курсов. Вам необходимо хранить информацию
обо всех студентах. Как вы это сделаете? Сразу хочу отметить, что
и количество университетов, и количество факультетов, и количество
курсов являются известными строго ограниченными значениями, потому
как массивы используются для статического хранения данных. Вы не
можете в любой момент изменить размерность массива, и в этом заключается
самый большой недостаток массивов. Именно из-за того, что размерность
массива должна быть заранее известна, массивы используются
лишь в простейших случаях хранения данных. Для динамического хранения
данных предназначены списки, но об этом немного позже.
Рассмотрим далее наш пример. У вас имеется N университетов, М факультетов
и К курсов, на каждом курсе учится S студентов. Массив какой
размерности вам придется использовать для хранения этих данных? Ответ
однозначный - четырехмерный. То есть, мы объявим наш массив как:
string arr [,,,];
arr = new string [N,M, K, S] ;.
Каждая ячейка массива arr[N] является трехмерным массивом. Значит,
если мы знаем индекс университета в массиве, то, написав arr[id
...Закладка в соц.сетях